diff --git a/src/components/AttachButton.js b/src/components/AttachButton.js
deleted file mode 100644
index 4b3b4f98a5..0000000000
--- a/src/components/AttachButton.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import styled from '@stream-io/styled-components';
-import iconAddAttachment from '../images/icons/plus-outline.png';
-import { themed } from '../styles/theme';
-import PropTypes from 'prop-types';
-
-const Container = styled.TouchableOpacity`
- margin-right: 8;
- ${({ theme }) => theme.messageInput.attachButton.css}
-`;
-
-const AttachButtonIcon = styled.Image`
- width: 15;
- height: 15;
- ${({ theme }) => theme.messageInput.attachButtonIcon.css}
-`;
-
-/**
- * UI Component for attach button in MessageInput component.
- *
- * @extends PureComponent
- * @example ./docs/AttachButton.md
- */
-export const AttachButton = themed(
- class AttachButton extends React.PureComponent {
- static themePath = 'messageInput';
- static propTypes = {
- handleOnPress: PropTypes.func,
- disabled: PropTypes.bool,
- };
- static defaultProps = {
- disabled: false,
- };
-
- render() {
- const { handleOnPress, disabled } = this.props;
- return (
-
-
-
- );
- }
- },
-);
diff --git a/src/components/Attachment.js b/src/components/Attachment.js
deleted file mode 100644
index 522235ebe6..0000000000
--- a/src/components/Attachment.js
+++ /dev/null
@@ -1,280 +0,0 @@
-import React from 'react';
-import { View } from 'react-native';
-
-import { themed } from '../styles/theme';
-
-import PropTypes from 'prop-types';
-import { Card } from './Card';
-import { FileIcon } from './FileIcon';
-import { AttachmentActions } from './AttachmentActions';
-import { Gallery } from './Gallery';
-
-import { withMessageContentContext } from '../context';
-import { FileAttachment } from './FileAttachment';
-
-/**
- * Attachment - The message attachment
- *
- * @example ./docs/Attachment.md
- * @extends PureComponent
- */
-export const Attachment = withMessageContentContext(
- themed(
- class Attachment extends React.PureComponent {
- static themePath = 'attachment';
- static propTypes = {
- /** The attachment to render */
- attachment: PropTypes.object.isRequired,
- /**
- * Position of message. 'right' | 'left'
- * 'right' message belongs with current user while 'left' message belonds to other users.
- * */
- alignment: PropTypes.string,
- /** Handler for actions. Actions in combination with attachments can be used to build [commands](https://getstream.io/chat/docs/#channel_commands). */
- actionHandler: PropTypes.func,
- /**
- * Position of message in group - top, bottom, middle, single.
- *
- * Message group is a group of consecutive messages from same user. groupStyles can be used to style message as per their position in message group
- * e.g., user avatar (to which message belongs to) is only showed for last (bottom) message in group.
- */
- groupStyle: PropTypes.oneOf(['single', 'top', 'middle', 'bottom']),
- /** Handler for long press event on attachment */
- onLongPress: PropTypes.func,
- /**
- * Provide any additional props for child `TouchableOpacity`.
- * Please check docs for TouchableOpacity for supported props - https://reactnative.dev/docs/touchableopacity#props
- */
- additionalTouchableProps: PropTypes.object,
- /**
- * Custom UI component to display enriched url preview.
- * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/Card.js
- */
- UrlPreview: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * Custom UI component to display Giphy image.
- * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/Card.js
- */
- Giphy: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
- /**
- * Custom UI component to display group of File type attachments or multiple file attachments (in single message).
- * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/FileAttachmentGroup.js
- */
- FileAttachmentGroup: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * Custom UI component to display File type attachment.
- * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/FileAttachment.js
- */
- FileAttachment: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * Custom UI component for attachment icon for type 'file' attachment.
- * Defaults to: https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/FileIcon.js
- */
- AttachmentFileIcon: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * Custom UI component to display image attachments.
- * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/Gallery.js
- */
- Gallery: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
- /**
- * Custom UI component to display generic media type e.g. giphy, url preview etc
- * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/Card.js
- */
- Card: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
- /**
- * Custom UI component to override default header of Card component.
- * Accepts the same props as Card component.
- */
- CardHeader: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * Custom UI component to override default cover (between Header and Footer) of Card component.
- * Accepts the same props as Card component.
- */
- CardCover: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
- /**
- * Custom UI component to override default Footer of Card component.
- * Accepts the same props as Card component.
- */
- CardFooter: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * Custom UI component to display attachment actions. e.g., send, shuffle, cancel in case of giphy
- * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/AttachmentActions.js
- */
- AttachmentActions: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- };
- static defaultProps = {
- AttachmentFileIcon: FileIcon,
- AttachmentActions,
- Gallery,
- Card,
- FileAttachment,
- };
-
- constructor(props) {
- super(props);
- }
-
- render() {
- const {
- attachment: a,
- Gallery,
- Card,
- CardHeader,
- CardCover,
- CardFooter,
- FileAttachment,
- AttachmentActions,
- } = this.props;
- if (!a) {
- return null;
- }
-
- const Giphy = this.props.Giphy ? this.props.Giphy : Card;
- const UrlPreview = this.props.UrlPreview ? this.props.UrlPreview : Card;
-
- const cardProps = {
- Header: CardHeader ? CardHeader : undefined,
- Cover: CardCover ? CardCover : undefined,
- Footer: CardFooter ? CardFooter : undefined,
- };
- let type;
-
- if (a.type === 'giphy' || a.type === 'imgur') {
- type = 'giphy';
- } else if (
- (a.title_link || a.og_scrape_url) &&
- (a.image_url || a.thumb_url)
- ) {
- type = 'urlPreview';
- } else if (a.type === 'image') {
- type = 'image';
- } else if (a.type === 'file') {
- type = 'file';
- } else if (a.type === 'audio') {
- type = 'audio';
- } else if (a.type === 'video') {
- type = 'media';
- } else if (a.type === 'product') {
- type = 'product';
- } else {
- type = 'card';
- // extra = 'no-image';
- }
-
- if (type === 'image') {
- return (
-
-
- {a.actions && a.actions.length > 0 && (
-
- )}
-
- );
- }
- if (type === 'giphy') {
- if (a.actions && a.actions.length) {
- return (
-
-
- {a.actions && a.actions.length > 0 && (
-
- )}
-
- );
- } else {
- return (
-
- );
- }
- }
- if (type === 'card') {
- if (a.actions && a.actions.length) {
- return (
-
-
- {a.actions && a.actions.length > 0 && (
-
- )}
-
- );
- } else {
- return ;
- }
- }
-
- if (type === 'urlPreview') {
- return (
-
- );
- }
-
- if (type === 'file') {
- const {
- AttachmentFileIcon,
- actionHandler,
- onLongPress,
- alignment,
- groupStyle,
- } = this.props;
-
- return (
-
- );
- }
-
- if (type === 'media' && a.asset_url && a.image_url) {
- return (
- // TODO: Put in video component
-
- );
- }
-
- return false;
- }
- },
- ),
-);
diff --git a/src/components/Attachment/Attachment.js b/src/components/Attachment/Attachment.js
new file mode 100644
index 0000000000..80a2706fc5
--- /dev/null
+++ b/src/components/Attachment/Attachment.js
@@ -0,0 +1,264 @@
+import React from 'react';
+import { View } from 'react-native';
+
+import { themed } from '../../styles/theme';
+
+import PropTypes from 'prop-types';
+
+import Card from './Card';
+import FileIcon from './FileIcon';
+import AttachmentActions from './AttachmentActions';
+import Gallery from './Gallery';
+import FileAttachment from './FileAttachment';
+
+import { withMessageContentContext } from '../../context';
+
+/**
+ * Attachment - The message attachment
+ *
+ * @example ./docs/Attachment.md
+ * @extends PureComponent
+ */
+class Attachment extends React.PureComponent {
+ static themePath = 'attachment';
+ static propTypes = {
+ /** The attachment to render */
+ attachment: PropTypes.object.isRequired,
+ /**
+ * Position of message. 'right' | 'left'
+ * 'right' message belongs with current user while 'left' message belonds to other users.
+ * */
+ alignment: PropTypes.string,
+ /** Handler for actions. Actions in combination with attachments can be used to build [commands](https://getstream.io/chat/docs/#channel_commands). */
+ actionHandler: PropTypes.func,
+ /**
+ * Position of message in group - top, bottom, middle, single.
+ *
+ * Message group is a group of consecutive messages from same user. groupStyles can be used to style message as per their position in message group
+ * e.g., user avatar (to which message belongs to) is only showed for last (bottom) message in group.
+ */
+ groupStyle: PropTypes.oneOf(['single', 'top', 'middle', 'bottom']),
+ /** Handler for long press event on attachment */
+ onLongPress: PropTypes.func,
+ /**
+ * Provide any additional props for child `TouchableOpacity`.
+ * Please check docs for TouchableOpacity for supported props - https://reactnative.dev/docs/touchableopacity#props
+ */
+ additionalTouchableProps: PropTypes.object,
+ /**
+ * Custom UI component to display enriched url preview.
+ * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/Card.js
+ */
+ UrlPreview: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ /**
+ * Custom UI component to display Giphy image.
+ * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/Card.js
+ */
+ Giphy: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ /**
+ * Custom UI component to display group of File type attachments or multiple file attachments (in single message).
+ * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/FileAttachmentGroup.js
+ */
+ FileAttachmentGroup: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /**
+ * Custom UI component to display File type attachment.
+ * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/FileAttachment.js
+ */
+ FileAttachment: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /**
+ * Custom UI component for attachment icon for type 'file' attachment.
+ * Defaults to: https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/FileIcon.js
+ */
+ AttachmentFileIcon: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /**
+ * Custom UI component to display image attachments.
+ * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/Gallery.js
+ */
+ Gallery: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ /**
+ * Custom UI component to display generic media type e.g. giphy, url preview etc
+ * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/Card.js
+ */
+ Card: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ /**
+ * Custom UI component to override default header of Card component.
+ * Accepts the same props as Card component.
+ */
+ CardHeader: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ /**
+ * Custom UI component to override default cover (between Header and Footer) of Card component.
+ * Accepts the same props as Card component.
+ */
+ CardCover: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ /**
+ * Custom UI component to override default Footer of Card component.
+ * Accepts the same props as Card component.
+ */
+ CardFooter: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ /**
+ * Custom UI component to display attachment actions. e.g., send, shuffle, cancel in case of giphy
+ * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/AttachmentActions.js
+ */
+ AttachmentActions: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ };
+ static defaultProps = {
+ AttachmentFileIcon: FileIcon,
+ AttachmentActions,
+ Gallery,
+ Card,
+ FileAttachment,
+ };
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const {
+ attachment: a,
+ Gallery,
+ Card,
+ CardHeader,
+ CardCover,
+ CardFooter,
+ FileAttachment,
+ AttachmentActions,
+ } = this.props;
+ if (!a) {
+ return null;
+ }
+
+ const Giphy = this.props.Giphy ? this.props.Giphy : Card;
+ const UrlPreview = this.props.UrlPreview ? this.props.UrlPreview : Card;
+
+ const cardProps = {
+ Header: CardHeader ? CardHeader : undefined,
+ Cover: CardCover ? CardCover : undefined,
+ Footer: CardFooter ? CardFooter : undefined,
+ };
+ let type;
+
+ if (a.type === 'giphy' || a.type === 'imgur') {
+ type = 'giphy';
+ } else if (
+ (a.title_link || a.og_scrape_url) &&
+ (a.image_url || a.thumb_url)
+ ) {
+ type = 'urlPreview';
+ } else if (a.type === 'image') {
+ type = 'image';
+ } else if (a.type === 'file') {
+ type = 'file';
+ } else if (a.type === 'audio') {
+ type = 'audio';
+ } else if (a.type === 'video') {
+ type = 'media';
+ } else if (a.type === 'product') {
+ type = 'product';
+ } else {
+ type = 'card';
+ // extra = 'no-image';
+ }
+
+ if (type === 'image') {
+ return (
+
+
+ {a.actions && a.actions.length > 0 && (
+
+ )}
+
+ );
+ }
+ if (type === 'giphy') {
+ if (a.actions && a.actions.length) {
+ return (
+
+
+ {a.actions && a.actions.length > 0 && (
+
+ )}
+
+ );
+ } else {
+ return ;
+ }
+ }
+ if (type === 'card') {
+ if (a.actions && a.actions.length) {
+ return (
+
+
+ {a.actions && a.actions.length > 0 && (
+
+ )}
+
+ );
+ } else {
+ return ;
+ }
+ }
+
+ if (type === 'urlPreview') {
+ return (
+
+ );
+ }
+
+ if (type === 'file') {
+ const {
+ AttachmentFileIcon,
+ actionHandler,
+ onLongPress,
+ alignment,
+ groupStyle,
+ } = this.props;
+
+ return (
+
+ );
+ }
+
+ if (type === 'media' && a.asset_url && a.image_url) {
+ return (
+ // TODO: Put in video component
+
+ );
+ }
+
+ return false;
+ }
+}
+
+export default withMessageContentContext(themed(Attachment));
diff --git a/src/components/AttachmentActions.js b/src/components/Attachment/AttachmentActions.js
similarity index 62%
rename from src/components/AttachmentActions.js
rename to src/components/Attachment/AttachmentActions.js
index e46d199ce2..ff80c2e99c 100644
--- a/src/components/AttachmentActions.js
+++ b/src/components/Attachment/AttachmentActions.js
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import { Text, TouchableOpacity } from 'react-native';
import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
-import registerCSS from '../css.macro';
+import { themed } from '../../styles/theme';
+import registerCSS from '../../css.macro';
const Container = registerCSS(
'message.actions.container',
@@ -55,35 +55,35 @@ const ButtonText = registerCSS(
* @example ./docs/AttachmentActions.md
* @extends PureComponent
*/
-export const AttachmentActions = themed(
- class AttachmentActions extends React.PureComponent {
- static themePath = 'message.actions';
- static propTypes = {
- // /** The id of the form input */
- // id: PropTypes.string.isRequired,
- /** The text for the form input */
- text: PropTypes.string,
- /** A list of actions */
- actions: PropTypes.array.isRequired,
- /** The handler to execute after selecting an action */
- actionHandler: PropTypes.func.isRequired,
- };
+class AttachmentActions extends React.PureComponent {
+ static themePath = 'message.actions';
+ static propTypes = {
+ // /** The id of the form input */
+ // id: PropTypes.string.isRequired,
+ /** The text for the form input */
+ text: PropTypes.string,
+ /** A list of actions */
+ actions: PropTypes.array.isRequired,
+ /** The handler to execute after selecting an action */
+ actionHandler: PropTypes.func.isRequired,
+ };
- render() {
- const { id, actions, actionHandler } = this.props;
- return (
-
- {actions.map((action) => (
-
- ))}
-
- );
- }
- },
-);
+ render() {
+ const { id, actions, actionHandler } = this.props;
+ return (
+
+ {actions.map((action) => (
+
+ ))}
+
+ );
+ }
+}
+
+export default themed(AttachmentActions);
diff --git a/src/components/Attachment/Card.js b/src/components/Attachment/Card.js
new file mode 100644
index 0000000000..3ddfd1a5e1
--- /dev/null
+++ b/src/components/Attachment/Card.js
@@ -0,0 +1,161 @@
+import React from 'react';
+import { View, Linking } from 'react-native';
+import PropTypes from 'prop-types';
+import giphyLogo from '../../assets/Poweredby_100px-White_VertText.png';
+import { themed } from '../../styles/theme';
+import { withMessageContentContext } from '../../context';
+
+import styled from '@stream-io/styled-components';
+import { makeImageCompatibleUrl } from '../../utils';
+
+const Container = styled.TouchableOpacity`
+ border-top-left-radius: 16;
+ border-top-right-radius: 16;
+ overflow: hidden;
+ border-bottom-left-radius: ${({ alignment }) =>
+ alignment === 'right' ? 16 : 2};
+ border-bottom-right-radius: ${({ alignment }) =>
+ alignment === 'left' ? 16 : 2};
+ background-color: ${({ theme }) => theme.colors.light};
+ width: 250;
+ ${({ theme }) => theme.message.card.container.css}
+`;
+
+const CardCover = styled.Image`
+ display: flex;
+ height: 150;
+ ${({ theme }) => theme.message.card.cover.css}
+`;
+
+const CardFooter = styled.View`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 10px;
+ ${({ theme }) => theme.message.card.footer.css}
+`;
+
+const FooterTitle = styled.Text`
+ ${({ theme }) => theme.message.card.footer.title.css}
+`;
+const FooterDescription = styled.Text`
+ ${({ theme }) => theme.message.card.footer.description.css}
+`;
+const FooterLink = styled.Text`
+ ${({ theme }) => theme.message.card.footer.link.css}
+`;
+const FooterLogo = styled.Image`
+ ${({ theme }) => theme.message.card.footer.logo.css}
+`;
+/**
+ * UI component for card in attachments.
+ *
+ * @example ./docs/Card.md
+ * @extends PureComponent
+ */
+class Card extends React.Component {
+ static themePath = 'card';
+ static propTypes = {
+ /** Title retured by the OG scraper */
+ title: PropTypes.string,
+ /** Link retured by the OG scraper */
+ title_link: PropTypes.string,
+ /** The scraped url, used as a fallback if the OG-data doesnt include a link */
+ og_scrape_url: PropTypes.string,
+ /** The url of the full sized image */
+ image_url: PropTypes.string,
+ /** The url for thumbnail sized image*/
+ thumb_url: PropTypes.string,
+ /** Description retured by the OG scraper */
+ text: PropTypes.string,
+ type: PropTypes.string,
+ alignment: PropTypes.string,
+ onLongPress: PropTypes.func,
+ /**
+ * Provide any additional props for child `TouchableOpacity`.
+ * Please check docs for TouchableOpacity for supported props - https://reactnative.dev/docs/touchableopacity#props
+ */
+ additionalTouchableProps: PropTypes.object,
+ Header: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ Cover: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ Footer: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ };
+
+ constructor(props) {
+ super(props);
+ }
+
+ trimUrl = (url) => {
+ let trimmedUrl;
+ if (url !== undefined && url !== null) {
+ trimmedUrl = url
+ .replace(/^(?:https?:\/\/)?(?:www\.)?/i, '')
+ .split('/')[0];
+ }
+ return trimmedUrl;
+ };
+
+ _goToURL = (url) => {
+ Linking.canOpenURL(url).then((supported) => {
+ if (supported) {
+ Linking.openURL(url);
+ } else {
+ console.log("Don't know how to open URI: " + url);
+ }
+ });
+ };
+
+ render() {
+ const {
+ image_url,
+ thumb_url,
+ title,
+ text,
+ title_link,
+ og_scrape_url,
+ type,
+ alignment,
+ onLongPress,
+ additionalTouchableProps,
+ Header,
+ Cover,
+ Footer,
+ } = this.props;
+ const uri = makeImageCompatibleUrl(image_url || thumb_url);
+ return (
+ {
+ this._goToURL(og_scrape_url || image_url || thumb_url);
+ }}
+ onLongPress={onLongPress}
+ alignment={alignment}
+ {...additionalTouchableProps}
+ >
+ {Header && }
+ {Cover && }
+ {uri && !Cover && }
+ {Footer ? (
+
+ ) : (
+
+
+ {title && {title}}
+ {text && {text}}
+
+ {this.trimUrl(title_link || og_scrape_url)}
+
+
+ {type === 'giphy' && }
+
+ )}
+
+ );
+ }
+}
+export default withMessageContentContext(themed(Card));
diff --git a/src/components/FileAttachment.js b/src/components/Attachment/FileAttachment.js
similarity index 72%
rename from src/components/FileAttachment.js
rename to src/components/Attachment/FileAttachment.js
index 64480ffb10..051ceb82f3 100644
--- a/src/components/FileAttachment.js
+++ b/src/components/Attachment/FileAttachment.js
@@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
import styled from '@stream-io/styled-components';
-import { AttachmentActions } from './AttachmentActions';
-import { withMessageContentContext } from '../context';
+import AttachmentActions from './AttachmentActions';
+import { withMessageContentContext } from '../../context';
const FileContainer = styled.View`
display: flex;
@@ -57,44 +57,42 @@ const goToURL = (url) => {
});
};
-export const FileAttachment = withMessageContentContext(
- ({
- attachment,
- actionHandler,
- AttachmentFileIcon,
- onLongPress,
- alignment,
- groupStyle,
- additionalTouchableProps,
- }) => (
- {
- goToURL(attachment.asset_url);
- }}
- onLongPress={onLongPress}
- {...additionalTouchableProps}
- >
-
-
-
-
- {attachment.title}
-
- {attachment.file_size} KB
-
-
- {attachment.actions && attachment.actions.length > 0 && (
-
- )}
-
- ),
+const FileAttachment = ({
+ attachment,
+ actionHandler,
+ AttachmentFileIcon,
+ onLongPress,
+ alignment,
+ groupStyle,
+ additionalTouchableProps,
+}) => (
+ {
+ goToURL(attachment.asset_url);
+ }}
+ onLongPress={onLongPress}
+ {...additionalTouchableProps}
+ >
+
+
+
+
+ {attachment.title}
+
+ {attachment.file_size} KB
+
+
+ {attachment.actions && attachment.actions.length > 0 && (
+
+ )}
+
);
FileAttachment.propTypes = {
@@ -134,3 +132,5 @@ FileAttachment.propTypes = {
PropTypes.elementType,
]),
};
+
+export default withMessageContentContext(FileAttachment);
diff --git a/src/components/FileAttachmentGroup.js b/src/components/Attachment/FileAttachmentGroup.js
similarity index 94%
rename from src/components/FileAttachmentGroup.js
rename to src/components/Attachment/FileAttachmentGroup.js
index 8247ab1b19..31ef3df161 100644
--- a/src/components/FileAttachmentGroup.js
+++ b/src/components/Attachment/FileAttachmentGroup.js
@@ -1,14 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from '@stream-io/styled-components';
-import { Attachment } from './Attachment';
+import Attachment from './Attachment';
const Container = styled.View`
display: flex;
align-items: stretch;
`;
-export class FileAttachmentGroup extends React.PureComponent {
+class FileAttachmentGroup extends React.PureComponent {
static propTypes = {
messageId: PropTypes.string,
files: PropTypes.array,
@@ -77,3 +77,5 @@ export class FileAttachmentGroup extends React.PureComponent {
);
}
}
+
+export default FileAttachmentGroup;
diff --git a/src/components/FileIcon.js b/src/components/Attachment/FileIcon.js
similarity index 94%
rename from src/components/FileIcon.js
rename to src/components/Attachment/FileIcon.js
index 81c723030b..12c4322692 100644
--- a/src/components/FileIcon.js
+++ b/src/components/Attachment/FileIcon.js
@@ -1,11 +1,11 @@
import * as React from 'react';
import styled from '@stream-io/styled-components';
-import iconPDF from '../images/PDF.png';
-import iconDOC from '../images/DOC.png';
-import iconPPT from '../images/PPT.png';
-import iconXLS from '../images/XLS.png';
-import iconTAR from '../images/TAR.png';
+import iconPDF from '../../images/PDF.png';
+import iconDOC from '../../images/DOC.png';
+import iconPPT from '../../images/PPT.png';
+import iconXLS from '../../images/XLS.png';
+import iconTAR from '../../images/TAR.png';
const Icon = styled.Image`
${({ theme }) => theme.message.file.icon.css};
@@ -205,7 +205,7 @@ function mimeTypeToIcon(mimeType) {
return iconDOC;
}
-export class FileIcon extends React.Component {
+export default class FileIcon extends React.Component {
render() {
const { mimeType, size } = this.props;
return (
diff --git a/src/components/Gallery.js b/src/components/Attachment/Gallery.js
similarity index 96%
rename from src/components/Gallery.js
rename to src/components/Attachment/Gallery.js
index 23390f4166..c54a3577f0 100644
--- a/src/components/Gallery.js
+++ b/src/components/Attachment/Gallery.js
@@ -3,11 +3,14 @@ import { Text, View, Modal, Image, SafeAreaView } from 'react-native';
import ImageViewer from 'react-native-image-zoom-viewer';
import PropTypes from 'prop-types';
import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
-import { withMessageContentContext, withTranslationContext } from '../context';
-import { makeImageCompatibleUrl } from '../utils';
+import { themed } from '../../styles/theme';
+import {
+ withMessageContentContext,
+ withTranslationContext,
+} from '../../context';
+import { makeImageCompatibleUrl } from '../../utils';
-import { CloseButton } from './CloseButton';
+import { CloseButton } from '../CloseButton';
const Single = styled.TouchableOpacity`
display: flex;
@@ -286,8 +289,6 @@ const GalleryHeader = ({ handleDismiss }) => (
);
-const GalleyWithContext = withTranslationContext(
+export default withTranslationContext(
withMessageContentContext(themed(Gallery)),
);
-
-export { GalleyWithContext as Gallery };
diff --git a/src/components/Attachment/index.js b/src/components/Attachment/index.js
new file mode 100644
index 0000000000..9e9c0b165c
--- /dev/null
+++ b/src/components/Attachment/index.js
@@ -0,0 +1,7 @@
+export { default as Attachment } from './Attachment';
+export { default as AttachmentActions } from './AttachmentActions';
+export { default as Card } from './Card';
+export { default as Gallery } from './Gallery';
+export { default as FileAttachment } from './FileAttachment';
+export { default as FileIcon } from './FileIcon';
+export { default as FileAttachmentGroup } from './FileAttachmentGroup';
diff --git a/src/components/AutoCompleteInput.js b/src/components/AutoCompleteInput/AutoCompleteInput.js
similarity index 97%
rename from src/components/AutoCompleteInput.js
rename to src/components/AutoCompleteInput/AutoCompleteInput.js
index f101d7d230..863a39c5e6 100644
--- a/src/components/AutoCompleteInput.js
+++ b/src/components/AutoCompleteInput/AutoCompleteInput.js
@@ -1,7 +1,8 @@
import React from 'react';
import styled from '@stream-io/styled-components';
import PropTypes from 'prop-types';
-import { withTranslationContext } from '../context';
+
+import { withTranslationContext } from '../../context';
const InputBox = styled.TextInput`
max-height: 60px;
@@ -254,5 +255,4 @@ class AutoCompleteInput extends React.PureComponent {
}
}
-const AutoCompleteInputWithContext = withTranslationContext(AutoCompleteInput);
-export { AutoCompleteInputWithContext as AutoCompleteInput };
+export default withTranslationContext(AutoCompleteInput);
diff --git a/src/components/CommandsItem.js b/src/components/AutoCompleteInput/CommandsItem.js
similarity index 55%
rename from src/components/CommandsItem.js
rename to src/components/AutoCompleteInput/CommandsItem.js
index 4caeb495d6..0a3fdf1b50 100644
--- a/src/components/CommandsItem.js
+++ b/src/components/AutoCompleteInput/CommandsItem.js
@@ -1,7 +1,8 @@
import React from 'react';
import styled from '@stream-io/styled-components';
import PropTypes from 'prop-types';
-import { themed } from '../styles/theme';
+
+import { themed } from '../../styles/theme';
const Container = styled.View`
flex-direction: column;
@@ -32,29 +33,29 @@ const CommandDescription = styled.Text`
* @example ./docs/CommandsItem.md
* @extends PureComponent
*/
-export const CommandsItem = themed(
- class CommandsItem extends React.Component {
- static themePath = 'messageInput.suggestions.command';
- static propTypes = {
- name: PropTypes.string,
- args: PropTypes.string,
- description: PropTypes.string,
- };
-
- render() {
- const {
- item: { name, args, description },
- } = this.props;
-
- return (
-
-
- /{name}
- {args}
-
- {description}
-
- );
- }
- },
-);
+class CommandsItem extends React.Component {
+ static themePath = 'messageInput.suggestions.command';
+ static propTypes = {
+ name: PropTypes.string,
+ args: PropTypes.string,
+ description: PropTypes.string,
+ };
+
+ render() {
+ const {
+ item: { name, args, description },
+ } = this.props;
+
+ return (
+
+
+ /{name}
+ {args}
+
+ {description}
+
+ );
+ }
+}
+
+export default themed(CommandsItem);
diff --git a/src/components/MentionsItem.js b/src/components/AutoCompleteInput/MentionsItem.js
similarity index 53%
rename from src/components/MentionsItem.js
rename to src/components/AutoCompleteInput/MentionsItem.js
index 53fdfd2cf0..0bdf722625 100644
--- a/src/components/MentionsItem.js
+++ b/src/components/AutoCompleteInput/MentionsItem.js
@@ -1,9 +1,9 @@
import React from 'react';
-import { Avatar } from './Avatar';
+import { Avatar } from '../Avatar';
import PropTypes from 'prop-types';
import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
+import { themed } from '../../styles/theme';
const Container = styled.View`
flex-direction: row;
@@ -19,25 +19,25 @@ const Name = styled.Text`
${({ theme }) => theme.messageInput.suggestions.mention.name.css}
`;
-export const MentionsItem = themed(
- class MentionsItem extends React.Component {
- static themePath = 'messageInput.suggestions.mention';
- render() {
- const {
- item: { name, image, id },
- } = this.props;
- return (
-
-
- {name || id}
-
- );
- }
- },
-);
+class MentionsItem extends React.Component {
+ static themePath = 'messageInput.suggestions.mention';
+ render() {
+ const {
+ item: { name, image, id },
+ } = this.props;
+ return (
+
+
+ {name || id}
+
+ );
+ }
+}
MentionsItem.propTypes = {
name: PropTypes.string,
image: PropTypes.string,
id: PropTypes.string,
};
+
+export default themed(MentionsItem);
diff --git a/src/components/AutoCompleteInput/index.js b/src/components/AutoCompleteInput/index.js
new file mode 100644
index 0000000000..a9d283cff7
--- /dev/null
+++ b/src/components/AutoCompleteInput/index.js
@@ -0,0 +1,3 @@
+export { default as AutoCompleteInput } from './AutoCompleteInput';
+export { default as CommandsItem } from './CommandsItem';
+export { default as MentionsItem } from './MentionsItem';
diff --git a/src/components/Avatar.js b/src/components/Avatar.js
deleted file mode 100644
index ce7e7f6213..0000000000
--- a/src/components/Avatar.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
-
-const BASE_AVATAR_FALLBACK_TEXT_SIZE = 14;
-const BASE_AVATAR_SIZE = 32;
-
-const AvatarContainer = styled.View`
- display: flex;
- align-items: center;
- ${({ theme }) => theme.avatar.container.css}
-`;
-
-const AvatarImage = styled.Image`
- border-radius: ${({ size }) => size / 2};
- width: ${({ size }) => size};
- height: ${({ size }) => size};
- ${({ theme }) => theme.avatar.image.css}
-`;
-
-const AvatarFallback = styled.View`
- border-radius: ${({ size }) => size / 2};
- width: ${({ size }) => size};
- height: ${({ size }) => size};
- background-color: ${({ theme }) => theme.colors.primary};
- justify-content: center;
- align-items: center;
- ${({ theme }) => theme.avatar.fallback.css}
-`;
-
-const AvatarText = styled.Text`
- color: ${({ theme }) => theme.colors.textLight};
- text-transform: uppercase;
- font-size: ${({ fontSize }) => fontSize};
- font-weight: bold;
- ${({ theme }) => theme.avatar.text.css}
-`;
-
-/**
- * Avatar - A round avatar image with fallback to user's initials
- *
- * @example ./docs/Avatar.md
- * @extends PureComponent
- */
-export const Avatar = themed(
- class Avatar extends React.PureComponent {
- static themePath = 'avatar';
- static propTypes = {
- /** image url */
- image: PropTypes.string,
- /** name of the picture, used for title tag fallback */
- name: PropTypes.string,
- /** size in pixels */
- size: PropTypes.number,
- /** Style overrides */
- style: PropTypes.object,
- };
-
- static defaultProps = {
- size: 32,
- };
-
- state = {
- imageError: false,
- };
-
- setError = () => {
- this.setState({
- imageError: true,
- });
- };
-
- getInitials = (name) =>
- name
- ? name
- .split(' ')
- .slice(0, 2)
- .map((name) => name.charAt(0))
- : null;
-
- render() {
- const { size, name, image } = this.props;
- const initials = this.getInitials(name);
- const fontSize =
- BASE_AVATAR_FALLBACK_TEXT_SIZE * (size / BASE_AVATAR_SIZE);
-
- return (
-
- {image && !this.state.imageError ? (
-
- ) : (
-
- {initials}
-
- )}
-
- );
- }
- },
-);
diff --git a/src/components/Avatar/Avatar.js b/src/components/Avatar/Avatar.js
new file mode 100644
index 0000000000..7edfebd48b
--- /dev/null
+++ b/src/components/Avatar/Avatar.js
@@ -0,0 +1,107 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import styled from '@stream-io/styled-components';
+import { themed } from '../../styles/theme';
+
+const BASE_AVATAR_FALLBACK_TEXT_SIZE = 14;
+const BASE_AVATAR_SIZE = 32;
+
+const AvatarContainer = styled.View`
+ display: flex;
+ align-items: center;
+ ${({ theme }) => theme.avatar.container.css}
+`;
+
+const AvatarImage = styled.Image`
+ border-radius: ${({ size }) => size / 2};
+ width: ${({ size }) => size};
+ height: ${({ size }) => size};
+ ${({ theme }) => theme.avatar.image.css}
+`;
+
+const AvatarFallback = styled.View`
+ border-radius: ${({ size }) => size / 2};
+ width: ${({ size }) => size};
+ height: ${({ size }) => size};
+ background-color: ${({ theme }) => theme.colors.primary};
+ justify-content: center;
+ align-items: center;
+ ${({ theme }) => theme.avatar.fallback.css}
+`;
+
+const AvatarText = styled.Text`
+ color: ${({ theme }) => theme.colors.textLight};
+ text-transform: uppercase;
+ font-size: ${({ fontSize }) => fontSize};
+ font-weight: bold;
+ ${({ theme }) => theme.avatar.text.css}
+`;
+
+/**
+ * Avatar - A round avatar image with fallback to user's initials
+ *
+ * @example ./docs/Avatar.md
+ * @extends PureComponent
+ */
+class Avatar extends React.PureComponent {
+ static themePath = 'avatar';
+ static propTypes = {
+ /** image url */
+ image: PropTypes.string,
+ /** name of the picture, used for title tag fallback */
+ name: PropTypes.string,
+ /** size in pixels */
+ size: PropTypes.number,
+ /** Style overrides */
+ style: PropTypes.object,
+ };
+
+ static defaultProps = {
+ size: 32,
+ };
+
+ state = {
+ imageError: false,
+ };
+
+ setError = () => {
+ this.setState({
+ imageError: true,
+ });
+ };
+
+ getInitials = (name) =>
+ name
+ ? name
+ .split(' ')
+ .slice(0, 2)
+ .map((name) => name.charAt(0))
+ : null;
+
+ render() {
+ const { size, name, image } = this.props;
+ const initials = this.getInitials(name);
+ const fontSize = BASE_AVATAR_FALLBACK_TEXT_SIZE * (size / BASE_AVATAR_SIZE);
+
+ return (
+
+ {image && !this.state.imageError ? (
+
+ ) : (
+
+ {initials}
+
+ )}
+
+ );
+ }
+}
+
+export default themed(Avatar);
diff --git a/src/components/Avatar/index.js b/src/components/Avatar/index.js
new file mode 100644
index 0000000000..fdf35d3a1e
--- /dev/null
+++ b/src/components/Avatar/index.js
@@ -0,0 +1 @@
+export { default as Avatar } from './Avatar';
diff --git a/src/components/Card.js b/src/components/Card.js
deleted file mode 100644
index a3cf0766f9..0000000000
--- a/src/components/Card.js
+++ /dev/null
@@ -1,164 +0,0 @@
-import React from 'react';
-import { View, Linking } from 'react-native';
-import PropTypes from 'prop-types';
-import giphyLogo from '../assets/Poweredby_100px-White_VertText.png';
-import { themed } from '../styles/theme';
-import { withMessageContentContext } from '../context';
-
-import styled from '@stream-io/styled-components';
-import { makeImageCompatibleUrl } from '../utils';
-
-const Container = styled.TouchableOpacity`
- border-top-left-radius: 16;
- border-top-right-radius: 16;
- overflow: hidden;
- border-bottom-left-radius: ${({ alignment }) =>
- alignment === 'right' ? 16 : 2};
- border-bottom-right-radius: ${({ alignment }) =>
- alignment === 'left' ? 16 : 2};
- background-color: ${({ theme }) => theme.colors.light};
- width: 250;
- ${({ theme }) => theme.message.card.container.css}
-`;
-
-const CardCover = styled.Image`
- display: flex;
- height: 150;
- ${({ theme }) => theme.message.card.cover.css}
-`;
-
-const CardFooter = styled.View`
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- padding: 10px;
- ${({ theme }) => theme.message.card.footer.css}
-`;
-
-const FooterTitle = styled.Text`
- ${({ theme }) => theme.message.card.footer.title.css}
-`;
-const FooterDescription = styled.Text`
- ${({ theme }) => theme.message.card.footer.description.css}
-`;
-const FooterLink = styled.Text`
- ${({ theme }) => theme.message.card.footer.link.css}
-`;
-const FooterLogo = styled.Image`
- ${({ theme }) => theme.message.card.footer.logo.css}
-`;
-/**
- * UI component for card in attachments.
- *
- * @example ./docs/Card.md
- * @extends PureComponent
- */
-export const Card = withMessageContentContext(
- themed(
- class Card extends React.Component {
- static themePath = 'card';
- static propTypes = {
- /** Title retured by the OG scraper */
- title: PropTypes.string,
- /** Link retured by the OG scraper */
- title_link: PropTypes.string,
- /** The scraped url, used as a fallback if the OG-data doesnt include a link */
- og_scrape_url: PropTypes.string,
- /** The url of the full sized image */
- image_url: PropTypes.string,
- /** The url for thumbnail sized image*/
- thumb_url: PropTypes.string,
- /** Description retured by the OG scraper */
- text: PropTypes.string,
- type: PropTypes.string,
- alignment: PropTypes.string,
- onLongPress: PropTypes.func,
- /**
- * Provide any additional props for child `TouchableOpacity`.
- * Please check docs for TouchableOpacity for supported props - https://reactnative.dev/docs/touchableopacity#props
- */
- additionalTouchableProps: PropTypes.object,
- Header: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
- Cover: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
- Footer: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
- };
-
- constructor(props) {
- super(props);
- }
-
- trimUrl = (url) => {
- let trimmedUrl;
- if (url !== undefined && url !== null) {
- trimmedUrl = url
- .replace(/^(?:https?:\/\/)?(?:www\.)?/i, '')
- .split('/')[0];
- }
- return trimmedUrl;
- };
-
- _goToURL = (url) => {
- Linking.canOpenURL(url).then((supported) => {
- if (supported) {
- Linking.openURL(url);
- } else {
- console.log("Don't know how to open URI: " + url);
- }
- });
- };
-
- render() {
- const {
- image_url,
- thumb_url,
- title,
- text,
- title_link,
- og_scrape_url,
- type,
- alignment,
- onLongPress,
- additionalTouchableProps,
- Header,
- Cover,
- Footer,
- } = this.props;
- const uri = makeImageCompatibleUrl(image_url || thumb_url);
- return (
- {
- this._goToURL(og_scrape_url || image_url || thumb_url);
- }}
- onLongPress={onLongPress}
- alignment={alignment}
- {...additionalTouchableProps}
- >
- {Header && }
- {Cover && }
- {uri && !Cover && }
- {Footer ? (
-
- ) : (
-
-
- {title && {title}}
- {text && {text}}
-
- {this.trimUrl(title_link || og_scrape_url)}
-
-
- {type === 'giphy' && }
-
- )}
-
- );
- }
- },
- ),
-);
diff --git a/src/components/Channel.js b/src/components/Channel/Channel.js
similarity index 94%
rename from src/components/Channel.js
rename to src/components/Channel/Channel.js
index 1739c24a4f..c335b609fe 100644
--- a/src/components/Channel.js
+++ b/src/components/Channel/Channel.js
@@ -1,5 +1,4 @@
import React, { PureComponent } from 'react';
-import { withChatContext, withTranslationContext } from '../context';
import { Text } from 'react-native';
// import { LoadingIndicator } from './LoadingIndicator';
@@ -8,9 +7,10 @@ import PropTypes from 'prop-types';
// import { MessageSimple } from './MessageSimple';
// import { Attachment } from './Attachment';
-import { ChannelInner } from './ChannelInner';
-import { KeyboardCompatibleView } from './KeyboardCompatibleView';
+import ChannelInner from './ChannelInner';
+import { KeyboardCompatibleView } from '../KeyboardCompatibleView';
+import { withChatContext, withTranslationContext } from '../../context';
/**
* Channel - Wrapper component for a channel. It needs to be place inside of the Chat component.
* ChannelHeader, MessageList, Thread and MessageInput should be used as children of the Channel component.
@@ -145,5 +145,4 @@ class Channel extends PureComponent {
}
}
-const ChannelWithContext = withTranslationContext(withChatContext(Channel));
-export { ChannelWithContext as Channel };
+export default withTranslationContext(withChatContext(Channel));
diff --git a/src/components/ChannelInner.js b/src/components/Channel/ChannelInner.js
similarity index 97%
rename from src/components/ChannelInner.js
rename to src/components/Channel/ChannelInner.js
index 79fcd31dec..6c2af1c9b3 100644
--- a/src/components/ChannelInner.js
+++ b/src/components/Channel/ChannelInner.js
@@ -1,18 +1,21 @@
import React, { PureComponent } from 'react';
import { View, Text } from 'react-native';
-import { ChannelContext, withTranslationContext } from '../context';
-import { SuggestionsProvider } from './SuggestionsProvider';
import uuidv4 from 'uuid/v4';
import PropTypes from 'prop-types';
import Immutable from 'seamless-immutable';
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
-import { emojiData } from '../utils';
+import { emojiData } from '../../utils';
-import { LoadingIndicator } from './LoadingIndicator';
-import { LoadingErrorIndicator } from './LoadingErrorIndicator';
-import { EmptyStateIndicator } from './EmptyStateIndicator';
+import {
+ LoadingIndicator,
+ LoadingErrorIndicator,
+ EmptyStateIndicator,
+} from '../Indicators';
+import { SuggestionsProvider } from '../SuggestionsProvider';
+
+import { ChannelContext, withTranslationContext } from '../../context';
import { logChatPromiseExecution } from 'stream-chat';
/**
@@ -700,6 +703,4 @@ class ChannelInner extends PureComponent {
}
}
-const ChannelInnerWithContext = withTranslationContext(ChannelInner);
-
-export { ChannelInnerWithContext as ChannelInner };
+export default withTranslationContext(ChannelInner);
diff --git a/src/components/Channel/index.js b/src/components/Channel/index.js
new file mode 100644
index 0000000000..80cc492709
--- /dev/null
+++ b/src/components/Channel/index.js
@@ -0,0 +1,2 @@
+export { default as Channel } from './Channel';
+export { default as ChannelInner } from './ChannelInner';
diff --git a/src/components/ChannelList.js b/src/components/ChannelList.js
deleted file mode 100644
index b8a2d18206..0000000000
--- a/src/components/ChannelList.js
+++ /dev/null
@@ -1,750 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import { ChannelPreviewMessenger } from './ChannelPreviewMessenger';
-import { withChatContext } from '../context';
-import { ChannelListMessenger } from './ChannelListMessenger';
-import Immutable from 'seamless-immutable';
-import debounce from 'lodash/debounce';
-
-import { LoadingIndicator } from './LoadingIndicator';
-import { LoadingErrorIndicator } from './LoadingErrorIndicator';
-import { EmptyStateIndicator } from './EmptyStateIndicator';
-import uniqBy from 'lodash/uniqBy';
-import uniqWith from 'lodash/uniqWith';
-import isEqual from 'lodash/isEqual';
-
-export const isPromise = (thing) => {
- const promise = thing && typeof thing.then === 'function';
- return promise;
-};
-
-export const DEFAULT_QUERY_CHANNELS_LIMIT = 10;
-export const MAX_QUERY_CHANNELS_LIMIT = 30;
-
-/**
- * ChannelList - A preview list of channels, allowing you to select the channel you want to open.
- * This components doesn't provide any UI for the list. UI is provided by component `List` which should be
- * provided to this component as prop. By default ChannelListMessenger is used a list UI.
- *
- * @extends PureComponent
- * @example ./docs/ChannelList.md
- */
-const ChannelList = withChatContext(
- class ChannelList extends PureComponent {
- static propTypes = {
- /** The Preview to use, defaults to [ChannelPreviewMessenger](https://getstream.github.io/stream-chat-react-native/#channelpreviewmessenger) */
- Preview: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
-
- /** The loading indicator to use */
- LoadingIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /** The indicator to use when there is error in fetching channels */
- LoadingErrorIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /** The indicator to use when channel list is empty */
- EmptyStateIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * The indicator to display network-down error at top of list, if there is connectivity issue
- * Default: [ChannelListHeaderNetworkDownIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListHeaderNetworkDownIndicator)
- */
- HeaderNetworkDownIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * The indicator to display error at top of list, if there was an error loading some page/channels after the first page.
- * Default: [ChannelListHeaderErrorIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListHeaderErrorIndicator)
- */
- HeaderErrorIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * Loading indicator to display at bottom of the list, while loading further pages.
- * Default: [ChannelListFooterLoadingIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListFooterLoadingIndicator)
- */
- FooterLoadingIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- List: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
- onSelect: PropTypes.func,
- /**
- * Function that overrides default behaviour when new message is received on channel that is not being watched
- *
- * @param {Component} thisArg Reference to ChannelList component
- * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `notification.message_new` event
- * */
- onMessageNew: PropTypes.func,
- /**
- * Function that overrides default behaviour when users gets added to a channel
- *
- * @param {Component} thisArg Reference to ChannelList component
- * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `notification.added_to_channel` event
- * */
- onAddedToChannel: PropTypes.func,
- /**
- * Function that overrides default behaviour when users gets removed from a channel
- *
- * @param {Component} thisArg Reference to ChannelList component
- * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `notification.removed_from_channel` event
- * */
- onRemovedFromChannel: PropTypes.func,
- /**
- * Function that overrides default behaviour when channel gets updated
- *
- * @param {Component} thisArg Reference to ChannelList component
- * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `channel.updated` event
- * */
- onChannelUpdated: PropTypes.func,
- /**
- * Function to customize behaviour when channel gets truncated
- *
- * @param {Component} thisArg Reference to ChannelList component
- * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `channel.truncated` event
- * */
- onChannelTruncated: PropTypes.func,
- /**
- * Function that overrides default behaviour when channel gets deleted. In absence of this prop, channel will be removed from the list.
- *
- * @param {Component} thisArg Reference to ChannelList component
- * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `channel.deleted` event
- * */
- onChannelDeleted: PropTypes.func,
- /**
- * Function that overrides default behaviour when channel gets hidden. In absence of this prop, channel will be removed from the list.
- *
- * @param {Component} thisArg Reference to ChannelList component
- * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `channel.hidden` event
- * */
- onChannelHidden: PropTypes.func,
- /**
- * Object containing query filters
- * @see See [Channel query documentation](https://getstream.io/chat/docs/query_channels) for a list of available fields for filter.
- * */
- filters: PropTypes.object,
- /**
- * Object containing query options
- * @see See [Channel query documentation](https://getstream.io/chat/docs/query_channels) for a list of available fields for options.
- * */
- options: PropTypes.object,
- /**
- * Object containing sort parameters
- * @see See [Channel query documentation](https://getstream.io/chat/docs/query_channels) for a list of available fields for sort.
- * */
- sort: PropTypes.object,
- /** For flatlist */
- loadMoreThreshold: PropTypes.number,
- /** Client object. Avaiable from [Chat context](#chatcontext) */
- client: PropTypes.object,
- /**
- * Function to set change active channel. This function acts as bridge between channel list and currently active channel component.
- *
- * @param channel A Channel object
- */
- setActiveChannel: PropTypes.func,
- /**
- * If true, channels won't be dynamically sorted by most recent message.
- */
- lockChannelOrder: PropTypes.bool,
- /**
- * Besides existing (default) UX behaviour of underlying flatlist of ChannelList component, if you want
- * to attach some additional props to un derlying flatlist, you can add it to following prop.
- *
- * You can find list of all the available FlatList props here - https://facebook.github.io/react-native/docs/flatlist#props
- *
- * **NOTE** Don't use `additionalFlatListProps` to get access to ref of flatlist. Use `setFlatListRef` instead.
- *
- * e.g.
- * ```
- *
- * ```
- */
- additionalFlatListProps: PropTypes.object,
- /**
- * Use `setFlatListRef` to get access to ref to inner FlatList.
- *
- * e.g.
- * ```
- * {
- * // Use ref for your own good
- * }}
- * ```
- */
- setFlatListRef: PropTypes.func,
- };
-
- static defaultProps = {
- Preview: ChannelPreviewMessenger,
- List: ChannelListMessenger,
- LoadingIndicator,
- LoadingErrorIndicator,
- EmptyStateIndicator,
- filters: {},
- options: {},
- sort: {},
- // https://github.com/facebook/react-native/blob/a7a7970e543959e9db5281914d5f132beb01db8d/Libraries/Lists/VirtualizedList.js#L466
- loadMoreThreshold: 2,
- lockChannelOrder: false,
- additionalFlatListProps: {},
- logger: () => {},
- };
-
- constructor(props) {
- super(props);
- this.state = {
- error: false,
- channels: Immutable([]),
- channelIds: Immutable([]),
- refreshing: false,
- loadingChannels: true,
- loadingNextPage: false,
- hasNextPage: true,
- offset: 0,
- };
- this.listRef = React.createRef();
- this.lastRefresh = new Date();
- this._queryChannelsDebounced = debounce(
- async (params = {}) => {
- await this.queryChannelsPromise;
- if (this.state.error) {
- return;
- }
-
- if (!this.state.hasNextPage) {
- return;
- }
-
- this.queryChannels(params.queryType);
- },
- 1000,
- {
- leading: true,
- trailing: true,
- },
- );
- this._unmounted = false;
- }
-
- async componentDidMount() {
- await this.queryChannels('reload');
- this.listenToChanges();
- }
-
- async componentDidUpdate(prevProps) {
- // do we need deepequal?
- if (
- !isEqual(prevProps.filters, this.props.filters) ||
- !isEqual(prevProps.sort, this.props.sort)
- ) {
- this._queryChannelsDebounced.cancel();
- await this.queryChannels('reload');
- }
- }
-
- componentWillUnmount() {
- this._unmounted = true;
- this.props.client.off(this.handleEvent);
- this._queryChannelsDebounced.cancel();
- }
-
- static getDerivedStateFromError(error) {
- return { error };
- }
-
- componentDidCatch(error, info) {
- console.warn(error, error.isUnmounted, info);
- }
-
- setStateAsync = (newState) => {
- if (this._unmounted) {
- this._queryChannelsDebounced.cancel();
- return;
- }
-
- return new Promise((resolve) => {
- this.setState(newState, resolve);
- });
- };
-
- wait = (ms) =>
- new Promise((resolve) => {
- setTimeout(resolve, ms);
- });
-
- queryChannelsRequest = async (filters, sort, options, retryCount = 1) => {
- let channelQueryResponse;
- try {
- channelQueryResponse = await this.props.client.queryChannels(
- filters,
- sort,
- options,
- );
- } catch (e) {
- // Wait for 2 seconds before making another attempt
- await this.wait(2000);
- // Don't try more than 3 times.
- if (retryCount === 3) {
- throw e;
- }
- return this.queryChannelsRequest(
- filters,
- sort,
- options,
- retryCount + 1,
- );
- }
-
- return channelQueryResponse;
- };
-
- getQueryParams = (queryType) => {
- const { options, filters, sort } = this.props;
- let offset;
- let limit;
-
- if (queryType === 'refresh' || queryType === 'reload') {
- offset = 0;
- limit = MAX_QUERY_CHANNELS_LIMIT;
- if (this.state.channels.length === 0) {
- limit = options.limit || DEFAULT_QUERY_CHANNELS_LIMIT;
- } else if (this.state.channels.length < MAX_QUERY_CHANNELS_LIMIT) {
- limit = Math.max(
- this.state.channels.length,
- DEFAULT_QUERY_CHANNELS_LIMIT,
- );
- }
- } else {
- limit = options.limit || DEFAULT_QUERY_CHANNELS_LIMIT;
- offset = this.state.offset;
- }
-
- const queryOptions = {
- ...options,
- offset,
- limit,
- };
-
- return {
- filters,
- sort,
- options: queryOptions,
- };
- };
-
- setRefreshingUIState = () =>
- this.setStateAsync({
- refreshing: true,
- loadingChannels: false,
- loadingNextPage: false,
- });
-
- setReloadingUIState = () =>
- this.setStateAsync({
- refreshing: false,
- loadingChannels: true,
- loadingNextPage: false,
- error: false,
- channels: Immutable([]),
- channelIds: Immutable([]),
- });
-
- setLoadingNextPageUIState = () =>
- this.setStateAsync({
- refreshing: false,
- loadingChannels: false,
- loadingNextPage: true,
- });
-
- // Sets the loading UI state before the star
- startQueryLoadingUIState = async (queryType) => {
- switch (queryType) {
- case 'refresh':
- await this.setRefreshingUIState();
- break;
- case 'reload':
- await this.setReloadingUIState();
- break;
- default:
- await this.setLoadingNextPageUIState();
- break;
- }
- };
-
- finishQueryLoadingUIState = () =>
- this.setStateAsync({
- refreshing: false,
- loadingChannels: false,
- loadingNextPage: false,
- });
-
- /**
- * queryType - 'refresh' | 'reload'
- *
- * refresh - Refresh the channel list. You will see the existing channels during refreshing.a
- * Mainly used for pull to refresh or resyning upong network recovery.
- *
- * reload - Reload the channel list from begining. You won't see existing channels during reload.
- */
- queryChannels = (queryType) => {
- // Don't query again if query is already active or there are no more results.
- this.queryChannelsPromise = new Promise(async (resolve) => {
- try {
- await this.startQueryLoadingUIState(queryType);
-
- const { filters, sort, options } = this.getQueryParams(queryType);
- const channelQueryResponse = await this.queryChannelsRequest(
- filters,
- sort,
- options,
- );
-
- // Set the active channel only in case of reload.
- if (queryType === 'reload' && channelQueryResponse.length >= 1) {
- this.props.setActiveChannel(channelQueryResponse[0]);
- }
-
- this.finishQueryLoadingUIState();
- const hasNextPage =
- channelQueryResponse.length >=
- (options.limit || DEFAULT_QUERY_CHANNELS_LIMIT);
-
- if (queryType === 'refresh' || queryType === 'reload') {
- await this.setChannels(channelQueryResponse, {
- hasNextPage,
- error: false,
- });
- } else {
- await this.appendChannels(channelQueryResponse, {
- hasNextPage,
- error: false,
- });
- }
-
- resolve(true);
- } catch (e) {
- await this.handleError(e);
-
- resolve(false);
- return;
- }
- });
-
- return this.queryChannelsPromise;
- };
-
- handleError = () => {
- this._queryChannelsDebounced.cancel();
- this.finishQueryLoadingUIState();
- return this.setStateAsync({
- error: true,
- });
- };
-
- appendChannels = (channels = [], additionalState = {}) => {
- // Remove duplicate channels in worse case we get repeted channel from backend.
- let distinctChannels = channels.filter(
- (c) => this.state.channelIds.indexOf(c.id) === -1,
- );
-
- distinctChannels = [...this.state.channels, ...distinctChannels];
- const channelIds = [
- ...this.state.channelIds,
- ...distinctChannels.map((c) => c.id),
- ];
-
- return this.setStateAsync({
- channels: distinctChannels,
- channelIds,
- offset: distinctChannels.length,
- ...additionalState,
- });
- };
-
- setChannels = (channels = [], additionalState = {}) => {
- const distinctChannels = [...channels];
- const channelIds = [...channels.map((c) => c.id)];
-
- return this.setStateAsync({
- channels: distinctChannels,
- channelIds,
- offset: distinctChannels.length,
- ...additionalState,
- });
- };
-
- listenToChanges() {
- this.props.client.on(this.handleEvent);
- }
-
- handleEvent = async (e) => {
- if (e.type === 'channel.hidden') {
- if (
- this.props.onChannelHidden &&
- typeof this.props.onChannelHidden === 'function'
- ) {
- this.props.onChannelHidden(this, e);
- } else {
- const channels = this.state.channels;
- const channelIndex = channels.findIndex(
- (channel) => channel.cid === e.cid,
- );
-
- if (channelIndex < 0) return;
-
- // Remove the hidden channel from the list.
- channels.splice(channelIndex, 1);
- this.setStateAsync({
- channels: [...channels],
- });
- }
- }
-
- if (e.type === 'user.presence.changed' || e.type === 'user.updated') {
- let newChannels = this.state.channels;
-
- newChannels = newChannels.map((channel) => {
- if (!channel.state.members[e.user.id]) return channel;
-
- channel.state.members.setIn([e.user.id, 'user'], e.user);
-
- return channel;
- });
-
- this.setStateAsync({ channels: [...newChannels] });
- }
-
- if (e.type === 'message.new') {
- !this.props.lockChannelOrder && this.moveChannelUp(e.cid);
- }
-
- // make sure to re-render the channel list after connection is recovered
- if (e.type === 'connection.recovered') {
- this._queryChannelsDebounced.cancel();
- this.queryChannels('refresh');
- }
-
- // move channel to start
- if (e.type === 'notification.message_new') {
- if (
- this.props.onMessageNew &&
- typeof this.props.onMessageNew === 'function'
- ) {
- this.props.onMessageNew(this, e);
- } else {
- const channel = await this.getChannel(e.channel.type, e.channel.id);
-
- // move channel to starting position
- if (this._unmounted) return;
- this.setState((prevState) => ({
- channels: uniqBy([channel, ...prevState.channels], 'cid'),
- channelIds: uniqWith(
- [channel.id, ...prevState.channelIds],
- isEqual,
- ),
- offset: prevState.offset + 1,
- }));
- }
- }
-
- // add to channel
- if (e.type === 'notification.added_to_channel') {
- if (
- this.props.onAddedToChannel &&
- typeof this.props.onAddedToChannel === 'function'
- ) {
- this.props.onAddedToChannel(this, e);
- } else {
- const channel = await this.getChannel(e.channel.type, e.channel.id);
-
- if (this._unmounted) return;
- this.setState((prevState) => ({
- channels: uniqBy([channel, ...prevState.channels], 'cid'),
- channelIds: uniqWith(
- [channel.id, ...prevState.channelIds],
- isEqual,
- ),
- offset: prevState.offset + 1,
- }));
- }
- }
-
- // remove from channel
- if (e.type === 'notification.removed_from_channel') {
- if (
- this.props.onRemovedFromChannel &&
- typeof this.props.onRemovedFromChannel === 'function'
- ) {
- this.props.onRemovedFromChannel(this, e);
- } else {
- if (this._unmounted) return;
- this.setState((prevState) => {
- const channels = prevState.channels.filter(
- (channel) => channel.cid !== e.channel.cid,
- );
- const channelIds = prevState.channelIds.filter(
- (cid) => cid !== e.channel.cid,
- );
- return {
- channels,
- channelIds,
- };
- });
- }
- }
-
- // Channel data is updated
- if (e.type === 'channel.updated') {
- const channels = this.state.channels;
- const channelIndex = channels.findIndex(
- (channel) => channel.cid === e.channel.cid,
- );
-
- if (channelIndex > -1) {
- channels[channelIndex].data = Immutable(e.channel);
- this.setStateAsync({
- channels: [...channels],
- });
- }
-
- if (
- this.props.onChannelUpdated &&
- typeof this.props.onChannelUpdated === 'function'
- )
- this.props.onChannelUpdated(this, e);
- }
-
- // Channel is deleted
- if (e.type === 'channel.deleted') {
- if (
- this.props.onChannelDeleted &&
- typeof this.props.onChannelDeleted === 'function'
- ) {
- this.props.onChannelDeleted(this, e);
- } else {
- const channels = this.state.channels;
- const channelIndex = channels.findIndex(
- (channel) => channel.cid === e.channel.cid,
- );
-
- if (channelIndex < 0) return;
-
- // Remove the deleted channel from the list.
- channels.splice(channelIndex, 1);
- this.setStateAsync({
- channels: [...channels],
- });
- }
- }
-
- if (e.type === 'channel.truncated') {
- this.setState((prevState) => ({
- channels: [...prevState.channels],
- }));
-
- if (
- this.props.onChannelTruncated &&
- typeof this.props.onChannelTruncated === 'function'
- )
- this.props.onChannelTruncated(this, e);
- }
-
- return null;
- };
-
- getChannel = async (type, id) => {
- const channel = this.props.client.channel(type, id);
- await channel.watch();
- return channel;
- };
-
- moveChannelUp = (cid) => {
- if (this._unmounted) return;
- const channels = this.state.channels;
-
- // get channel index
- const channelIndex = this.state.channels.findIndex(
- (channel) => channel.cid === cid,
- );
- if (channelIndex <= 0) return;
-
- // get channel from channels
- const channel = channels[channelIndex];
-
- //remove channel from current position
- channels.splice(channelIndex, 1);
- //add channel at the start
- channels.unshift(channel);
-
- // set new channel state
- if (this._unmounted) return;
- this.setStateAsync({
- channels: [...channels],
- });
- };
-
- // Refreshes the list. Existing list of channels will still be visible on UI while refreshing.
- refreshList = async () => {
- const now = new Date();
- // Only allow pull-to-refresh 10 seconds after last successful refresh.
- if (now - this.lastRefresh < 10000 && !this.state.error) {
- return;
- }
-
- // if (!this.state.error) return;
- this.listRef.scrollToIndex({ index: 0 });
- this._queryChannelsDebounced.cancel();
- const success = await this.queryChannels('refresh');
-
- if (success) this.lastRefresh = new Date();
- };
-
- // Reloads the channel list. All the existing channels will be wiped out first from UI, and then
- // queryChannels api will be called to fetch new channels.
- reloadList = () => {
- this._queryChannelsDebounced.cancel();
- this.queryChannels('reload');
- };
-
- loadNextPage = () => {
- this._queryChannelsDebounced();
- };
-
- render() {
- const context = {
- loadNextPage: this.loadNextPage,
- refreshList: this.refreshList,
- reloadList: this.reloadList,
- };
- const List = this.props.List;
- const props = { ...this.props, setActiveChannel: this.props.onSelect };
-
- return (
-
- {
- this.listRef = ref;
- this.props.setFlatListRef && this.props.setFlatListRef(ref);
- }}
- />
-
- );
- }
- },
-);
-
-export { ChannelList };
diff --git a/src/components/ChannelList/ChannelList.js b/src/components/ChannelList/ChannelList.js
new file mode 100644
index 0000000000..9d5c564a7f
--- /dev/null
+++ b/src/components/ChannelList/ChannelList.js
@@ -0,0 +1,742 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import Immutable from 'seamless-immutable';
+import debounce from 'lodash/debounce';
+
+import {
+ LoadingIndicator,
+ LoadingErrorIndicator,
+ EmptyStateIndicator,
+} from '../Indicators';
+
+import { ChannelPreviewMessenger } from '../ChannelPreview';
+import ChannelListMessenger from './ChannelListMessenger';
+
+import { withChatContext } from '../../context';
+
+import uniqBy from 'lodash/uniqBy';
+import uniqWith from 'lodash/uniqWith';
+import isEqual from 'lodash/isEqual';
+
+export const isPromise = (thing) => {
+ const promise = thing && typeof thing.then === 'function';
+ return promise;
+};
+
+export const DEFAULT_QUERY_CHANNELS_LIMIT = 10;
+export const MAX_QUERY_CHANNELS_LIMIT = 30;
+
+/**
+ * ChannelList - A preview list of channels, allowing you to select the channel you want to open.
+ * This components doesn't provide any UI for the list. UI is provided by component `List` which should be
+ * provided to this component as prop. By default ChannelListMessenger is used a list UI.
+ *
+ * @extends PureComponent
+ * @example ./docs/ChannelList.md
+ */
+class ChannelList extends PureComponent {
+ static propTypes = {
+ /** The Preview to use, defaults to [ChannelPreviewMessenger](https://getstream.github.io/stream-chat-react-native/#channelpreviewmessenger) */
+ Preview: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+
+ /** The loading indicator to use */
+ LoadingIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /** The indicator to use when there is error in fetching channels */
+ LoadingErrorIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /** The indicator to use when channel list is empty */
+ EmptyStateIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /**
+ * The indicator to display network-down error at top of list, if there is connectivity issue
+ * Default: [ChannelListHeaderNetworkDownIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListHeaderNetworkDownIndicator)
+ */
+ HeaderNetworkDownIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /**
+ * The indicator to display error at top of list, if there was an error loading some page/channels after the first page.
+ * Default: [ChannelListHeaderErrorIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListHeaderErrorIndicator)
+ */
+ HeaderErrorIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /**
+ * Loading indicator to display at bottom of the list, while loading further pages.
+ * Default: [ChannelListFooterLoadingIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListFooterLoadingIndicator)
+ */
+ FooterLoadingIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ List: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ onSelect: PropTypes.func,
+ /**
+ * Function that overrides default behaviour when new message is received on channel that is not being watched
+ *
+ * @param {Component} thisArg Reference to ChannelList component
+ * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `notification.message_new` event
+ * */
+ onMessageNew: PropTypes.func,
+ /**
+ * Function that overrides default behaviour when users gets added to a channel
+ *
+ * @param {Component} thisArg Reference to ChannelList component
+ * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `notification.added_to_channel` event
+ * */
+ onAddedToChannel: PropTypes.func,
+ /**
+ * Function that overrides default behaviour when users gets removed from a channel
+ *
+ * @param {Component} thisArg Reference to ChannelList component
+ * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `notification.removed_from_channel` event
+ * */
+ onRemovedFromChannel: PropTypes.func,
+ /**
+ * Function that overrides default behaviour when channel gets updated
+ *
+ * @param {Component} thisArg Reference to ChannelList component
+ * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `channel.updated` event
+ * */
+ onChannelUpdated: PropTypes.func,
+ /**
+ * Function to customize behaviour when channel gets truncated
+ *
+ * @param {Component} thisArg Reference to ChannelList component
+ * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `channel.truncated` event
+ * */
+ onChannelTruncated: PropTypes.func,
+ /**
+ * Function that overrides default behaviour when channel gets deleted. In absence of this prop, channel will be removed from the list.
+ *
+ * @param {Component} thisArg Reference to ChannelList component
+ * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `channel.deleted` event
+ * */
+ onChannelDeleted: PropTypes.func,
+ /**
+ * Function that overrides default behaviour when channel gets hidden. In absence of this prop, channel will be removed from the list.
+ *
+ * @param {Component} thisArg Reference to ChannelList component
+ * @param {Event} event [Event object](https://getstream.io/chat/docs/event_object) corresponding to `channel.hidden` event
+ * */
+ onChannelHidden: PropTypes.func,
+ /**
+ * Object containing query filters
+ * @see See [Channel query documentation](https://getstream.io/chat/docs/query_channels) for a list of available fields for filter.
+ * */
+ filters: PropTypes.object,
+ /**
+ * Object containing query options
+ * @see See [Channel query documentation](https://getstream.io/chat/docs/query_channels) for a list of available fields for options.
+ * */
+ options: PropTypes.object,
+ /**
+ * Object containing sort parameters
+ * @see See [Channel query documentation](https://getstream.io/chat/docs/query_channels) for a list of available fields for sort.
+ * */
+ sort: PropTypes.object,
+ /** For flatlist */
+ loadMoreThreshold: PropTypes.number,
+ /** Client object. Avaiable from [Chat context](#chatcontext) */
+ client: PropTypes.object,
+ /**
+ * Function to set change active channel. This function acts as bridge between channel list and currently active channel component.
+ *
+ * @param channel A Channel object
+ */
+ setActiveChannel: PropTypes.func,
+ /**
+ * If true, channels won't be dynamically sorted by most recent message.
+ */
+ lockChannelOrder: PropTypes.bool,
+ /**
+ * Besides existing (default) UX behaviour of underlying flatlist of ChannelList component, if you want
+ * to attach some additional props to un derlying flatlist, you can add it to following prop.
+ *
+ * You can find list of all the available FlatList props here - https://facebook.github.io/react-native/docs/flatlist#props
+ *
+ * **NOTE** Don't use `additionalFlatListProps` to get access to ref of flatlist. Use `setFlatListRef` instead.
+ *
+ * e.g.
+ * ```
+ *
+ * ```
+ */
+ additionalFlatListProps: PropTypes.object,
+ /**
+ * Use `setFlatListRef` to get access to ref to inner FlatList.
+ *
+ * e.g.
+ * ```
+ * {
+ * // Use ref for your own good
+ * }}
+ * ```
+ */
+ setFlatListRef: PropTypes.func,
+ };
+
+ static defaultProps = {
+ Preview: ChannelPreviewMessenger,
+ List: ChannelListMessenger,
+ LoadingIndicator,
+ LoadingErrorIndicator,
+ EmptyStateIndicator,
+ filters: {},
+ options: {},
+ sort: {},
+ // https://github.com/facebook/react-native/blob/a7a7970e543959e9db5281914d5f132beb01db8d/Libraries/Lists/VirtualizedList.js#L466
+ loadMoreThreshold: 2,
+ lockChannelOrder: false,
+ additionalFlatListProps: {},
+ logger: () => {},
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ error: false,
+ channels: Immutable([]),
+ channelIds: Immutable([]),
+ refreshing: false,
+ loadingChannels: true,
+ loadingNextPage: false,
+ hasNextPage: true,
+ offset: 0,
+ };
+ this.listRef = React.createRef();
+ this.lastRefresh = new Date();
+ this._queryChannelsDebounced = debounce(
+ async (params = {}) => {
+ await this.queryChannelsPromise;
+ if (this.state.error) {
+ return;
+ }
+
+ if (!this.state.hasNextPage) {
+ return;
+ }
+
+ this.queryChannels(params.queryType);
+ },
+ 1000,
+ {
+ leading: true,
+ trailing: true,
+ },
+ );
+ this._unmounted = false;
+ }
+
+ async componentDidMount() {
+ await this.queryChannels('reload');
+ this.listenToChanges();
+ }
+
+ async componentDidUpdate(prevProps) {
+ // do we need deepequal?
+ if (
+ !isEqual(prevProps.filters, this.props.filters) ||
+ !isEqual(prevProps.sort, this.props.sort)
+ ) {
+ this._queryChannelsDebounced.cancel();
+ await this.queryChannels('reload');
+ }
+ }
+
+ componentWillUnmount() {
+ this._unmounted = true;
+ this.props.client.off(this.handleEvent);
+ this._queryChannelsDebounced.cancel();
+ }
+
+ static getDerivedStateFromError(error) {
+ return { error };
+ }
+
+ componentDidCatch(error, info) {
+ console.warn(error, error.isUnmounted, info);
+ }
+
+ setStateAsync = (newState) => {
+ if (this._unmounted) {
+ this._queryChannelsDebounced.cancel();
+ return;
+ }
+
+ return new Promise((resolve) => {
+ this.setState(newState, resolve);
+ });
+ };
+
+ wait = (ms) =>
+ new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+
+ queryChannelsRequest = async (filters, sort, options, retryCount = 1) => {
+ let channelQueryResponse;
+ try {
+ channelQueryResponse = await this.props.client.queryChannels(
+ filters,
+ sort,
+ options,
+ );
+ } catch (e) {
+ // Wait for 2 seconds before making another attempt
+ await this.wait(2000);
+ // Don't try more than 3 times.
+ if (retryCount === 3) {
+ throw e;
+ }
+ return this.queryChannelsRequest(filters, sort, options, retryCount + 1);
+ }
+
+ return channelQueryResponse;
+ };
+
+ getQueryParams = (queryType) => {
+ const { options, filters, sort } = this.props;
+ let offset;
+ let limit;
+
+ if (queryType === 'refresh' || queryType === 'reload') {
+ offset = 0;
+ limit = MAX_QUERY_CHANNELS_LIMIT;
+ if (this.state.channels.length === 0) {
+ limit = options.limit || DEFAULT_QUERY_CHANNELS_LIMIT;
+ } else if (this.state.channels.length < MAX_QUERY_CHANNELS_LIMIT) {
+ limit = Math.max(
+ this.state.channels.length,
+ DEFAULT_QUERY_CHANNELS_LIMIT,
+ );
+ }
+ } else {
+ limit = options.limit || DEFAULT_QUERY_CHANNELS_LIMIT;
+ offset = this.state.offset;
+ }
+
+ const queryOptions = {
+ ...options,
+ offset,
+ limit,
+ };
+
+ return {
+ filters,
+ sort,
+ options: queryOptions,
+ };
+ };
+
+ setRefreshingUIState = () =>
+ this.setStateAsync({
+ refreshing: true,
+ loadingChannels: false,
+ loadingNextPage: false,
+ });
+
+ setReloadingUIState = () =>
+ this.setStateAsync({
+ refreshing: false,
+ loadingChannels: true,
+ loadingNextPage: false,
+ error: false,
+ channels: Immutable([]),
+ channelIds: Immutable([]),
+ });
+
+ setLoadingNextPageUIState = () =>
+ this.setStateAsync({
+ refreshing: false,
+ loadingChannels: false,
+ loadingNextPage: true,
+ });
+
+ // Sets the loading UI state before the star
+ startQueryLoadingUIState = async (queryType) => {
+ switch (queryType) {
+ case 'refresh':
+ await this.setRefreshingUIState();
+ break;
+ case 'reload':
+ await this.setReloadingUIState();
+ break;
+ default:
+ await this.setLoadingNextPageUIState();
+ break;
+ }
+ };
+
+ finishQueryLoadingUIState = () =>
+ this.setStateAsync({
+ refreshing: false,
+ loadingChannels: false,
+ loadingNextPage: false,
+ });
+
+ /**
+ * queryType - 'refresh' | 'reload'
+ *
+ * refresh - Refresh the channel list. You will see the existing channels during refreshing.a
+ * Mainly used for pull to refresh or resyning upong network recovery.
+ *
+ * reload - Reload the channel list from begining. You won't see existing channels during reload.
+ */
+ queryChannels = (queryType) => {
+ // Don't query again if query is already active or there are no more results.
+ this.queryChannelsPromise = new Promise(async (resolve) => {
+ try {
+ await this.startQueryLoadingUIState(queryType);
+
+ const { filters, sort, options } = this.getQueryParams(queryType);
+ const channelQueryResponse = await this.queryChannelsRequest(
+ filters,
+ sort,
+ options,
+ );
+
+ // Set the active channel only in case of reload.
+ if (queryType === 'reload' && channelQueryResponse.length >= 1) {
+ this.props.setActiveChannel(channelQueryResponse[0]);
+ }
+
+ this.finishQueryLoadingUIState();
+ const hasNextPage =
+ channelQueryResponse.length >=
+ (options.limit || DEFAULT_QUERY_CHANNELS_LIMIT);
+
+ if (queryType === 'refresh' || queryType === 'reload') {
+ await this.setChannels(channelQueryResponse, {
+ hasNextPage,
+ error: false,
+ });
+ } else {
+ await this.appendChannels(channelQueryResponse, {
+ hasNextPage,
+ error: false,
+ });
+ }
+
+ resolve(true);
+ } catch (e) {
+ await this.handleError(e);
+
+ resolve(false);
+ return;
+ }
+ });
+
+ return this.queryChannelsPromise;
+ };
+
+ handleError = () => {
+ this._queryChannelsDebounced.cancel();
+ this.finishQueryLoadingUIState();
+ return this.setStateAsync({
+ error: true,
+ });
+ };
+
+ appendChannels = (channels = [], additionalState = {}) => {
+ // Remove duplicate channels in worse case we get repeted channel from backend.
+ let distinctChannels = channels.filter(
+ (c) => this.state.channelIds.indexOf(c.id) === -1,
+ );
+
+ distinctChannels = [...this.state.channels, ...distinctChannels];
+ const channelIds = [
+ ...this.state.channelIds,
+ ...distinctChannels.map((c) => c.id),
+ ];
+
+ return this.setStateAsync({
+ channels: distinctChannels,
+ channelIds,
+ offset: distinctChannels.length,
+ ...additionalState,
+ });
+ };
+
+ setChannels = (channels = [], additionalState = {}) => {
+ const distinctChannels = [...channels];
+ const channelIds = [...channels.map((c) => c.id)];
+
+ return this.setStateAsync({
+ channels: distinctChannels,
+ channelIds,
+ offset: distinctChannels.length,
+ ...additionalState,
+ });
+ };
+
+ listenToChanges() {
+ this.props.client.on(this.handleEvent);
+ }
+
+ handleEvent = async (e) => {
+ if (e.type === 'channel.hidden') {
+ if (
+ this.props.onChannelHidden &&
+ typeof this.props.onChannelHidden === 'function'
+ ) {
+ this.props.onChannelHidden(this, e);
+ } else {
+ const channels = this.state.channels;
+ const channelIndex = channels.findIndex(
+ (channel) => channel.cid === e.cid,
+ );
+
+ if (channelIndex < 0) return;
+
+ // Remove the hidden channel from the list.
+ channels.splice(channelIndex, 1);
+ this.setStateAsync({
+ channels: [...channels],
+ });
+ }
+ }
+
+ if (e.type === 'user.presence.changed' || e.type === 'user.updated') {
+ let newChannels = this.state.channels;
+
+ newChannels = newChannels.map((channel) => {
+ if (!channel.state.members[e.user.id]) return channel;
+
+ channel.state.members.setIn([e.user.id, 'user'], e.user);
+
+ return channel;
+ });
+
+ this.setStateAsync({ channels: [...newChannels] });
+ }
+
+ if (e.type === 'message.new') {
+ !this.props.lockChannelOrder && this.moveChannelUp(e.cid);
+ }
+
+ // make sure to re-render the channel list after connection is recovered
+ if (e.type === 'connection.recovered') {
+ this._queryChannelsDebounced.cancel();
+ this.queryChannels('refresh');
+ }
+
+ // move channel to start
+ if (e.type === 'notification.message_new') {
+ if (
+ this.props.onMessageNew &&
+ typeof this.props.onMessageNew === 'function'
+ ) {
+ this.props.onMessageNew(this, e);
+ } else {
+ const channel = await this.getChannel(e.channel.type, e.channel.id);
+
+ // move channel to starting position
+ if (this._unmounted) return;
+ this.setState((prevState) => ({
+ channels: uniqBy([channel, ...prevState.channels], 'cid'),
+ channelIds: uniqWith([channel.id, ...prevState.channelIds], isEqual),
+ offset: prevState.offset + 1,
+ }));
+ }
+ }
+
+ // add to channel
+ if (e.type === 'notification.added_to_channel') {
+ if (
+ this.props.onAddedToChannel &&
+ typeof this.props.onAddedToChannel === 'function'
+ ) {
+ this.props.onAddedToChannel(this, e);
+ } else {
+ const channel = await this.getChannel(e.channel.type, e.channel.id);
+
+ if (this._unmounted) return;
+ this.setState((prevState) => ({
+ channels: uniqBy([channel, ...prevState.channels], 'cid'),
+ channelIds: uniqWith([channel.id, ...prevState.channelIds], isEqual),
+ offset: prevState.offset + 1,
+ }));
+ }
+ }
+
+ // remove from channel
+ if (e.type === 'notification.removed_from_channel') {
+ if (
+ this.props.onRemovedFromChannel &&
+ typeof this.props.onRemovedFromChannel === 'function'
+ ) {
+ this.props.onRemovedFromChannel(this, e);
+ } else {
+ if (this._unmounted) return;
+ this.setState((prevState) => {
+ const channels = prevState.channels.filter(
+ (channel) => channel.cid !== e.channel.cid,
+ );
+ const channelIds = prevState.channelIds.filter(
+ (cid) => cid !== e.channel.cid,
+ );
+ return {
+ channels,
+ channelIds,
+ };
+ });
+ }
+ }
+
+ // Channel data is updated
+ if (e.type === 'channel.updated') {
+ const channels = this.state.channels;
+ const channelIndex = channels.findIndex(
+ (channel) => channel.cid === e.channel.cid,
+ );
+
+ if (channelIndex > -1) {
+ channels[channelIndex].data = Immutable(e.channel);
+ this.setStateAsync({
+ channels: [...channels],
+ });
+ }
+
+ if (
+ this.props.onChannelUpdated &&
+ typeof this.props.onChannelUpdated === 'function'
+ )
+ this.props.onChannelUpdated(this, e);
+ }
+
+ // Channel is deleted
+ if (e.type === 'channel.deleted') {
+ if (
+ this.props.onChannelDeleted &&
+ typeof this.props.onChannelDeleted === 'function'
+ ) {
+ this.props.onChannelDeleted(this, e);
+ } else {
+ const channels = this.state.channels;
+ const channelIndex = channels.findIndex(
+ (channel) => channel.cid === e.channel.cid,
+ );
+
+ if (channelIndex < 0) return;
+
+ // Remove the deleted channel from the list.
+ channels.splice(channelIndex, 1);
+ this.setStateAsync({
+ channels: [...channels],
+ });
+ }
+ }
+
+ if (e.type === 'channel.truncated') {
+ this.setState((prevState) => ({
+ channels: [...prevState.channels],
+ }));
+
+ if (
+ this.props.onChannelTruncated &&
+ typeof this.props.onChannelTruncated === 'function'
+ )
+ this.props.onChannelTruncated(this, e);
+ }
+
+ return null;
+ };
+
+ getChannel = async (type, id) => {
+ const channel = this.props.client.channel(type, id);
+ await channel.watch();
+ return channel;
+ };
+
+ moveChannelUp = (cid) => {
+ if (this._unmounted) return;
+ const channels = this.state.channels;
+
+ // get channel index
+ const channelIndex = this.state.channels.findIndex(
+ (channel) => channel.cid === cid,
+ );
+ if (channelIndex <= 0) return;
+
+ // get channel from channels
+ const channel = channels[channelIndex];
+
+ //remove channel from current position
+ channels.splice(channelIndex, 1);
+ //add channel at the start
+ channels.unshift(channel);
+
+ // set new channel state
+ if (this._unmounted) return;
+ this.setStateAsync({
+ channels: [...channels],
+ });
+ };
+
+ // Refreshes the list. Existing list of channels will still be visible on UI while refreshing.
+ refreshList = async () => {
+ const now = new Date();
+ // Only allow pull-to-refresh 10 seconds after last successful refresh.
+ if (now - this.lastRefresh < 10000 && !this.state.error) {
+ return;
+ }
+
+ // if (!this.state.error) return;
+ this.listRef.scrollToIndex({ index: 0 });
+ this._queryChannelsDebounced.cancel();
+ const success = await this.queryChannels('refresh');
+
+ if (success) this.lastRefresh = new Date();
+ };
+
+ // Reloads the channel list. All the existing channels will be wiped out first from UI, and then
+ // queryChannels api will be called to fetch new channels.
+ reloadList = () => {
+ this._queryChannelsDebounced.cancel();
+ this.queryChannels('reload');
+ };
+
+ loadNextPage = () => {
+ this._queryChannelsDebounced();
+ };
+
+ render() {
+ const context = {
+ loadNextPage: this.loadNextPage,
+ refreshList: this.refreshList,
+ reloadList: this.reloadList,
+ };
+ const List = this.props.List;
+ const props = { ...this.props, setActiveChannel: this.props.onSelect };
+
+ return (
+
+ {
+ this.listRef = ref;
+ this.props.setFlatListRef && this.props.setFlatListRef(ref);
+ }}
+ />
+
+ );
+ }
+}
+
+export default withChatContext(ChannelList);
diff --git a/src/components/ChannelListFooterLoadingIndicator.js b/src/components/ChannelList/ChannelListFooterLoadingIndicator.js
similarity index 68%
rename from src/components/ChannelListFooterLoadingIndicator.js
rename to src/components/ChannelList/ChannelListFooterLoadingIndicator.js
index ed32b06d91..36ba353bd9 100644
--- a/src/components/ChannelListFooterLoadingIndicator.js
+++ b/src/components/ChannelList/ChannelListFooterLoadingIndicator.js
@@ -1,7 +1,7 @@
import React from 'react';
import styled from '@stream-io/styled-components';
-import { Spinner } from './Spinner';
+import { Spinner } from '../Spinner';
const Container = styled.View`
width: 100%;
@@ -10,8 +10,10 @@ const Container = styled.View`
${({ theme }) => theme.channelListFooterLoadingIndicator.container.css}
`;
-export const ChannelListFooterLoadingIndicator = () => (
+const ChannelListFooterLoadingIndicator = () => (
);
+
+export default ChannelListFooterLoadingIndicator;
diff --git a/src/components/ChannelListHeaderErrorIndicator.js b/src/components/ChannelList/ChannelListHeaderErrorIndicator.js
similarity index 89%
rename from src/components/ChannelListHeaderErrorIndicator.js
rename to src/components/ChannelList/ChannelListHeaderErrorIndicator.js
index 47b64e1d31..72c3284e17 100644
--- a/src/components/ChannelListHeaderErrorIndicator.js
+++ b/src/components/ChannelList/ChannelListHeaderErrorIndicator.js
@@ -1,6 +1,6 @@
import React from 'react';
import styled from '@stream-io/styled-components';
-import { withTranslationContext } from '../context';
+import { withTranslationContext } from '../../context';
import PropTypes from 'prop-types';
const Container = styled.TouchableOpacity`
@@ -36,4 +36,4 @@ ChannelListHeaderErrorIndicator.propTypes = {
onPress: PropTypes.func,
};
-export { ChannelListHeaderErrorIndicator };
+export default ChannelListHeaderErrorIndicator;
diff --git a/src/components/ChannelListHeaderNetworkDownIndicator.js b/src/components/ChannelList/ChannelListHeaderNetworkDownIndicator.js
similarity index 76%
rename from src/components/ChannelListHeaderNetworkDownIndicator.js
rename to src/components/ChannelList/ChannelListHeaderNetworkDownIndicator.js
index ca7452707c..962775ed4e 100644
--- a/src/components/ChannelListHeaderNetworkDownIndicator.js
+++ b/src/components/ChannelList/ChannelListHeaderNetworkDownIndicator.js
@@ -1,6 +1,6 @@
import React from 'react';
import styled from '@stream-io/styled-components';
-import { withTranslationContext } from '../context';
+import { withTranslationContext } from '../../context';
const Container = styled.View`
width: 100%;
@@ -19,10 +19,12 @@ const ErrorText = styled.Text`
${({ theme }) => theme.channelListHeaderErrorIndicator.errorText.css}
`;
-export const ChannelListHeaderNetworkDownIndicator = withTranslationContext(
+const ChannelListHeaderNetworkDownIndicator = withTranslationContext(
({ t }) => (
{t('Connection failure, reconnecting now ...')}
),
);
+
+export default ChannelListHeaderNetworkDownIndicator;
diff --git a/src/components/ChannelList/ChannelListMessenger.js b/src/components/ChannelList/ChannelListMessenger.js
new file mode 100644
index 0000000000..cfe6de0026
--- /dev/null
+++ b/src/components/ChannelList/ChannelListMessenger.js
@@ -0,0 +1,224 @@
+import React, { PureComponent } from 'react';
+import { FlatList } from 'react-native';
+import PropTypes from 'prop-types';
+import { ChannelPreview, ChannelPreviewMessenger } from '../ChannelPreview';
+
+import { withChatContext } from '../../context';
+
+import ChannelListHeaderNetworkDownIndicator from './ChannelListHeaderNetworkDownIndicator';
+import ChannelListHeaderErrorIndicator from './ChannelListHeaderErrorIndicator';
+import ChannelListFooterLoadingIndicator from './ChannelListFooterLoadingIndicator';
+
+import {
+ LoadingIndicator,
+ LoadingErrorIndicator,
+ EmptyStateIndicator,
+} from '../Indicators';
+
+/**
+ * ChannelListMessenger - UI component for list of channels, allowing you to select the channel you want to open
+ *
+ * @example ./docs/ChannelListMessenger.md
+ */
+class ChannelListMessenger extends PureComponent {
+ static propTypes = {
+ /** Channels can be either an array of channels or a promise which resolves to an array of channels */
+ channels: PropTypes.oneOfType([
+ PropTypes.array,
+ PropTypes.objectOf({
+ then: PropTypes.func,
+ }),
+ PropTypes.object,
+ ]).isRequired,
+ setActiveChannel: PropTypes.func,
+ /** UI Component to display individual channel item in list.
+ * Defaults to [ChannelPreviewMessenger](https://getstream.github.io/stream-chat-react-native/#channelpreviewmessenger) */
+ Preview: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
+ /** The loading indicator to use. Default: [LoadingIndicator](https://getstream.github.io/stream-chat-react-native/#loadingindicator) */
+ LoadingIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /** The indicator to use when there is error in fetching channels. Default: [LoadingErrorIndicator](https://getstream.github.io/stream-chat-react-native/#loadingerrorindicator) */
+ LoadingErrorIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /** The indicator to use when channel list is empty. Default: [EmptyStateIndicator](https://getstream.github.io/stream-chat-react-native/#emptystateindicator) */
+ EmptyStateIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /**
+ * The indicator to display network-down error at top of list, if there is connectivity issue
+ * Default: [ChannelListHeaderNetworkDownIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListHeaderNetworkDownIndicator)
+ */
+ HeaderNetworkDownIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /**
+ * The indicator to display error at top of list, if there was an error loading some page/channels after the first page.
+ * Default: [ChannelListHeaderErrorIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListHeaderErrorIndicator)
+ */
+ HeaderErrorIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /**
+ * Loading indicator to display at bottom of the list, while loading further pages.
+ * Default: [ChannelListFooterLoadingIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListFooterLoadingIndicator)
+ */
+ FooterLoadingIndicator: PropTypes.oneOfType([
+ PropTypes.node,
+ PropTypes.elementType,
+ ]),
+ /** Remove all the existing channels from UI and load fresh channels. */
+ reloadList: PropTypes.func,
+ /** Loads next page of channels in channels object, which is present here as prop */
+ loadNextPage: PropTypes.func,
+ /**
+ * Refresh the channel list. Its similar to `reloadList`, but it doesn't wipe out existing channels
+ * from UI before loading new set of channels.
+ */
+ refreshList: PropTypes.func,
+ /**
+ * For flatlist
+ * @see See loeadMoreThreshold [doc](https://facebook.github.io/react-native/docs/flatlist#onendreachedthreshold)
+ * */
+ loadMoreThreshold: PropTypes.number,
+ /** If there is error in querying channels */
+ error: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
+ /** If channels are being queries. LoadingIndicator will be displayed if true */
+ loadingChannels: PropTypes.bool,
+ /** If channel list is being refreshed. Loader at top of the list will be displayed if true. */
+ refreshing: PropTypes.bool,
+ /** If further channels are being loadded. Loader will be shown at bottom of the list */
+ loadingNextPage: PropTypes.bool,
+ /**
+ * Besides existing (default) UX behaviour of underlying flatlist of ChannelListMessenger component, if you want
+ * to attach some additional props to un derlying flatlist, you can add it to following prop.
+ *
+ * You can find list of all the available FlatList props here - https://facebook.github.io/react-native/docs/flatlist#props
+ *
+ * **NOTE** Don't use `additionalFlatListProps` to get access to ref of flatlist. Use `setFlatListRef` instead.
+ *
+ * e.g.
+ * ```
+ *
+ * ```
+ */
+ additionalFlatListProps: PropTypes.object,
+ /**
+ * Use `setFlatListRef` to get access to ref to inner FlatList.
+ *
+ * e.g.
+ * ```
+ * {
+ * // Use ref for your own good
+ * }}
+ * ```
+ */
+ setFlatListRef: PropTypes.func,
+ };
+
+ static defaultProps = {
+ Preview: ChannelPreviewMessenger,
+ LoadingIndicator,
+ LoadingErrorIndicator,
+ HeaderNetworkDownIndicator: ChannelListHeaderNetworkDownIndicator,
+ HeaderErrorIndicator: ChannelListHeaderErrorIndicator,
+ FooterLoadingIndicator: ChannelListFooterLoadingIndicator,
+ EmptyStateIndicator,
+ // https://github.com/facebook/react-native/blob/a7a7970e543959e9db5281914d5f132beb01db8d/Libraries/Lists/VirtualizedList.js#L466
+ loadMoreThreshold: 2,
+ additionalFlatListProps: {},
+ };
+
+ renderLoading = () => {
+ const Indicator = this.props.LoadingIndicator;
+ return ;
+ };
+
+ renderLoadingError = () => {
+ const Indicator = this.props.LoadingErrorIndicator;
+ return (
+
+ );
+ };
+
+ renderEmptyState = () => {
+ const Indicator = this.props.EmptyStateIndicator;
+ return ;
+ };
+
+ renderHeaderIndicator = () => {
+ const { isOnline, error, refreshList } = this.props;
+
+ if (!isOnline) {
+ const HeaderNetworkDownIndicator = this.props.HeaderNetworkDownIndicator;
+ return ;
+ }
+
+ if (error) {
+ const HeaderErrorIndicator = this.props.HeaderErrorIndicator;
+ return ;
+ }
+ };
+
+ renderChannels = () => (
+ <>
+ {this.renderHeaderIndicator()}
+ {
+ this.props.setFlatListRef && this.props.setFlatListRef(flRef);
+ }}
+ data={this.props.channels}
+ onEndReached={() => this.props.loadNextPage(false)}
+ onRefresh={() => this.props.refreshList()}
+ refreshing={this.props.refreshing}
+ onEndReachedThreshold={this.props.loadMoreThreshold}
+ ListEmptyComponent={this.renderEmptyState}
+ ListFooterComponent={() => {
+ if (this.props.loadingNextPage) {
+ const FooterLoadingIndicator = this.props.FooterLoadingIndicator;
+
+ return ;
+ }
+
+ return null;
+ }}
+ renderItem={({ item: channel }) => (
+
+ )}
+ keyExtractor={(item) => item.cid}
+ {...this.props.additionalFlatListProps}
+ />
+ >
+ );
+
+ render() {
+ if (this.props.error && this.props.channels.length === 0) {
+ return this.renderLoadingError();
+ } else if (this.props.loadingChannels) {
+ return this.renderLoading();
+ } else {
+ return this.renderChannels();
+ }
+ }
+}
+
+export default withChatContext(ChannelListMessenger);
diff --git a/src/components/ChannelList/index.js b/src/components/ChannelList/index.js
new file mode 100644
index 0000000000..c8cfc69cfd
--- /dev/null
+++ b/src/components/ChannelList/index.js
@@ -0,0 +1,5 @@
+export { default as ChannelList } from './ChannelList';
+export { default as ChannelListFooterLoadingIndicator } from './ChannelListFooterLoadingIndicator';
+export { default as ChannelListHeaderErrorIndicator } from './ChannelListHeaderErrorIndicator';
+export { default as ChannelListHeaderNetworkDownIndicator } from './ChannelListHeaderNetworkDownIndicator';
+export { default as ChannelListMessenger } from './ChannelListMessenger';
diff --git a/src/components/ChannelListMessenger.js b/src/components/ChannelListMessenger.js
deleted file mode 100644
index 2ce5c46882..0000000000
--- a/src/components/ChannelListMessenger.js
+++ /dev/null
@@ -1,224 +0,0 @@
-import React, { PureComponent } from 'react';
-import { FlatList } from 'react-native';
-import PropTypes from 'prop-types';
-import { ChannelPreview } from './ChannelPreview';
-import { ChannelPreviewMessenger } from './ChannelPreviewMessenger';
-import { withChatContext } from '../context';
-import { ChannelListHeaderNetworkDownIndicator } from './ChannelListHeaderNetworkDownIndicator';
-import { ChannelListHeaderErrorIndicator } from './ChannelListHeaderErrorIndicator';
-
-import { LoadingIndicator } from './LoadingIndicator';
-import { LoadingErrorIndicator } from './LoadingErrorIndicator';
-import { EmptyStateIndicator } from './EmptyStateIndicator';
-import { ChannelListFooterLoadingIndicator } from './ChannelListFooterLoadingIndicator';
-
-/**
- * ChannelListMessenger - UI component for list of channels, allowing you to select the channel you want to open
- *
- * @example ./docs/ChannelListMessenger.md
- */
-const ChannelListMessenger = withChatContext(
- class ChannelListMessenger extends PureComponent {
- static propTypes = {
- /** Channels can be either an array of channels or a promise which resolves to an array of channels */
- channels: PropTypes.oneOfType([
- PropTypes.array,
- PropTypes.objectOf({
- then: PropTypes.func,
- }),
- PropTypes.object,
- ]).isRequired,
- setActiveChannel: PropTypes.func,
- /** UI Component to display individual channel item in list.
- * Defaults to [ChannelPreviewMessenger](https://getstream.github.io/stream-chat-react-native/#channelpreviewmessenger) */
- Preview: PropTypes.oneOfType([PropTypes.node, PropTypes.elementType]),
- /** The loading indicator to use. Default: [LoadingIndicator](https://getstream.github.io/stream-chat-react-native/#loadingindicator) */
- LoadingIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /** The indicator to use when there is error in fetching channels. Default: [LoadingErrorIndicator](https://getstream.github.io/stream-chat-react-native/#loadingerrorindicator) */
- LoadingErrorIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /** The indicator to use when channel list is empty. Default: [EmptyStateIndicator](https://getstream.github.io/stream-chat-react-native/#emptystateindicator) */
- EmptyStateIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * The indicator to display network-down error at top of list, if there is connectivity issue
- * Default: [ChannelListHeaderNetworkDownIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListHeaderNetworkDownIndicator)
- */
- HeaderNetworkDownIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * The indicator to display error at top of list, if there was an error loading some page/channels after the first page.
- * Default: [ChannelListHeaderErrorIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListHeaderErrorIndicator)
- */
- HeaderErrorIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /**
- * Loading indicator to display at bottom of the list, while loading further pages.
- * Default: [ChannelListFooterLoadingIndicator](https://getstream.github.io/stream-chat-react-native/#ChannelListFooterLoadingIndicator)
- */
- FooterLoadingIndicator: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
- /** Remove all the existing channels from UI and load fresh channels. */
- reloadList: PropTypes.func,
- /** Loads next page of channels in channels object, which is present here as prop */
- loadNextPage: PropTypes.func,
- /**
- * Refresh the channel list. Its similar to `reloadList`, but it doesn't wipe out existing channels
- * from UI before loading new set of channels.
- */
- refreshList: PropTypes.func,
- /**
- * For flatlist
- * @see See loeadMoreThreshold [doc](https://facebook.github.io/react-native/docs/flatlist#onendreachedthreshold)
- * */
- loadMoreThreshold: PropTypes.number,
- /** If there is error in querying channels */
- error: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
- /** If channels are being queries. LoadingIndicator will be displayed if true */
- loadingChannels: PropTypes.bool,
- /** If channel list is being refreshed. Loader at top of the list will be displayed if true. */
- refreshing: PropTypes.bool,
- /** If further channels are being loadded. Loader will be shown at bottom of the list */
- loadingNextPage: PropTypes.bool,
- /**
- * Besides existing (default) UX behaviour of underlying flatlist of ChannelListMessenger component, if you want
- * to attach some additional props to un derlying flatlist, you can add it to following prop.
- *
- * You can find list of all the available FlatList props here - https://facebook.github.io/react-native/docs/flatlist#props
- *
- * **NOTE** Don't use `additionalFlatListProps` to get access to ref of flatlist. Use `setFlatListRef` instead.
- *
- * e.g.
- * ```
- *
- * ```
- */
- additionalFlatListProps: PropTypes.object,
- /**
- * Use `setFlatListRef` to get access to ref to inner FlatList.
- *
- * e.g.
- * ```
- * {
- * // Use ref for your own good
- * }}
- * ```
- */
- setFlatListRef: PropTypes.func,
- };
-
- static defaultProps = {
- Preview: ChannelPreviewMessenger,
- LoadingIndicator,
- LoadingErrorIndicator,
- HeaderNetworkDownIndicator: ChannelListHeaderNetworkDownIndicator,
- HeaderErrorIndicator: ChannelListHeaderErrorIndicator,
- FooterLoadingIndicator: ChannelListFooterLoadingIndicator,
- EmptyStateIndicator,
- // https://github.com/facebook/react-native/blob/a7a7970e543959e9db5281914d5f132beb01db8d/Libraries/Lists/VirtualizedList.js#L466
- loadMoreThreshold: 2,
- additionalFlatListProps: {},
- };
-
- renderLoading = () => {
- const Indicator = this.props.LoadingIndicator;
- return ;
- };
-
- renderLoadingError = () => {
- const Indicator = this.props.LoadingErrorIndicator;
- return (
-
- );
- };
-
- renderEmptyState = () => {
- const Indicator = this.props.EmptyStateIndicator;
- return ;
- };
-
- renderHeaderIndicator = () => {
- const { isOnline, error, refreshList } = this.props;
-
- if (!isOnline) {
- const HeaderNetworkDownIndicator = this.props
- .HeaderNetworkDownIndicator;
- return ;
- }
-
- if (error) {
- const HeaderErrorIndicator = this.props.HeaderErrorIndicator;
- return ;
- }
- };
-
- renderChannels = () => (
- <>
- {this.renderHeaderIndicator()}
- {
- this.props.setFlatListRef && this.props.setFlatListRef(flRef);
- }}
- data={this.props.channels}
- onEndReached={() => this.props.loadNextPage(false)}
- onRefresh={() => this.props.refreshList()}
- refreshing={this.props.refreshing}
- onEndReachedThreshold={this.props.loadMoreThreshold}
- ListEmptyComponent={this.renderEmptyState}
- ListFooterComponent={() => {
- if (this.props.loadingNextPage) {
- const FooterLoadingIndicator = this.props.FooterLoadingIndicator;
-
- return ;
- }
-
- return null;
- }}
- renderItem={({ item: channel }) => (
-
- )}
- keyExtractor={(item) => item.cid}
- {...this.props.additionalFlatListProps}
- />
- >
- );
-
- render() {
- if (this.props.error && this.props.channels.length === 0) {
- return this.renderLoadingError();
- } else if (this.props.loadingChannels) {
- return this.renderLoading();
- } else {
- return this.renderChannels();
- }
- }
- },
-);
-
-export { ChannelListMessenger };
diff --git a/src/components/ChannelPreview.js b/src/components/ChannelPreview/ChannelPreview.js
similarity index 93%
rename from src/components/ChannelPreview.js
rename to src/components/ChannelPreview/ChannelPreview.js
index 77c99225c7..37a571cd86 100644
--- a/src/components/ChannelPreview.js
+++ b/src/components/ChannelPreview/ChannelPreview.js
@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
-import { withTranslationContext } from '../context';
+import { withTranslationContext } from '../../context';
class ChannelPreview extends PureComponent {
constructor(props) {
@@ -104,6 +104,4 @@ class ChannelPreview extends PureComponent {
}
}
-const ChannelPreviewWithContext = withTranslationContext(ChannelPreview);
-
-export { ChannelPreviewWithContext as ChannelPreview };
+export default withTranslationContext(ChannelPreview);
diff --git a/src/components/ChannelPreviewMessenger.js b/src/components/ChannelPreview/ChannelPreviewMessenger.js
similarity index 93%
rename from src/components/ChannelPreviewMessenger.js
rename to src/components/ChannelPreview/ChannelPreviewMessenger.js
index 696607c247..e771f23a37 100644
--- a/src/components/ChannelPreviewMessenger.js
+++ b/src/components/ChannelPreview/ChannelPreviewMessenger.js
@@ -1,10 +1,11 @@
import React, { PureComponent } from 'react';
-import { Avatar } from './Avatar';
import truncate from 'lodash/truncate';
import styled from '@stream-io/styled-components';
import PropTypes from 'prop-types';
-import { themed } from '../styles/theme';
-import { withTranslationContext } from '../context';
+
+import { Avatar } from '../Avatar';
+import { themed } from '../../styles/theme';
+import { withTranslationContext } from '../../context';
const Container = styled.TouchableOpacity`
display: flex;
@@ -162,7 +163,4 @@ class ChannelPreviewMessenger extends PureComponent {
}
}
-const ChannelPreviewMessengerWithContext = withTranslationContext(
- themed(ChannelPreviewMessenger),
-);
-export { ChannelPreviewMessengerWithContext as ChannelPreviewMessenger };
+export default withTranslationContext(themed(ChannelPreviewMessenger));
diff --git a/src/components/ChannelPreview/index.js b/src/components/ChannelPreview/index.js
new file mode 100644
index 0000000000..41043e5c6a
--- /dev/null
+++ b/src/components/ChannelPreview/index.js
@@ -0,0 +1,2 @@
+export { default as ChannelPreview } from './ChannelPreview';
+export { default as ChannelPreviewMessenger } from './ChannelPreviewMessenger';
diff --git a/src/components/Chat.js b/src/components/Chat.js
deleted file mode 100644
index a879939383..0000000000
--- a/src/components/Chat.js
+++ /dev/null
@@ -1,238 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import { ChatContext, TranslationContext } from '../context';
-import { NetInfo } from '../native';
-
-import { themed } from '../styles/theme';
-import { Streami18n } from '../utils/Streami18n';
-/**
- * Chat - Wrapper component for Chat. The needs to be placed around any other chat components.
- * This Chat component provides the ChatContext to all other components.
- *
- * The ChatContext provides the following props:
- *
- * - client (the client connection)
- * - channels (the list of channels)
- * - setActiveChannel (a function to set the currently active channel)
- * - channel (the currently active channel)
- *
- * It also exposes the withChatContext HOC which you can use to consume the ChatContext
- *
- * @example ./docs/Chat.md
- * @extends PureComponent
- */
-
-export const Chat = themed(
- class Chat extends PureComponent {
- static themePath = '';
- static propTypes = {
- /** The StreamChat client object */
- client: PropTypes.object.isRequired,
- /**
- * Theme object
- *
- * @ref https://getstream.io/chat/react-native-chat/tutorial/#custom-styles
- * */
- style: PropTypes.object,
- logger: PropTypes.func,
- /**
- * Instance of Streami18n class should be provided to Chat component to enable internationalization.
- *
- * Stream provides following list of in-built translations:
- * 1. English (en)
- * 2. Dutch (nl)
- * 3. ...
- * 4. ...
- *
- * Simplest way to start using chat components in one of the in-built languages would be following:
- *
- * ```
- * const i18n = new Streami18n('nl);
- *
- * ...
- *
- * ```
- *
- * If you would like to override certain keys in in-built translation.
- * UI will be automatically updated in this case.
- *
- * ```
- * const i18n = new Streami18n('nl);
- *
- * i18n.registerTranslation('nl', {
- * 'Nothing yet...': 'Nog Niet ...',
- * '{{ firstUser }} and {{ secondUser }} are typing...': '{{ firstUser }} en {{ secondUser }} zijn aan het typen...',
- * });
- *
- *
- * ...
- *
- * ```
- *
- * You can use the same function to add whole new language.
- *
- * ```
- * const i18n = new Streami18n('it');
- *
- * i18n.registerTranslation('it', {
- * 'Nothing yet...': 'Non ancora ...',
- * '{{ firstUser }} and {{ secondUser }} are typing...': '{{ firstUser }} a {{ secondUser }} stanno scrivendo...',
- * });
- *
- * // Make sure to call setLanguage to reflect new language in UI.
- * i18n.setLanguage('it');
- *
- * ...
- *
- * ```
- */
- i18nInstance: PropTypes.instanceOf(Streami18n),
- };
-
- static defaultProps = {
- logger: () => {},
- };
-
- constructor(props) {
- super(props);
-
- this.state = {
- // currently active channel
- channel: {},
- isOnline: true,
- connectionRecovering: false,
- t: null,
- };
-
- this.unsubscribeNetInfo = null;
- this.setConnectionListener();
-
- this.props.client.on('connection.changed', (event) => {
- if (this._unmounted) return;
- this.setState({
- isOnline: event.online,
- connectionRecovering: !event.online,
- });
- });
-
- this.props.client.on('connection.recovered', () => {
- if (this._unmounted) return;
- this.setState({ connectionRecovering: false });
- });
-
- this._unmounted = false;
- }
-
- async componentDidMount() {
- this.props.logger('Chat component', 'componentDidMount', {
- tags: ['lifecycle', 'chat'],
- props: this.props,
- state: this.state,
- });
-
- const { i18nInstance } = this.props;
-
- let streami18n;
- if (i18nInstance && i18nInstance instanceof Streami18n) {
- streami18n = i18nInstance;
- } else {
- streami18n = new Streami18n({ language: 'en' });
- }
-
- streami18n.registerSetLanguageCallback((t) => {
- this.setState({ t });
- });
-
- const { t, tDateTimeParser } = await streami18n.getTranslators();
- this.setState({ t, tDateTimeParser });
- }
-
- componentDidUpdate() {
- this.props.logger('Chat component', 'componentDidUpdate', {
- tags: ['lifecycle', 'chat'],
- props: this.props,
- state: this.state,
- });
- }
-
- componentWillUnmount() {
- this.props.logger('Chat component', 'componentWillUnmount', {
- tags: ['lifecycle', 'chat'],
- props: this.props,
- state: this.state,
- });
-
- this._unmounted = true;
- this.props.client.off('connection.recovered');
- this.props.client.off('connection.changed');
- this.unsubscribeNetInfo && this.unsubscribeNetInfo();
- }
-
- notifyChatClient = (isConnected) => {
- if (this.props.client != null && this.props.client.wsConnection != null) {
- if (isConnected) {
- this.props.client.wsConnection.onlineStatusChanged({
- type: 'online',
- });
- } else {
- this.props.client.wsConnection.onlineStatusChanged({
- type: 'offline',
- });
- }
- }
- };
-
- setConnectionListener = () => {
- NetInfo.fetch().then((isConnected) => {
- this.notifyChatClient(isConnected);
- });
- this.unsubscribeNetInfo = NetInfo.addEventListener((isConnected) => {
- this.notifyChatClient(isConnected);
- });
- };
-
- setActiveChannel = (channel) => {
- this.props.logger('Chat component', 'setActiveChannel', {
- tags: ['chat'],
- props: this.props,
- state: this.state,
- });
-
- if (this._unmounted) return;
- this.setState(() => ({
- channel,
- }));
- };
-
- getContext = () => ({
- client: this.props.client,
- channel: this.state.channel,
- setActiveChannel: this.setActiveChannel,
- isOnline: this.state.isOnline,
- connectionRecovering: this.state.connectionRecovering,
- logger: this.props.logger,
- });
-
- render() {
- this.props.logger('Chat component', 'Rerendering', {
- props: this.props,
- state: this.state,
- });
-
- if (!this.state.t) return null;
-
- return (
-
-
- {this.props.children}
-
-
- );
- }
- },
-);
diff --git a/src/components/Chat/Chat.js b/src/components/Chat/Chat.js
new file mode 100644
index 0000000000..eea2c1885b
--- /dev/null
+++ b/src/components/Chat/Chat.js
@@ -0,0 +1,238 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { ChatContext, TranslationContext } from '../../context';
+import { NetInfo } from '../../native';
+
+import { themed } from '../../styles/theme';
+import { Streami18n } from '../../utils/Streami18n';
+/**
+ * Chat - Wrapper component for Chat. The needs to be placed around any other chat components.
+ * This Chat component provides the ChatContext to all other components.
+ *
+ * The ChatContext provides the following props:
+ *
+ * - client (the client connection)
+ * - channels (the list of channels)
+ * - setActiveChannel (a function to set the currently active channel)
+ * - channel (the currently active channel)
+ *
+ * It also exposes the withChatContext HOC which you can use to consume the ChatContext
+ *
+ * @example ./docs/Chat.md
+ * @extends PureComponent
+ */
+
+class Chat extends PureComponent {
+ static themePath = '';
+ static propTypes = {
+ /** The StreamChat client object */
+ client: PropTypes.object.isRequired,
+ /**
+ * Theme object
+ *
+ * @ref https://getstream.io/chat/react-native-chat/tutorial/#custom-styles
+ * */
+ style: PropTypes.object,
+ logger: PropTypes.func,
+ /**
+ * Instance of Streami18n class should be provided to Chat component to enable internationalization.
+ *
+ * Stream provides following list of in-built translations:
+ * 1. English (en)
+ * 2. Dutch (nl)
+ * 3. ...
+ * 4. ...
+ *
+ * Simplest way to start using chat components in one of the in-built languages would be following:
+ *
+ * ```
+ * const i18n = new Streami18n('nl);
+ *
+ * ...
+ *
+ * ```
+ *
+ * If you would like to override certain keys in in-built translation.
+ * UI will be automatically updated in this case.
+ *
+ * ```
+ * const i18n = new Streami18n('nl);
+ *
+ * i18n.registerTranslation('nl', {
+ * 'Nothing yet...': 'Nog Niet ...',
+ * '{{ firstUser }} and {{ secondUser }} are typing...': '{{ firstUser }} en {{ secondUser }} zijn aan het typen...',
+ * });
+ *
+ *
+ * ...
+ *
+ * ```
+ *
+ * You can use the same function to add whole new language.
+ *
+ * ```
+ * const i18n = new Streami18n('it');
+ *
+ * i18n.registerTranslation('it', {
+ * 'Nothing yet...': 'Non ancora ...',
+ * '{{ firstUser }} and {{ secondUser }} are typing...': '{{ firstUser }} a {{ secondUser }} stanno scrivendo...',
+ * });
+ *
+ * // Make sure to call setLanguage to reflect new language in UI.
+ * i18n.setLanguage('it');
+ *
+ * ...
+ *
+ * ```
+ */
+ i18nInstance: PropTypes.instanceOf(Streami18n),
+ };
+
+ static defaultProps = {
+ logger: () => {},
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ // currently active channel
+ channel: {},
+ isOnline: true,
+ connectionRecovering: false,
+ t: null,
+ };
+
+ this.unsubscribeNetInfo = null;
+ this.setConnectionListener();
+
+ this.props.client.on('connection.changed', (event) => {
+ if (this._unmounted) return;
+ this.setState({
+ isOnline: event.online,
+ connectionRecovering: !event.online,
+ });
+ });
+
+ this.props.client.on('connection.recovered', () => {
+ if (this._unmounted) return;
+ this.setState({ connectionRecovering: false });
+ });
+
+ this._unmounted = false;
+ }
+
+ async componentDidMount() {
+ this.props.logger('Chat component', 'componentDidMount', {
+ tags: ['lifecycle', 'chat'],
+ props: this.props,
+ state: this.state,
+ });
+
+ const { i18nInstance } = this.props;
+
+ let streami18n;
+ if (i18nInstance && i18nInstance instanceof Streami18n) {
+ streami18n = i18nInstance;
+ } else {
+ streami18n = new Streami18n({ language: 'en' });
+ }
+
+ streami18n.registerSetLanguageCallback((t) => {
+ this.setState({ t });
+ });
+
+ const { t, tDateTimeParser } = await streami18n.getTranslators();
+ this.setState({ t, tDateTimeParser });
+ }
+
+ componentDidUpdate() {
+ this.props.logger('Chat component', 'componentDidUpdate', {
+ tags: ['lifecycle', 'chat'],
+ props: this.props,
+ state: this.state,
+ });
+ }
+
+ componentWillUnmount() {
+ this.props.logger('Chat component', 'componentWillUnmount', {
+ tags: ['lifecycle', 'chat'],
+ props: this.props,
+ state: this.state,
+ });
+
+ this._unmounted = true;
+ this.props.client.off('connection.recovered');
+ this.props.client.off('connection.changed');
+ this.unsubscribeNetInfo && this.unsubscribeNetInfo();
+ }
+
+ notifyChatClient = (isConnected) => {
+ if (this.props.client != null && this.props.client.wsConnection != null) {
+ if (isConnected) {
+ this.props.client.wsConnection.onlineStatusChanged({
+ type: 'online',
+ });
+ } else {
+ this.props.client.wsConnection.onlineStatusChanged({
+ type: 'offline',
+ });
+ }
+ }
+ };
+
+ setConnectionListener = () => {
+ NetInfo.fetch().then((isConnected) => {
+ this.notifyChatClient(isConnected);
+ });
+ this.unsubscribeNetInfo = NetInfo.addEventListener((isConnected) => {
+ this.notifyChatClient(isConnected);
+ });
+ };
+
+ setActiveChannel = (channel) => {
+ this.props.logger('Chat component', 'setActiveChannel', {
+ tags: ['chat'],
+ props: this.props,
+ state: this.state,
+ });
+
+ if (this._unmounted) return;
+ this.setState(() => ({
+ channel,
+ }));
+ };
+
+ getContext = () => ({
+ client: this.props.client,
+ channel: this.state.channel,
+ setActiveChannel: this.setActiveChannel,
+ isOnline: this.state.isOnline,
+ connectionRecovering: this.state.connectionRecovering,
+ logger: this.props.logger,
+ });
+
+ render() {
+ this.props.logger('Chat component', 'Rerendering', {
+ props: this.props,
+ state: this.state,
+ });
+
+ if (!this.state.t) return null;
+
+ return (
+
+
+ {this.props.children}
+
+
+ );
+ }
+}
+
+export default themed(Chat);
diff --git a/src/components/Chat/index.js b/src/components/Chat/index.js
new file mode 100644
index 0000000000..b9eec2fda9
--- /dev/null
+++ b/src/components/Chat/index.js
@@ -0,0 +1 @@
+export { default as Chat } from './Chat';
diff --git a/src/components/CloseButton.js b/src/components/CloseButton/CloseButton.js
similarity index 78%
rename from src/components/CloseButton.js
rename to src/components/CloseButton/CloseButton.js
index 6fdea851b0..a6f2687e6c 100644
--- a/src/components/CloseButton.js
+++ b/src/components/CloseButton/CloseButton.js
@@ -1,8 +1,10 @@
import React from 'react';
-import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
import { Image } from 'react-native';
-import closeRound from '../images/icons/close-round.png';
+
+import styled from '@stream-io/styled-components';
+
+import { themed } from '../../styles/theme';
+import closeRound from '../../images/icons/close-round.png';
const Container = styled.View`
width: 30;
@@ -15,7 +17,7 @@ const Container = styled.View`
${({ theme }) => theme.closeButton.container.css}
`;
-export const CloseButton = themed(
+const CloseButton = themed(
class CloseButton extends React.PureComponent {
static themePath = 'closeButton';
render() {
@@ -29,3 +31,5 @@ export const CloseButton = themed(
);
CloseButton.propTypes = {};
+
+export default CloseButton;
diff --git a/src/components/CloseButton/index.js b/src/components/CloseButton/index.js
new file mode 100644
index 0000000000..ac9b445dcd
--- /dev/null
+++ b/src/components/CloseButton/index.js
@@ -0,0 +1 @@
+export { default as CloseButton } from './CloseButton';
diff --git a/src/components/ImageUploadPreview.js b/src/components/ImageUploadPreview.js
deleted file mode 100644
index 28cc980a69..0000000000
--- a/src/components/ImageUploadPreview.js
+++ /dev/null
@@ -1,149 +0,0 @@
-import React from 'react';
-import { FlatList } from 'react-native';
-import { UploadProgressIndicator } from './UploadProgressIndicator';
-import PropTypes from 'prop-types';
-import { FileState, ProgressIndicatorTypes } from '../utils';
-import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
-
-import closeRound from '../images/icons/close-round.png';
-
-const Container = styled.View`
- height: 70;
- display: flex;
- padding: 10px;
- ${({ theme }) => theme.messageInput.imageUploadPreview.container.css};
-`;
-
-const ItemContainer = styled.View`
- display: flex;
- height: 50;
- flex-direction: row;
- align-items: flex-start;
- margin-left: 5;
- ${({ theme }) => theme.messageInput.imageUploadPreview.itemContainer.css};
-`;
-
-const Dismiss = styled.TouchableOpacity`
- position: absolute;
- top: 5;
- right: 5;
- background-color: #fff;
- width: 20;
- height: 20;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 20;
- ${({ theme }) => theme.messageInput.imageUploadPreview.dismiss.css};
-`;
-
-const Upload = styled.Image`
- width: 50;
- height: 50;
- border-radius: 10;
- ${({ theme }) => theme.messageInput.imageUploadPreview.upload.css};
-`;
-
-const DismissImage = styled.Image`
- width: 10;
- height: 10;
- ${({ theme }) => theme.messageInput.imageUploadPreview.dismissImage.css};
-`;
-
-/**
- * UI Component to preview the images set for upload
- *
- * @example ./docs/ImageUploadPreview.md
- * @extends PureComponent
- */
-export const ImageUploadPreview = themed(
- class ImageUploadPreview extends React.PureComponent {
- static themePath = 'messageInput.imageUploadPreview';
- constructor(props) {
- super(props);
- }
- static propTypes = {
- /**
- * Its an object/map of id vs image objects which are set for upload. It has following structure:
- *
- * ```json
- * {
- * "randomly_generated_temp_id_1": {
- * "id": "randomly_generated_temp_id_1",
- * "file": // File object
- * "status": "Uploading" // or "Finished"
- * },
- * "randomly_generated_temp_id_2": {
- * "id": "randomly_generated_temp_id_2",
- * "file": // File object
- * "status": "Uploading" // or "Finished"
- * },
- * }
- * ```
- *
- * */
- imageUploads: PropTypes.array.isRequired,
- /**
- * @param id Index of image in `imageUploads` array in state of MessageInput.
- */
- removeImage: PropTypes.func,
- /**
- * @param id Index of image in `imageUploads` array in state of MessageInput.
- */
- retryUpload: PropTypes.func,
- };
-
- _renderItem = ({ item }) => {
- let type;
-
- const { retryUpload } = this.props;
-
- if (item.state === FileState.UPLOADING)
- type = ProgressIndicatorTypes.IN_PROGRESS;
-
- if (item.state === FileState.UPLOAD_FAILED)
- type = ProgressIndicatorTypes.RETRY;
- return (
-
-
-
-
-
- {
- this.props.removeImage(item.id);
- }}
- >
-
-
-
-
- );
- };
-
- render() {
- if (!this.props.imageUploads || this.props.imageUploads.length === 0)
- return null;
-
- return (
-
- item.id}
- renderItem={this._renderItem}
- />
-
- );
- }
- },
-);
diff --git a/src/components/EmptyStateIndicator.js b/src/components/Indicators/EmptyStateIndicator.js
similarity index 80%
rename from src/components/EmptyStateIndicator.js
rename to src/components/Indicators/EmptyStateIndicator.js
index 6a5408c5d6..89ca8cb304 100644
--- a/src/components/EmptyStateIndicator.js
+++ b/src/components/Indicators/EmptyStateIndicator.js
@@ -1,7 +1,7 @@
import React from 'react';
import { Text } from 'react-native';
-export const EmptyStateIndicator = ({ listType }) => {
+const EmptyStateIndicator = ({ listType }) => {
let Indicator;
switch (listType) {
case 'channel':
@@ -17,3 +17,5 @@ export const EmptyStateIndicator = ({ listType }) => {
return Indicator;
};
+
+export default EmptyStateIndicator;
diff --git a/src/components/LoadingErrorIndicator.js b/src/components/Indicators/LoadingErrorIndicator.js
similarity index 87%
rename from src/components/LoadingErrorIndicator.js
rename to src/components/Indicators/LoadingErrorIndicator.js
index 674ecbcd6b..2d82661bb3 100644
--- a/src/components/LoadingErrorIndicator.js
+++ b/src/components/Indicators/LoadingErrorIndicator.js
@@ -1,7 +1,7 @@
import React from 'react';
import styled from '@stream-io/styled-components';
import PropTypes from 'prop-types';
-import { withTranslationContext } from '../context';
+import { withTranslationContext } from '../../context';
const Container = styled.TouchableOpacity`
height: 100%;
@@ -65,7 +65,4 @@ LoadingErrorIndicator.propTypes = {
retry: PropTypes.func,
};
-const LoadingErrorIndicatorWithContext = withTranslationContext(
- LoadingErrorIndicator,
-);
-export { LoadingErrorIndicatorWithContext as LoadingErrorIndicator };
+export default withTranslationContext(LoadingErrorIndicator);
diff --git a/src/components/LoadingIndicator.js b/src/components/Indicators/LoadingIndicator.js
similarity index 86%
rename from src/components/LoadingIndicator.js
rename to src/components/Indicators/LoadingIndicator.js
index e72ab6b91c..70e6b3e6c7 100644
--- a/src/components/LoadingIndicator.js
+++ b/src/components/Indicators/LoadingIndicator.js
@@ -1,8 +1,8 @@
import React from 'react';
import styled from '@stream-io/styled-components';
import PropTypes from 'prop-types';
-import { Spinner } from './Spinner';
-import { withTranslationContext } from '../context';
+import { Spinner } from '../Spinner';
+import { withTranslationContext } from '../../context';
const Container = styled.View`
height: 100%;
@@ -62,5 +62,4 @@ class LoadingIndicator extends React.PureComponent {
}
}
-const LoadingIndicatorWithContext = withTranslationContext(LoadingIndicator);
-export { LoadingIndicatorWithContext as LoadingIndicator };
+export default withTranslationContext(LoadingIndicator);
diff --git a/src/components/Indicators/index.js b/src/components/Indicators/index.js
new file mode 100644
index 0000000000..39ce36ac33
--- /dev/null
+++ b/src/components/Indicators/index.js
@@ -0,0 +1,3 @@
+export { default as EmptyStateIndicator } from './EmptyStateIndicator';
+export { default as LoadingErrorIndicator } from './LoadingErrorIndicator';
+export { default as LoadingIndicator } from './LoadingIndicator';
diff --git a/src/components/KeyboardCompatibleView.js b/src/components/KeyboardCompatibleView/KeyboardCompatibleView.js
similarity index 98%
rename from src/components/KeyboardCompatibleView.js
rename to src/components/KeyboardCompatibleView/KeyboardCompatibleView.js
index 00f14a48c8..dc93571363 100644
--- a/src/components/KeyboardCompatibleView.js
+++ b/src/components/KeyboardCompatibleView/KeyboardCompatibleView.js
@@ -8,7 +8,8 @@ import {
StatusBar,
AppState,
} from 'react-native';
-import { KeyboardContext } from '../context';
+
+import { KeyboardContext } from '../../context';
import PropTypes from 'prop-types';
/**
@@ -26,7 +27,7 @@ import PropTypes from 'prop-types';
*
* ```
*/
-export class KeyboardCompatibleView extends React.PureComponent {
+class KeyboardCompatibleView extends React.PureComponent {
static propTypes = {
keyboardDismissAnimationDuration: PropTypes.number,
keyboardOpenAnimationDuration: PropTypes.number,
@@ -296,3 +297,5 @@ export class KeyboardCompatibleView extends React.PureComponent {
);
}
}
+
+export default KeyboardCompatibleView;
diff --git a/src/components/KeyboardCompatibleView/index.js b/src/components/KeyboardCompatibleView/index.js
new file mode 100644
index 0000000000..69339ae075
--- /dev/null
+++ b/src/components/KeyboardCompatibleView/index.js
@@ -0,0 +1 @@
+export { default as KeyboardCompatibleView } from './KeyboardCompatibleView';
diff --git a/src/components/Message.js b/src/components/Message/Message.js
similarity index 98%
rename from src/components/Message.js
rename to src/components/Message/Message.js
index f499913afe..de25f20a27 100644
--- a/src/components/Message.js
+++ b/src/components/Message/Message.js
@@ -1,11 +1,13 @@
import React from 'react';
import { TouchableOpacity } from 'react-native';
-import { Attachment } from './Attachment';
-import { MessageSimple } from './MessageSimple';
import PropTypes from 'prop-types';
import deepequal from 'deep-equal';
-import { withKeyboardContext } from '../context';
-import { MESSAGE_ACTIONS } from '../utils';
+
+import { Attachment } from '../Attachment';
+import { MessageSimple } from './MessageSimple';
+
+import { withKeyboardContext } from '../../context';
+import { MESSAGE_ACTIONS } from '../../utils';
/**
* Message - A high level component which implements all the logic required for a message.
@@ -356,4 +358,4 @@ const Message = withKeyboardContext(
},
);
-export { Message };
+export default Message;
diff --git a/src/components/MessageSimple/MessageAvatar.js b/src/components/Message/MessageSimple/MessageAvatar.js
similarity index 94%
rename from src/components/MessageSimple/MessageAvatar.js
rename to src/components/Message/MessageSimple/MessageAvatar.js
index 614f86c137..600751bd18 100644
--- a/src/components/MessageSimple/MessageAvatar.js
+++ b/src/components/Message/MessageSimple/MessageAvatar.js
@@ -1,6 +1,6 @@
import React from 'react';
import styled from '@stream-io/styled-components';
-import { Avatar } from '../Avatar';
+import { Avatar } from '../../Avatar';
import PropTypes from 'prop-types';
const Container = styled.View`
@@ -15,7 +15,7 @@ const Spacer = styled.View`
${({ theme }) => theme.message.avatarWrapper.spacer.css}
`;
-export const MessageAvatar = ({
+const MessageAvatar = ({
message,
alignment,
groupStyles,
@@ -59,3 +59,5 @@ MessageAvatar.propTypes = {
*/
groupStyles: PropTypes.array,
};
+
+export default MessageAvatar;
diff --git a/src/components/MessageSimple/MessageContent.js b/src/components/Message/MessageSimple/MessageContent.js
similarity index 97%
rename from src/components/MessageSimple/MessageContent.js
rename to src/components/Message/MessageSimple/MessageContent.js
index 3e42521d5b..808147d8d3 100644
--- a/src/components/MessageSimple/MessageContent.js
+++ b/src/components/Message/MessageSimple/MessageContent.js
@@ -1,20 +1,27 @@
import React from 'react';
-import { MessageContentContext, withTranslationContext } from '../../context';
import styled from '@stream-io/styled-components';
-import { themed } from '../../styles/theme';
-import { Attachment } from '../Attachment';
-import { ActionSheetCustom as ActionSheet } from 'react-native-actionsheet';
-import { ReactionList } from '../ReactionList';
-import { MessageTextContainer } from './MessageTextContainer';
-import { MessageReplies } from './MessageReplies';
-import { MESSAGE_ACTIONS } from '../../utils';
import Immutable from 'seamless-immutable';
import PropTypes from 'prop-types';
-import { Gallery } from '../Gallery';
-import { FileAttachment } from '../FileAttachment';
-import { FileAttachmentGroup } from '../FileAttachmentGroup';
-import { ReactionPickerWrapper } from '../ReactionPickerWrapper';
-import { emojiData } from '../../utils';
+import { ActionSheetCustom as ActionSheet } from 'react-native-actionsheet';
+
+import {
+ MessageContentContext,
+ withTranslationContext,
+} from '../../../context';
+import { themed } from '../../../styles/theme';
+
+import {
+ Attachment,
+ Gallery,
+ FileAttachment,
+ FileAttachmentGroup,
+} from '../../Attachment';
+import { ReactionList, ReactionPickerWrapper } from '../../Reaction';
+
+import MessageTextContainer from './MessageTextContainer';
+import MessageReplies from './MessageReplies';
+
+import { emojiData, MESSAGE_ACTIONS } from '../../../utils';
// Border radii are useful for the case of error message types only.
// Otherwise background is transparent, so border radius is not really visible.
@@ -767,8 +774,4 @@ class MessageContent extends React.PureComponent {
}
}
-const MessageContentWithContext = withTranslationContext(
- themed(MessageContent),
-);
-
-export { MessageContentWithContext as MessageContent };
+export default withTranslationContext(themed(MessageContent));
diff --git a/src/components/MessageSimple/MessageReplies.js b/src/components/Message/MessageSimple/MessageReplies.js
similarity index 87%
rename from src/components/MessageSimple/MessageReplies.js
rename to src/components/Message/MessageSimple/MessageReplies.js
index 18aa5b96a7..0a2425bdbf 100644
--- a/src/components/MessageSimple/MessageReplies.js
+++ b/src/components/Message/MessageSimple/MessageReplies.js
@@ -2,8 +2,8 @@ import React from 'react';
import styled from '@stream-io/styled-components';
import PropTypes from 'prop-types';
-import iconPath from '../../images/icons/icon_path.png';
-import { withTranslationContext } from '../../context';
+import iconPath from '../../../images/icons/icon_path.png';
+import { withTranslationContext } from '../../../context';
const Container = styled.TouchableOpacity`
padding: 5px;
@@ -62,6 +62,4 @@ MessageReplies.propTypes = {
alignment: PropTypes.oneOf(['right', 'left']),
};
-const MessageRepliesWithContext = withTranslationContext(MessageReplies);
-
-export { MessageRepliesWithContext as MessageReplies };
+export default withTranslationContext(MessageReplies);
diff --git a/src/components/MessageSimple/MessageStatus.js b/src/components/Message/MessageSimple/MessageStatus.js
similarity index 93%
rename from src/components/MessageSimple/MessageStatus.js
rename to src/components/Message/MessageSimple/MessageStatus.js
index 73025c6c49..d6760d9d11 100644
--- a/src/components/MessageSimple/MessageStatus.js
+++ b/src/components/Message/MessageSimple/MessageStatus.js
@@ -1,10 +1,13 @@
import React from 'react';
-import styled from '@stream-io/styled-components';
-import loadingGif from '../../images/loading.gif';
-import iconDeliveredUnseen from '../../images/icons/delivered_unseen.png';
-import { Avatar } from '../Avatar';
import PropTypes from 'prop-types';
+import styled from '@stream-io/styled-components';
+
+import loadingGif from '../../../images/loading.gif';
+import iconDeliveredUnseen from '../../../images/icons/delivered_unseen.png';
+
+import { Avatar } from '../../Avatar';
+
const Spacer = styled.View`
height: 10;
`;
@@ -58,7 +61,7 @@ const ReadByContainer = styled.View`
${({ theme }) => theme.message.status.readByContainer.css};
`;
-export const MessageStatus = ({
+const MessageStatus = ({
client,
readBy,
message,
@@ -126,3 +129,5 @@ MessageStatus.propTypes = {
/** Boolean if current message is part of thread */
isThreadList: PropTypes.bool,
};
+
+export default MessageStatus;
diff --git a/src/components/MessageSimple/MessageTextContainer.js b/src/components/Message/MessageSimple/MessageTextContainer.js
similarity index 96%
rename from src/components/MessageSimple/MessageTextContainer.js
rename to src/components/Message/MessageSimple/MessageTextContainer.js
index f1f77cf124..bdbc8e5f06 100644
--- a/src/components/MessageSimple/MessageTextContainer.js
+++ b/src/components/Message/MessageSimple/MessageTextContainer.js
@@ -1,8 +1,9 @@
import React from 'react';
+import PropTypes from 'prop-types';
+
import styled, { withTheme } from '@stream-io/styled-components';
-import { renderText, capitalize } from '../../utils';
-import PropTypes from 'prop-types';
+import { renderText, capitalize } from '../../../utils';
const TextContainer = styled.View`
border-bottom-left-radius: ${({ theme, groupStyle }) =>
@@ -42,7 +43,7 @@ const TextContainer = styled.View`
${({ theme }) => theme.message.content.textContainer.css}
`;
-export const MessageTextContainer = withTheme((props) => {
+const MessageTextContainer = withTheme((props) => {
const {
message,
groupStyles = ['bottom'],
@@ -100,3 +101,5 @@ MessageTextContainer.propTypes = {
/** Object specifying rules defined within simple-markdown https://github.com/Khan/simple-markdown#adding-a-simple-extension */
markdownRules: PropTypes.object,
};
+
+export default MessageTextContainer;
diff --git a/src/components/MessageSimple/index.js b/src/components/Message/MessageSimple/index.js
similarity index 92%
rename from src/components/MessageSimple/index.js
rename to src/components/Message/MessageSimple/index.js
index 2804dc9f73..6990d5af6d 100644
--- a/src/components/MessageSimple/index.js
+++ b/src/components/Message/MessageSimple/index.js
@@ -1,12 +1,12 @@
import React from 'react';
import styled from '@stream-io/styled-components';
-import { themed } from '../../styles/theme';
-import { MessageAvatar as DefaultMessageAvatar } from './MessageAvatar';
-import { MessageContent as DefaultMessageContent } from './MessageContent';
-import { MessageStatus as DefaultMessageStatus } from './MessageStatus';
-import { MessageSystem as DefaultMessageSystem } from '../MessageSystem';
-import { emojiData } from '../../utils';
+import { default as DefaultMessageAvatar } from './MessageAvatar';
+import { default as DefaultMessageContent } from './MessageContent';
+import { default as DefaultMessageStatus } from './MessageStatus';
+
+import { emojiData } from '../../../utils';
+import { themed } from '../../../styles/theme';
import PropTypes from 'prop-types';
@@ -57,14 +57,6 @@ export const MessageSimple = themed(
PropTypes.node,
PropTypes.elementType,
]),
- /**
- * Custom UI component for Messages of type "system"
- * Defaults to: https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/MessageSystem.js
- * */
- MessageSystem: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.elementType,
- ]),
/**
* Custom UI component to display reaction list.
* Defaults to: https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/ReactionList.js
@@ -328,7 +320,6 @@ export const MessageSimple = themed(
MessageAvatar: DefaultMessageAvatar,
MessageContent: DefaultMessageContent,
MessageStatus: DefaultMessageStatus,
- MessageSystem: DefaultMessageSystem,
supportedReactions: emojiData,
};
@@ -372,7 +363,6 @@ export const MessageSimple = themed(
MessageAvatar,
MessageContent,
MessageStatus,
- MessageSystem,
} = this.props;
let alignment;
@@ -392,17 +382,6 @@ export const MessageSimple = themed(
? true
: false;
- if (message.type === 'system') {
- return (
-
-
-
- );
- }
-
const hasReactions =
reactionsEnabled &&
message.latest_reactions &&
@@ -441,7 +420,7 @@ export const MessageSimple = themed(
},
);
-export { MessageStatus } from './MessageStatus';
-export { MessageContent } from './MessageContent';
-export { MessageAvatar } from './MessageAvatar';
-export { MessageTextContainer } from './MessageTextContainer';
+export * from './MessageStatus';
+export * from './MessageContent';
+export * from './MessageAvatar';
+export * from './MessageTextContainer';
diff --git a/src/components/Message/index.js b/src/components/Message/index.js
new file mode 100644
index 0000000000..9c761605c8
--- /dev/null
+++ b/src/components/Message/index.js
@@ -0,0 +1,2 @@
+export { default as Message } from './Message';
+export * from './MessageSimple';
diff --git a/src/components/MessageInput/AttachButton.js b/src/components/MessageInput/AttachButton.js
new file mode 100644
index 0000000000..b8d5aa1620
--- /dev/null
+++ b/src/components/MessageInput/AttachButton.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import styled from '@stream-io/styled-components';
+
+import iconAddAttachment from '../../images/icons/plus-outline.png';
+import { themed } from '../../styles/theme';
+
+const Container = styled.TouchableOpacity`
+ margin-right: 8;
+ ${({ theme }) => theme.messageInput.attachButton.css}
+`;
+
+const AttachButtonIcon = styled.Image`
+ width: 15;
+ height: 15;
+ ${({ theme }) => theme.messageInput.attachButtonIcon.css}
+`;
+
+/**
+ * UI Component for attach button in MessageInput component.
+ *
+ * @extends PureComponent
+ * @example ./docs/AttachButton.md
+ */
+class AttachButton extends React.PureComponent {
+ static themePath = 'messageInput';
+ static propTypes = {
+ handleOnPress: PropTypes.func,
+ disabled: PropTypes.bool,
+ };
+ static defaultProps = {
+ disabled: false,
+ };
+
+ render() {
+ const { handleOnPress, disabled } = this.props;
+ return (
+
+
+
+ );
+ }
+}
+
+export default themed(AttachButton);
diff --git a/src/components/FileUploadPreview.js b/src/components/MessageInput/FileUploadPreview.js
similarity index 91%
rename from src/components/FileUploadPreview.js
rename to src/components/MessageInput/FileUploadPreview.js
index 2ee92fa9ab..478930dea0 100644
--- a/src/components/FileUploadPreview.js
+++ b/src/components/MessageInput/FileUploadPreview.js
@@ -1,9 +1,10 @@
import React from 'react';
import { View, Text, FlatList } from 'react-native';
import PropTypes from 'prop-types';
-import { FileIcon } from './FileIcon';
-import { UploadProgressIndicator } from './UploadProgressIndicator';
-import { FileState, ProgressIndicatorTypes } from '../utils';
+
+import { FileIcon } from '../Attachment';
+import UploadProgressIndicator from './UploadProgressIndicator';
+import { FileState, ProgressIndicatorTypes } from '../../utils';
/**
* FileUploadPreview
*
@@ -19,7 +20,7 @@ const FILE_PREVIEW_PADDING = 10;
* @example ./docs/FileUploadPreview.md
* @extends PureComponent
*/
-export class FileUploadPreview extends React.PureComponent {
+class FileUploadPreview extends React.PureComponent {
constructor(props) {
super(props);
}
@@ -107,3 +108,5 @@ export class FileUploadPreview extends React.PureComponent {
);
}
}
+
+export default FileUploadPreview;
diff --git a/src/components/MessageInput/ImageUploadPreview.js b/src/components/MessageInput/ImageUploadPreview.js
new file mode 100644
index 0000000000..5340342206
--- /dev/null
+++ b/src/components/MessageInput/ImageUploadPreview.js
@@ -0,0 +1,150 @@
+import React from 'react';
+import { FlatList } from 'react-native';
+import PropTypes from 'prop-types';
+import styled from '@stream-io/styled-components';
+
+import UploadProgressIndicator from './UploadProgressIndicator';
+import { FileState, ProgressIndicatorTypes } from '../../utils';
+import { themed } from '../../styles/theme';
+
+import closeRound from '../../images/icons/close-round.png';
+
+const Container = styled.View`
+ height: 70;
+ display: flex;
+ padding: 10px;
+ ${({ theme }) => theme.messageInput.imageUploadPreview.container.css};
+`;
+
+const ItemContainer = styled.View`
+ display: flex;
+ height: 50;
+ flex-direction: row;
+ align-items: flex-start;
+ margin-left: 5;
+ ${({ theme }) => theme.messageInput.imageUploadPreview.itemContainer.css};
+`;
+
+const Dismiss = styled.TouchableOpacity`
+ position: absolute;
+ top: 5;
+ right: 5;
+ background-color: #fff;
+ width: 20;
+ height: 20;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 20;
+ ${({ theme }) => theme.messageInput.imageUploadPreview.dismiss.css};
+`;
+
+const Upload = styled.Image`
+ width: 50;
+ height: 50;
+ border-radius: 10;
+ ${({ theme }) => theme.messageInput.imageUploadPreview.upload.css};
+`;
+
+const DismissImage = styled.Image`
+ width: 10;
+ height: 10;
+ ${({ theme }) => theme.messageInput.imageUploadPreview.dismissImage.css};
+`;
+
+/**
+ * UI Component to preview the images set for upload
+ *
+ * @example ./docs/ImageUploadPreview.md
+ * @extends PureComponent
+ */
+class ImageUploadPreview extends React.PureComponent {
+ static themePath = 'messageInput.imageUploadPreview';
+ constructor(props) {
+ super(props);
+ }
+ static propTypes = {
+ /**
+ * Its an object/map of id vs image objects which are set for upload. It has following structure:
+ *
+ * ```json
+ * {
+ * "randomly_generated_temp_id_1": {
+ * "id": "randomly_generated_temp_id_1",
+ * "file": // File object
+ * "status": "Uploading" // or "Finished"
+ * },
+ * "randomly_generated_temp_id_2": {
+ * "id": "randomly_generated_temp_id_2",
+ * "file": // File object
+ * "status": "Uploading" // or "Finished"
+ * },
+ * }
+ * ```
+ *
+ * */
+ imageUploads: PropTypes.array.isRequired,
+ /**
+ * @param id Index of image in `imageUploads` array in state of MessageInput.
+ */
+ removeImage: PropTypes.func,
+ /**
+ * @param id Index of image in `imageUploads` array in state of MessageInput.
+ */
+ retryUpload: PropTypes.func,
+ };
+
+ _renderItem = ({ item }) => {
+ let type;
+
+ const { retryUpload } = this.props;
+
+ if (item.state === FileState.UPLOADING)
+ type = ProgressIndicatorTypes.IN_PROGRESS;
+
+ if (item.state === FileState.UPLOAD_FAILED)
+ type = ProgressIndicatorTypes.RETRY;
+ return (
+
+
+
+
+
+ {
+ this.props.removeImage(item.id);
+ }}
+ >
+
+
+
+
+ );
+ };
+
+ render() {
+ if (!this.props.imageUploads || this.props.imageUploads.length === 0)
+ return null;
+
+ return (
+
+ item.id}
+ renderItem={this._renderItem}
+ />
+
+ );
+ }
+}
+
+export default themed(ImageUploadPreview);
diff --git a/src/components/MessageInput.js b/src/components/MessageInput/MessageInput.js
similarity index 97%
rename from src/components/MessageInput.js
rename to src/components/MessageInput/MessageInput.js
index e8378da478..b45b4dea87 100644
--- a/src/components/MessageInput.js
+++ b/src/components/MessageInput/MessageInput.js
@@ -1,32 +1,35 @@
import React, { PureComponent } from 'react';
import { View } from 'react-native';
-import {
- withChannelContext,
- withSuggestionsContext,
- withKeyboardContext,
- withTranslationContext,
-} from '../context';
-import { logChatPromiseExecution } from 'stream-chat';
-import { ImageUploadPreview } from './ImageUploadPreview';
-import { FileUploadPreview } from './FileUploadPreview';
-import { IconSquare } from './IconSquare';
-import { pickImage, pickDocument } from '../native';
import { lookup } from 'mime-types';
import Immutable from 'seamless-immutable';
-import { FileState, ACITriggerSettings } from '../utils';
import PropTypes from 'prop-types';
import uniq from 'lodash/uniq';
import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
-import { SendButton } from './SendButton';
-import { AttachButton } from './AttachButton';
-
import { ActionSheetCustom as ActionSheet } from 'react-native-actionsheet';
-import iconGallery from '../images/icons/icon_attach-media.png';
-import iconFolder from '../images/icons/icon_folder.png';
-import iconClose from '../images/icons/icon_close.png';
-import { AutoCompleteInput } from './AutoCompleteInput';
+import { logChatPromiseExecution } from 'stream-chat';
+
+import {
+ withChannelContext,
+ withSuggestionsContext,
+ withKeyboardContext,
+ withTranslationContext,
+} from '../../context';
+import { IconSquare } from '../IconSquare';
+
+import { pickImage, pickDocument } from '../../native';
+import { FileState, ACITriggerSettings } from '../../utils';
+import { themed } from '../../styles/theme';
+
+import SendButton from './SendButton';
+import AttachButton from './AttachButton';
+import ImageUploadPreview from './ImageUploadPreview';
+import FileUploadPreview from './FileUploadPreview';
+import { AutoCompleteInput } from '../AutoCompleteInput';
+
+import iconGallery from '../../images/icons/icon_attach-media.png';
+import iconFolder from '../../images/icons/icon_folder.png';
+import iconClose from '../../images/icons/icon_close.png';
// https://stackoverflow.com/a/6860916/2570866
function generateRandomId() {
@@ -1036,12 +1039,11 @@ class MessageInput extends PureComponent {
}
}
-const MessageInputWithContext = withTranslationContext(
+export default withTranslationContext(
withKeyboardContext(
withSuggestionsContext(withChannelContext(themed(MessageInput))),
),
);
-export { MessageInputWithContext as MessageInput };
const AttachmentActionSheetItem = ({ icon, text }) => (
diff --git a/src/components/MessageInput/SendButton.js b/src/components/MessageInput/SendButton.js
new file mode 100644
index 0000000000..9b9ec633ef
--- /dev/null
+++ b/src/components/MessageInput/SendButton.js
@@ -0,0 +1,57 @@
+import React from 'react';
+import styled from '@stream-io/styled-components';
+
+import { themed } from '../../styles/theme';
+import iconEdit from '../../images/icons/icon_edit.png';
+import iconSendNewMessage from '../../images/icons/icon_new_message.png';
+
+import PropTypes from 'prop-types';
+
+const Container = styled.TouchableOpacity`
+ margin-left: 8;
+ ${({ theme }) => theme.messageInput.sendButton.css}
+`;
+
+const SendButtonIcon = styled.Image`
+ width: 15;
+ height: 15;
+ ${({ theme }) => theme.messageInput.sendButtonIcon.css}
+`;
+
+/**
+ * UI Component for send button in MessageInput component.
+ *
+ * @extends PureComponent
+ * @example ./docs/SendButton.md
+ */
+class SendButton extends React.PureComponent {
+ static themePath = 'messageInput';
+ static propTypes = {
+ title: PropTypes.string,
+ /** @see See [channel context](https://getstream.github.io/stream-chat-react-native/#channelcontext) */
+ editing: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
+ /** Function that sends message */
+ sendMessage: PropTypes.func.isRequired,
+ /** Disables the button */
+ disabled: PropTypes.bool,
+ };
+
+ static defaultProps = {
+ disabled: false,
+ };
+
+ render() {
+ const { sendMessage, editing, title, disabled } = this.props;
+ return (
+
+ {editing ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+}
+
+export default themed(SendButton);
diff --git a/src/components/MessageInput/UploadProgressIndicator.js b/src/components/MessageInput/UploadProgressIndicator.js
new file mode 100644
index 0000000000..e110665091
--- /dev/null
+++ b/src/components/MessageInput/UploadProgressIndicator.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import { View, ActivityIndicator, Image, TouchableOpacity } from 'react-native';
+import PropTypes from 'prop-types';
+import styled from '@stream-io/styled-components';
+
+import iconReload from '../../images/reload1.png';
+import { ProgressIndicatorTypes } from '../../utils';
+import { themed } from '../../styles/theme';
+
+const Overlay = styled.View`
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(0, 0, 0, 0.3);
+ opacity: 0;
+ ${({ theme }) => theme.messageInput.uploadProgressIndicator.overlay.css};
+`;
+
+const Container = styled.View`
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0);
+ ${({ theme }) => theme.messageInput.uploadProgressIndicator.container.css};
+`;
+
+class UploadProgressIndicator extends React.PureComponent {
+ static themePath = 'messageInput.uploadProgressIndicator';
+ constructor(props) {
+ super(props);
+ }
+
+ static propTypes = {
+ active: PropTypes.bool,
+ type: PropTypes.oneOf([
+ ProgressIndicatorTypes.IN_PROGRESS,
+ ProgressIndicatorTypes.RETRY,
+ ]),
+ action: PropTypes.func,
+ };
+
+ render() {
+ const { active, children, type } = this.props;
+ if (!active) {
+ return {children};
+ }
+
+ return (
+
+ {children}
+
+
+ {type === ProgressIndicatorTypes.IN_PROGRESS && (
+
+
+
+ )}
+ {type === ProgressIndicatorTypes.RETRY && (
+
+ )}
+
+
+ );
+ }
+}
+
+export default themed(UploadProgressIndicator);
diff --git a/src/components/MessageInput/index.js b/src/components/MessageInput/index.js
new file mode 100644
index 0000000000..036ef80ecc
--- /dev/null
+++ b/src/components/MessageInput/index.js
@@ -0,0 +1,6 @@
+export { default as AttachButton } from './AttachButton';
+export { default as FileUploadPreview } from './FileUploadPreview';
+export { default as ImageUploadPreview } from './ImageUploadPreview';
+export { default as MessageInput } from './MessageInput';
+export { default as SendButton } from './SendButton';
+export { default as UploadProgressIndicator } from './UploadProgressIndicator';
diff --git a/src/components/DateSeparator.js b/src/components/MessageList/DateSeparator.js
similarity index 88%
rename from src/components/DateSeparator.js
rename to src/components/MessageList/DateSeparator.js
index 86035ea426..a3657b32c0 100644
--- a/src/components/DateSeparator.js
+++ b/src/components/MessageList/DateSeparator.js
@@ -1,8 +1,9 @@
import React from 'react';
import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
import PropTypes from 'prop-types';
-import { withTranslationContext } from '../context';
+
+import { themed } from '../../styles/theme';
+import { withTranslationContext } from '../../context';
const Container = styled.View`
display: flex;
@@ -79,5 +80,4 @@ class DateSeparator extends React.PureComponent {
}
}
-const DateSeparatorWithContext = withTranslationContext(themed(DateSeparator));
-export { DateSeparatorWithContext as DateSeparator };
+export default withTranslationContext(themed(DateSeparator));
diff --git a/src/components/EventIndicator.js b/src/components/MessageList/EventIndicator.js
similarity index 89%
rename from src/components/EventIndicator.js
rename to src/components/MessageList/EventIndicator.js
index eebfaa6a9e..f6082eb5b0 100644
--- a/src/components/EventIndicator.js
+++ b/src/components/MessageList/EventIndicator.js
@@ -1,8 +1,9 @@
import React from 'react';
import styled from '@stream-io/styled-components';
-import { Avatar } from './Avatar';
import PropTypes from 'prop-types';
-import { withTranslationContext } from '../context';
+
+import { withTranslationContext } from '../../context';
+import { Avatar } from '../Avatar';
const Date = styled.Text`
font-size: 10;
@@ -66,5 +67,4 @@ EventIndicator.propTypes = {
event: PropTypes.object,
};
-const EventIndicatorWithContext = withTranslationContext(EventIndicator);
-export { EventIndicatorWithContext as EventIndicator };
+export default withTranslationContext(EventIndicator);
diff --git a/src/components/MessageList.js b/src/components/MessageList/MessageList.js
similarity index 97%
rename from src/components/MessageList.js
rename to src/components/MessageList/MessageList.js
index f18db57104..aa46c5699f 100644
--- a/src/components/MessageList.js
+++ b/src/components/MessageList/MessageList.js
@@ -1,15 +1,17 @@
import React, { PureComponent } from 'react';
import { View, TouchableOpacity } from 'react-native';
-import { withChannelContext, withTranslationContext } from '../context';
import styled from '@stream-io/styled-components';
import PropTypes from 'prop-types';
import uuidv4 from 'uuid/v4';
-import { Message } from './Message';
-import { EventIndicator } from './EventIndicator';
-import { MessageNotification } from './MessageNotification';
-import { DateSeparator } from './DateSeparator';
-import { TypingIndicator } from './TypingIndicator';
+import { withChannelContext, withTranslationContext } from '../../context';
+import { Message } from '../Message';
+
+import EventIndicator from './EventIndicator';
+import MessageNotification from './MessageNotification';
+import DateSeparator from './DateSeparator';
+import TypingIndicator from './TypingIndicator';
+import MessageSystem from './MessageSystem';
const ListContainer = styled.FlatList`
flex: 1;
@@ -523,6 +525,8 @@ class MessageList extends PureComponent {
const EventIndicator =
this.props.eventIndicator || this.props.EventIndicator;
return ;
+ } else if (message.type === 'system') {
+ return ;
} else if (message.type !== 'message.read') {
const readBy = this.readData[message.id] || [];
return (
@@ -696,7 +700,4 @@ class MessageList extends PureComponent {
}
}
-const MessageListWithContext = withTranslationContext(
- withChannelContext(MessageList),
-);
-export { MessageListWithContext as MessageList };
+export default withTranslationContext(withChannelContext(MessageList));
diff --git a/src/components/MessageNotification.js b/src/components/MessageList/MessageNotification.js
similarity index 90%
rename from src/components/MessageNotification.js
rename to src/components/MessageList/MessageNotification.js
index a772c4fdd8..c34d678b0a 100644
--- a/src/components/MessageNotification.js
+++ b/src/components/MessageList/MessageNotification.js
@@ -2,8 +2,9 @@ import React, { PureComponent } from 'react';
import { Animated } from 'react-native';
import PropTypes from 'prop-types';
import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
-import { withTranslationContext } from '../context';
+
+import { themed } from '../../styles/theme';
+import { withTranslationContext } from '../../context';
const Container = styled.TouchableOpacity`
display: flex;
@@ -92,7 +93,4 @@ class MessageNotification extends PureComponent {
}
}
-const MessageNotificationWithContext = withTranslationContext(
- themed(MessageNotification),
-);
-export { MessageNotificationWithContext as MessageNotification };
+export default withTranslationContext(themed(MessageNotification));
diff --git a/src/components/MessageSystem.js b/src/components/MessageList/MessageSystem.js
similarity index 89%
rename from src/components/MessageSystem.js
rename to src/components/MessageList/MessageSystem.js
index b5d484910f..55ff6421c9 100644
--- a/src/components/MessageSystem.js
+++ b/src/components/MessageList/MessageSystem.js
@@ -1,6 +1,7 @@
import React from 'react';
import styled from '@stream-io/styled-components';
-import { withTranslationContext } from '../context';
+
+import { withTranslationContext } from '../../context';
const Container = styled.View`
display: flex;
@@ -60,5 +61,4 @@ const MessageSystem = ({ message, tDateTimeParser }) => (
);
-const MessageSystemWithContext = withTranslationContext(MessageSystem);
-export { MessageSystemWithContext as MessageSystem };
+export default withTranslationContext(MessageSystem);
diff --git a/src/components/TypingIndicator.js b/src/components/MessageList/TypingIndicator.js
similarity index 90%
rename from src/components/TypingIndicator.js
rename to src/components/MessageList/TypingIndicator.js
index 6bd495cd17..88e387d2a2 100644
--- a/src/components/TypingIndicator.js
+++ b/src/components/MessageList/TypingIndicator.js
@@ -1,12 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Avatar } from './Avatar';
import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
-import { withTranslationContext } from '../context';
+
import { StreamChat } from 'stream-chat';
+import { Avatar } from '../Avatar';
+import { themed } from '../../styles/theme';
+import { withTranslationContext } from '../../context';
+
const TypingText = styled.Text`
margin-left: 10px;
font-size: ${({ theme }) => theme.typingIndicator.text.fontSize}px;
@@ -92,8 +94,4 @@ class TypingIndicator extends React.PureComponent {
}
}
-const TypingIndicatorWithContext = withTranslationContext(
- themed(TypingIndicator),
-);
-
-export { TypingIndicatorWithContext as TypingIndicator };
+export default withTranslationContext(themed(TypingIndicator));
diff --git a/src/components/MessageList/index.js b/src/components/MessageList/index.js
new file mode 100644
index 0000000000..48b047694f
--- /dev/null
+++ b/src/components/MessageList/index.js
@@ -0,0 +1,6 @@
+export { default as DateSeparator } from './DateSeparator';
+export { default as EventIndicator } from './EventIndicator';
+export { default as MessageList } from './MessageList';
+export { default as MessageNotification } from './MessageNotification';
+export { default as MessageSystem } from './MessageSystem';
+export { default as TypingIndicator } from './TypingIndicator';
diff --git a/src/components/Reaction/ReactionList.js b/src/components/Reaction/ReactionList.js
new file mode 100644
index 0000000000..0636acde34
--- /dev/null
+++ b/src/components/Reaction/ReactionList.js
@@ -0,0 +1,176 @@
+import React from 'react';
+import { Text } from 'react-native';
+import styled from '@stream-io/styled-components';
+import PropTypes from 'prop-types';
+
+import { themed } from '../../styles/theme';
+import { renderReactions } from '../../utils/renderReactions';
+import { emojiData } from '../../utils';
+
+import leftTail from '../../images/reactionlist/left-tail.png';
+import leftCenter from '../../images/reactionlist/left-center.png';
+import leftEnd from '../../images/reactionlist/left-end.png';
+
+import rightTail from '../../images/reactionlist/right-tail.png';
+import rightCenter from '../../images/reactionlist/right-center.png';
+import rightEnd from '../../images/reactionlist/right-end.png';
+
+const TouchableWrapper = styled.View`
+ position: relative;
+ ${({ alignment }) =>
+ alignment === 'left' ? 'left: -10px;' : 'right: -10px;'}
+ height: 28px;
+ z-index: 10;
+ align-self: ${({ alignment }) =>
+ alignment === 'left' ? 'flex-start' : 'flex-end'};
+`;
+
+const Container = styled.View`
+ opacity: ${({ visible }) => (visible ? 1 : 0)};
+ z-index: 10;
+ height: 24px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding-left: 5px;
+ padding-right: 5px;
+ ${({ theme }) => theme.message.reactionList.container.css}
+`;
+
+const ReactionCount = styled(({ reactionCounts, ...rest }) => (
+
+))`
+ color: white;
+ font-size: 12;
+ ${({ reactionCounts }) => (reactionCounts < 10 ? null : 'min-width: 20px;')}
+ ${({ theme }) => theme.message.reactionList.reactionCount.css}
+`;
+
+const ImageWrapper = styled.View`
+ display: flex;
+ flex-direction: row;
+ top: -23px;
+ opacity: ${({ visible }) => (visible ? 1 : 0)};
+`;
+
+const LeftTail = styled.Image`
+ width: 25px;
+ height: 33px;
+`;
+
+const LeftCenter = styled.Image`
+ height: 33;
+ flex: 1;
+`;
+
+const LeftEnd = styled.Image`
+ width: 14px;
+ height: 33px;
+`;
+
+const RightTail = styled.Image`
+ width: 25px;
+ height: 33px;
+`;
+
+const RightCenter = styled.Image`
+ height: 33;
+ flex: 1;
+`;
+
+const RightEnd = styled.Image`
+ width: 14px;
+ height: 33px;
+`;
+
+const Reactions = styled.View`
+ flex-direction: row;
+`;
+
+/**
+ * @example ./docs/ReactionList.md
+ * @extends PureComponent
+ */
+
+class ReactionList extends React.PureComponent {
+ static themePath = 'message.reactionList';
+
+ constructor(props) {
+ super(props);
+ }
+
+ static propTypes = {
+ latestReactions: PropTypes.array,
+ openReactionSelector: PropTypes.func,
+ getTotalReactionCount: PropTypes.func,
+ visible: PropTypes.bool,
+ position: PropTypes.string,
+ /**
+ * e.g.,
+ * [
+ * {
+ * id: 'like',
+ * icon: '👍',
+ * },
+ * {
+ * id: 'love',
+ * icon: '❤️️',
+ * },
+ * {
+ * id: 'haha',
+ * icon: '😂',
+ * },
+ * {
+ * id: 'wow',
+ * icon: '😮',
+ * },
+ * ]
+ */
+ supportedReactions: PropTypes.array,
+ };
+
+ static defaultProps = {
+ supportedReactions: emojiData,
+ };
+
+ render() {
+ const {
+ latestReactions,
+ getTotalReactionCount,
+ visible,
+ alignment,
+ supportedReactions,
+ } = this.props;
+ return (
+
+
+
+ {renderReactions(latestReactions, supportedReactions)}
+
+
+ {getTotalReactionCount(supportedReactions)}
+
+
+
+ {alignment === 'left' ? (
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ )}
+
+
+ );
+ }
+}
+
+export default themed(ReactionList);
diff --git a/src/components/Reaction/ReactionPicker.js b/src/components/Reaction/ReactionPicker.js
new file mode 100644
index 0000000000..6a7aabf246
--- /dev/null
+++ b/src/components/Reaction/ReactionPicker.js
@@ -0,0 +1,182 @@
+import React from 'react';
+import { View, Modal } from 'react-native';
+import PropTypes from 'prop-types';
+
+import styled from '@stream-io/styled-components';
+
+import { themed } from '../../styles/theme';
+import { Avatar } from '../Avatar';
+import { emojiData } from '../../utils';
+
+const Container = styled.TouchableOpacity`
+ flex: 1;
+ align-items: ${({ leftAlign }) => (leftAlign ? 'flex-start' : 'flex-end')};
+ ${({ theme }) => theme.message.reactionPicker.container.css}
+`;
+
+const ContainerView = styled.View`
+ display: flex;
+ flex-direction: row;
+ background-color: black;
+ padding-left: 20px;
+ height: 60;
+ padding-right: 20px;
+ border-radius: 30;
+ ${({ theme }) => theme.message.reactionPicker.containerView.css}
+`;
+
+const Column = styled.View`
+ flex-direction: column;
+ align-items: center;
+ margin-top: -5;
+ ${({ theme }) => theme.message.reactionPicker.column.css}
+`;
+
+const Emoji = styled.Text`
+ font-size: 20;
+ margin-bottom: 5;
+ margin-top: 5;
+ ${({ theme }) => theme.message.reactionPicker.emoji.css}
+`;
+
+const ReactionCount = styled.Text`
+ color: white;
+ font-size: 10;
+ font-weight: bold;
+ ${({ theme }) => theme.message.reactionPicker.text.css}
+`;
+
+class ReactionPicker extends React.PureComponent {
+ static themePath = 'message.reactionPicker';
+
+ static propTypes = {
+ hideReactionCount: PropTypes.bool,
+ hideReactionOwners: PropTypes.bool,
+ reactionPickerVisible: PropTypes.bool,
+ handleDismiss: PropTypes.func,
+ handleReaction: PropTypes.func,
+ latestReactions: PropTypes.array,
+ reactionCounts: PropTypes.object,
+ rpLeft: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ rpTop: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ rpRight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ supportedReactions: PropTypes.array,
+ };
+
+ static defaultProps = {
+ hideReactionCount: false,
+ hideReactionOwners: false,
+ supportedReactions: emojiData,
+ rpTop: 40,
+ rpLeft: 30,
+ rpRight: 10,
+ };
+
+ constructor(props) {
+ super(props);
+ }
+
+ getUsersPerReaction = (reactions, type) => {
+ const filtered =
+ reactions && reactions.filter((item) => item.type === type);
+ return filtered;
+ };
+
+ getLatestUser = (reactions, type) => {
+ const filtered = this.getUsersPerReaction(reactions, type);
+ if (filtered && filtered[0] && filtered[0].user) {
+ return filtered[0].user;
+ } else {
+ return 'NotFound';
+ }
+ };
+
+ render() {
+ const {
+ hideReactionCount,
+ hideReactionOwners,
+ reactionPickerVisible,
+ handleDismiss,
+ handleReaction,
+ latestReactions,
+ reactionCounts,
+ rpLeft,
+ rpTop,
+ rpRight,
+ supportedReactions,
+ } = this.props;
+
+ if (!reactionPickerVisible) return null;
+
+ const position = {
+ marginTop: rpTop,
+ };
+
+ if (rpLeft) position.marginLeft = rpLeft;
+
+ if (rpRight) position.marginRight = rpRight;
+
+ return (
+ {}}
+ onRequestClose={handleDismiss}
+ >
+ {reactionPickerVisible && (
+
+
+ {supportedReactions.map(({ id, icon }) => {
+ const latestUser = this.getLatestUser(latestReactions, id);
+ const count = reactionCounts && reactionCounts[id];
+ return (
+
+ {latestUser !== 'NotFound' && !hideReactionOwners ? (
+
+ ) : (
+ !hideReactionOwners && (
+
+ )
+ )}
+ {
+ handleReaction(id);
+ }}
+ >
+ {icon}
+
+ {!hideReactionCount && (
+ {count > 0 ? count : ''}
+ )}
+
+ );
+ })}
+
+
+ )}
+
+ );
+ }
+}
+
+export default themed(ReactionPicker);
diff --git a/src/components/ReactionPickerWrapper.js b/src/components/Reaction/ReactionPickerWrapper.js
similarity index 94%
rename from src/components/ReactionPickerWrapper.js
rename to src/components/Reaction/ReactionPickerWrapper.js
index 2e5c82ca80..a88343b225 100644
--- a/src/components/ReactionPickerWrapper.js
+++ b/src/components/Reaction/ReactionPickerWrapper.js
@@ -3,10 +3,10 @@ import PropTypes from 'prop-types';
import { TouchableOpacity, Dimensions } from 'react-native';
-import { ReactionPicker } from './ReactionPicker';
-import { emojiData } from '../utils';
+import ReactionPicker from './ReactionPicker';
+import { emojiData } from '../../utils';
-export class ReactionPickerWrapper extends React.PureComponent {
+class ReactionPickerWrapper extends React.PureComponent {
static propTypes = {
isMyMessage: PropTypes.func,
message: PropTypes.object,
@@ -140,3 +140,5 @@ export class ReactionPickerWrapper extends React.PureComponent {
);
}
}
+
+export default ReactionPickerWrapper;
diff --git a/src/components/Reaction/index.js b/src/components/Reaction/index.js
new file mode 100644
index 0000000000..b22a8ebb6c
--- /dev/null
+++ b/src/components/Reaction/index.js
@@ -0,0 +1,3 @@
+export { default as ReactionList } from './ReactionList';
+export { default as ReactionPicker } from './ReactionPicker';
+export { default as ReactionPickerWrapper } from './ReactionPickerWrapper';
diff --git a/src/components/ReactionList.js b/src/components/ReactionList.js
deleted file mode 100644
index ec2bb02c0e..0000000000
--- a/src/components/ReactionList.js
+++ /dev/null
@@ -1,175 +0,0 @@
-import React from 'react';
-import { Text } from 'react-native';
-import styled from '@stream-io/styled-components';
-import PropTypes from 'prop-types';
-import { themed } from '../styles/theme';
-import { renderReactions } from '../utils/renderReactions';
-import { emojiData } from '../utils';
-
-import leftTail from '../images/reactionlist/left-tail.png';
-import leftCenter from '../images/reactionlist/left-center.png';
-import leftEnd from '../images/reactionlist/left-end.png';
-
-import rightTail from '../images/reactionlist/right-tail.png';
-import rightCenter from '../images/reactionlist/right-center.png';
-import rightEnd from '../images/reactionlist/right-end.png';
-
-const TouchableWrapper = styled.View`
- position: relative;
- ${({ alignment }) =>
- alignment === 'left' ? 'left: -10px;' : 'right: -10px;'}
- height: 28px;
- z-index: 10;
- align-self: ${({ alignment }) =>
- alignment === 'left' ? 'flex-start' : 'flex-end'};
-`;
-
-const Container = styled.View`
- opacity: ${({ visible }) => (visible ? 1 : 0)};
- z-index: 10;
- height: 24px;
- display: flex;
- flex-direction: row;
- align-items: center;
- padding-left: 5px;
- padding-right: 5px;
- ${({ theme }) => theme.message.reactionList.container.css}
-`;
-
-const ReactionCount = styled(({ reactionCounts, ...rest }) => (
-
-))`
- color: white;
- font-size: 12;
- ${({ reactionCounts }) => (reactionCounts < 10 ? null : 'min-width: 20px;')}
- ${({ theme }) => theme.message.reactionList.reactionCount.css}
-`;
-
-const ImageWrapper = styled.View`
- display: flex;
- flex-direction: row;
- top: -23px;
- opacity: ${({ visible }) => (visible ? 1 : 0)};
-`;
-
-const LeftTail = styled.Image`
- width: 25px;
- height: 33px;
-`;
-
-const LeftCenter = styled.Image`
- height: 33;
- flex: 1;
-`;
-
-const LeftEnd = styled.Image`
- width: 14px;
- height: 33px;
-`;
-
-const RightTail = styled.Image`
- width: 25px;
- height: 33px;
-`;
-
-const RightCenter = styled.Image`
- height: 33;
- flex: 1;
-`;
-
-const RightEnd = styled.Image`
- width: 14px;
- height: 33px;
-`;
-
-const Reactions = styled.View`
- flex-direction: row;
-`;
-
-/**
- * @example ./docs/ReactionList.md
- * @extends PureComponent
- */
-
-export const ReactionList = themed(
- class ReactionList extends React.PureComponent {
- static themePath = 'message.reactionList';
-
- constructor(props) {
- super(props);
- }
-
- static propTypes = {
- latestReactions: PropTypes.array,
- openReactionSelector: PropTypes.func,
- getTotalReactionCount: PropTypes.func,
- visible: PropTypes.bool,
- position: PropTypes.string,
- /**
- * e.g.,
- * [
- * {
- * id: 'like',
- * icon: '👍',
- * },
- * {
- * id: 'love',
- * icon: '❤️️',
- * },
- * {
- * id: 'haha',
- * icon: '😂',
- * },
- * {
- * id: 'wow',
- * icon: '😮',
- * },
- * ]
- */
- supportedReactions: PropTypes.array,
- };
-
- static defaultProps = {
- supportedReactions: emojiData,
- };
-
- render() {
- const {
- latestReactions,
- getTotalReactionCount,
- visible,
- alignment,
- supportedReactions,
- } = this.props;
- return (
-
-
-
- {renderReactions(latestReactions, supportedReactions)}
-
-
- {getTotalReactionCount(supportedReactions)}
-
-
-
- {alignment === 'left' ? (
-
-
-
-
-
- ) : (
-
-
-
-
-
- )}
-
-
- );
- }
- },
-);
diff --git a/src/components/ReactionPicker.js b/src/components/ReactionPicker.js
deleted file mode 100644
index c44cd7b5fc..0000000000
--- a/src/components/ReactionPicker.js
+++ /dev/null
@@ -1,181 +0,0 @@
-import React from 'react';
-import { View, Modal } from 'react-native';
-import { themed } from '../styles/theme';
-import PropTypes from 'prop-types';
-
-import styled from '@stream-io/styled-components';
-import { Avatar } from './Avatar';
-import { emojiData } from '../utils';
-
-const Container = styled.TouchableOpacity`
- flex: 1;
- align-items: ${({ leftAlign }) => (leftAlign ? 'flex-start' : 'flex-end')};
- ${({ theme }) => theme.message.reactionPicker.container.css}
-`;
-
-const ContainerView = styled.View`
- display: flex;
- flex-direction: row;
- background-color: black;
- padding-left: 20px;
- height: 60;
- padding-right: 20px;
- border-radius: 30;
- ${({ theme }) => theme.message.reactionPicker.containerView.css}
-`;
-
-const Column = styled.View`
- flex-direction: column;
- align-items: center;
- margin-top: -5;
- ${({ theme }) => theme.message.reactionPicker.column.css}
-`;
-
-const Emoji = styled.Text`
- font-size: 20;
- margin-bottom: 5;
- margin-top: 5;
- ${({ theme }) => theme.message.reactionPicker.emoji.css}
-`;
-
-const ReactionCount = styled.Text`
- color: white;
- font-size: 10;
- font-weight: bold;
- ${({ theme }) => theme.message.reactionPicker.text.css}
-`;
-
-export const ReactionPicker = themed(
- class ReactionPicker extends React.PureComponent {
- static themePath = 'message.reactionPicker';
-
- static propTypes = {
- hideReactionCount: PropTypes.bool,
- hideReactionOwners: PropTypes.bool,
- reactionPickerVisible: PropTypes.bool,
- handleDismiss: PropTypes.func,
- handleReaction: PropTypes.func,
- latestReactions: PropTypes.array,
- reactionCounts: PropTypes.object,
- rpLeft: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- rpTop: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- rpRight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- supportedReactions: PropTypes.array,
- };
-
- static defaultProps = {
- hideReactionCount: false,
- hideReactionOwners: false,
- supportedReactions: emojiData,
- rpTop: 40,
- rpLeft: 30,
- rpRight: 10,
- };
-
- constructor(props) {
- super(props);
- }
-
- getUsersPerReaction = (reactions, type) => {
- const filtered =
- reactions && reactions.filter((item) => item.type === type);
- return filtered;
- };
-
- getLatestUser = (reactions, type) => {
- const filtered = this.getUsersPerReaction(reactions, type);
- if (filtered && filtered[0] && filtered[0].user) {
- return filtered[0].user;
- } else {
- return 'NotFound';
- }
- };
-
- render() {
- const {
- hideReactionCount,
- hideReactionOwners,
- reactionPickerVisible,
- handleDismiss,
- handleReaction,
- latestReactions,
- reactionCounts,
- rpLeft,
- rpTop,
- rpRight,
- supportedReactions,
- } = this.props;
-
- if (!reactionPickerVisible) return null;
-
- const position = {
- marginTop: rpTop,
- };
-
- if (rpLeft) position.marginLeft = rpLeft;
-
- if (rpRight) position.marginRight = rpRight;
-
- return (
- {}}
- onRequestClose={handleDismiss}
- >
- {reactionPickerVisible && (
-
-
- {supportedReactions.map(({ id, icon }) => {
- const latestUser = this.getLatestUser(latestReactions, id);
- const count = reactionCounts && reactionCounts[id];
- return (
-
- {latestUser !== 'NotFound' && !hideReactionOwners ? (
-
- ) : (
- !hideReactionOwners && (
-
- )
- )}
- {
- handleReaction(id);
- }}
- >
- {icon}
-
- {!hideReactionCount && (
- {count > 0 ? count : ''}
- )}
-
- );
- })}
-
-
- )}
-
- );
- }
- },
-);
diff --git a/src/components/SendButton.js b/src/components/SendButton.js
deleted file mode 100644
index 41f6c33ae1..0000000000
--- a/src/components/SendButton.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-import styled from '@stream-io/styled-components';
-import iconEdit from '../images/icons/icon_edit.png';
-import iconSendNewMessage from '../images/icons/icon_new_message.png';
-import { themed } from '../styles/theme';
-import PropTypes from 'prop-types';
-
-const Container = styled.TouchableOpacity`
- margin-left: 8;
- ${({ theme }) => theme.messageInput.sendButton.css}
-`;
-
-const SendButtonIcon = styled.Image`
- width: 15;
- height: 15;
- ${({ theme }) => theme.messageInput.sendButtonIcon.css}
-`;
-
-/**
- * UI Component for send button in MessageInput component.
- *
- * @extends PureComponent
- * @example ./docs/SendButton.md
- */
-export const SendButton = themed(
- class SendButton extends React.PureComponent {
- static themePath = 'messageInput';
- static propTypes = {
- title: PropTypes.string,
- /** @see See [channel context](https://getstream.github.io/stream-chat-react-native/#channelcontext) */
- editing: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
- /** Function that sends message */
- sendMessage: PropTypes.func.isRequired,
- /** Disables the button */
- disabled: PropTypes.bool,
- };
-
- static defaultProps = {
- disabled: false,
- };
-
- render() {
- const { sendMessage, editing, title, disabled } = this.props;
- return (
-
- {editing ? (
-
- ) : (
-
- )}
-
- );
- }
- },
-);
diff --git a/src/components/Spinner.js b/src/components/Spinner.js
deleted file mode 100644
index 45d07e01c5..0000000000
--- a/src/components/Spinner.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import React from 'react';
-import { View, Animated, Easing } from 'react-native';
-import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
-
-const AnimatedView = Animated.createAnimatedComponent(View);
-
-export const Circle = styled(AnimatedView)`
- height: 30px;
- width: 30px;
- margin: 5px;
- border-width: 2px;
- border-radius: 30px;
- justify-content: center;
- border-color: ${({ theme }) => theme.colors.primary};
- border-right-color: transparent;
- ${({ theme }) => theme.spinner.css}
-`;
-/**
- * @example ./docs/Spinner.md
- * @extends PureComponent
- */
-export const Spinner = themed(
- class Spinner extends React.PureComponent {
- static themePath = 'spinner';
- state = {
- rotateValue: new Animated.Value(0),
- };
-
- componentDidMount() {
- this._start();
- }
-
- _start = () => {
- Animated.loop(
- Animated.timing(this.state.rotateValue, {
- toValue: 1,
- duration: 800,
- easing: Easing.linear,
- useNativeDriver: true,
- }),
- ).start();
- };
-
- render() {
- return (
-
- );
- }
- },
-);
diff --git a/src/components/Spinner/Spinner.js b/src/components/Spinner/Spinner.js
new file mode 100644
index 0000000000..f7ad48e678
--- /dev/null
+++ b/src/components/Spinner/Spinner.js
@@ -0,0 +1,62 @@
+import React from 'react';
+import { View, Animated, Easing } from 'react-native';
+import styled from '@stream-io/styled-components';
+import { themed } from '../../styles/theme';
+
+const AnimatedView = Animated.createAnimatedComponent(View);
+
+export const Circle = styled(AnimatedView)`
+ height: 30px;
+ width: 30px;
+ margin: 5px;
+ border-width: 2px;
+ border-radius: 30px;
+ justify-content: center;
+ border-color: ${({ theme }) => theme.colors.primary};
+ border-right-color: transparent;
+ ${({ theme }) => theme.spinner.css}
+`;
+/**
+ * @example ./docs/Spinner.md
+ * @extends PureComponent
+ */
+class Spinner extends React.PureComponent {
+ static themePath = 'spinner';
+ state = {
+ rotateValue: new Animated.Value(0),
+ };
+
+ componentDidMount() {
+ this._start();
+ }
+
+ _start = () => {
+ Animated.loop(
+ Animated.timing(this.state.rotateValue, {
+ toValue: 1,
+ duration: 800,
+ easing: Easing.linear,
+ useNativeDriver: true,
+ }),
+ ).start();
+ };
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default themed(Spinner);
diff --git a/src/components/Spinner/index.js b/src/components/Spinner/index.js
new file mode 100644
index 0000000000..a82c4007c8
--- /dev/null
+++ b/src/components/Spinner/index.js
@@ -0,0 +1 @@
+export { default as Spinner } from './Spinner';
diff --git a/src/components/SuggestionsProvider.js b/src/components/SuggestionsProvider/SuggestionsProvider.js
similarity index 97%
rename from src/components/SuggestionsProvider.js
rename to src/components/SuggestionsProvider/SuggestionsProvider.js
index 4fc741d8e0..d5c75eca9a 100644
--- a/src/components/SuggestionsProvider.js
+++ b/src/components/SuggestionsProvider/SuggestionsProvider.js
@@ -1,7 +1,7 @@
import React from 'react';
import { View, FlatList, findNodeHandle } from 'react-native';
-import { SuggestionsContext } from '../context';
+import { SuggestionsContext } from '../../context';
import styled from '@stream-io/styled-components';
import PropTypes from 'prop-types';
@@ -52,7 +52,7 @@ const SuggestionsItem = styled.TouchableOpacity`
${({ theme }) => theme.messageInput.suggestions.item.css};
`;
-export class SuggestionsProvider extends React.PureComponent {
+class SuggestionsProvider extends React.PureComponent {
constructor(props) {
super(props);
@@ -236,3 +236,5 @@ class SuggestionsView extends React.PureComponent {
const SuggestionsHeader = ({ title }) => {title};
const SuggestionsSeparator = () => ;
+
+export default SuggestionsProvider;
diff --git a/src/components/SuggestionsProvider/index.js b/src/components/SuggestionsProvider/index.js
new file mode 100644
index 0000000000..6a1468f739
--- /dev/null
+++ b/src/components/SuggestionsProvider/index.js
@@ -0,0 +1 @@
+export { default as SuggestionsProvider } from './SuggestionsProvider';
diff --git a/src/components/Thread.js b/src/components/Thread/Thread.js
similarity index 94%
rename from src/components/Thread.js
rename to src/components/Thread/Thread.js
index d148e24a84..3d939c586f 100644
--- a/src/components/Thread.js
+++ b/src/components/Thread/Thread.js
@@ -1,13 +1,14 @@
import React from 'react';
-import { withChannelContext, withTranslationContext } from '../context';
-import { MessageList } from './MessageList';
-import { MessageInput } from './MessageInput';
import PropTypes from 'prop-types';
import styled from '@stream-io/styled-components';
-import { themed } from '../styles/theme';
-import { Message } from './Message';
+import { withChannelContext, withTranslationContext } from '../../context';
+import { MessageList } from '../MessageList';
+import { MessageInput } from '../MessageInput';
+import { Message } from '../Message';
+
+import { themed } from '../../styles/theme';
const NewThread = styled.View`
padding: 8px;
@@ -209,8 +210,4 @@ class ThreadInner extends React.PureComponent {
}
}
-const ThreadWithContext = withTranslationContext(
- withChannelContext(themed(Thread)),
-);
-
-export { ThreadWithContext as Thread };
+export default withTranslationContext(withChannelContext(themed(Thread)));
diff --git a/src/components/Thread/index.js b/src/components/Thread/index.js
new file mode 100644
index 0000000000..b268526a66
--- /dev/null
+++ b/src/components/Thread/index.js
@@ -0,0 +1 @@
+export { default as Thread } from './Thread';
diff --git a/src/components/UploadProgressIndicator.js b/src/components/UploadProgressIndicator.js
deleted file mode 100644
index bc7dfc7561..0000000000
--- a/src/components/UploadProgressIndicator.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import React from 'react';
-import { View, ActivityIndicator, Image, TouchableOpacity } from 'react-native';
-import iconReload from '../images/reload1.png';
-import PropTypes from 'prop-types';
-import styled from '@stream-io/styled-components';
-import { ProgressIndicatorTypes } from '../utils';
-import { themed } from '../styles/theme';
-
-const Overlay = styled.View`
- position: absolute;
- height: 100%;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: rgba(0, 0, 0, 0.3);
- opacity: 0;
- ${({ theme }) => theme.messageInput.uploadProgressIndicator.overlay.css};
-`;
-
-const Container = styled.View`
- position: absolute;
- height: 100%;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: rgba(255, 255, 255, 0);
- ${({ theme }) => theme.messageInput.uploadProgressIndicator.container.css};
-`;
-
-export const UploadProgressIndicator = themed(
- class UploadProgressIndicator extends React.PureComponent {
- static themePath = 'messageInput.uploadProgressIndicator';
- constructor(props) {
- super(props);
- }
-
- static propTypes = {
- active: PropTypes.bool,
- type: PropTypes.oneOf([
- ProgressIndicatorTypes.IN_PROGRESS,
- ProgressIndicatorTypes.RETRY,
- ]),
- action: PropTypes.func,
- };
-
- render() {
- const { active, children, type } = this.props;
- if (!active) {
- return {children};
- }
-
- return (
-
- {children}
-
-
- {type === ProgressIndicatorTypes.IN_PROGRESS && (
-
-
-
- )}
- {type === ProgressIndicatorTypes.RETRY && (
-
- )}
-
-
- );
- }
- },
-);
diff --git a/src/components/index.js b/src/components/index.js
index ab5dcfff7c..6889b177a2 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -1,41 +1,16 @@
-export { AutoCompleteInput } from './AutoCompleteInput';
-export { Card } from './Card';
-export { CommandsItem } from './CommandsItem';
-export { DateSeparator } from './DateSeparator';
-export { EmptyStateIndicator } from './EmptyStateIndicator';
-export { EventIndicator } from './EventIndicator';
-export { FileAttachmentGroup } from './FileAttachmentGroup';
-export { FileUploadPreview } from './FileUploadPreview';
-export { Gallery } from './Gallery';
-export { IconSquare } from './IconSquare';
-export { ImageUploadPreview } from './ImageUploadPreview';
-export { KeyboardCompatibleView } from './KeyboardCompatibleView';
-export { LoadingErrorIndicator } from './LoadingErrorIndicator';
-export { LoadingIndicator } from './LoadingIndicator';
-export { MentionsItem } from './MentionsItem';
-export { Message } from './Message';
-export { MessageNotification } from './MessageNotification';
-export { MessageSystem } from './MessageSystem';
-export { ReactionList } from './ReactionList';
-export { Circle, Spinner } from './Spinner';
-export { SuggestionsProvider } from './SuggestionsProvider';
-export { UploadProgressIndicator } from './UploadProgressIndicator';
-export { Attachment } from './Attachment';
-export { AttachmentActions } from './AttachmentActions';
-export { Avatar } from './Avatar';
-export { Chat } from './Chat';
-export { Channel } from './Channel';
-export { MessageList } from './MessageList';
-export { TypingIndicator } from './TypingIndicator';
-export { MessageInput } from './MessageInput';
-export * from './MessageSimple';
-export { ChannelList } from './ChannelList';
-export { Thread } from './Thread';
-export { ChannelPreviewMessenger } from './ChannelPreviewMessenger';
-export { CloseButton } from './CloseButton';
-export { IconBadge } from './IconBadge';
-export { ReactionPicker } from './ReactionPicker';
-export { ReactionPickerWrapper } from './ReactionPickerWrapper';
-export { SendButton } from './SendButton';
-export { AttachButton } from './AttachButton';
-export { FileIcon } from './FileIcon';
+export * from './Attachment';
+export * from './Avatar';
+export * from './Channel';
+export * from './ChannelList';
+export * from './ChannelPreview';
+export * from './Chat';
+export * from './CloseButton';
+export * from './Indicators';
+export * from './KeyboardCompatibleView';
+export * from './Message';
+export * from './MessageInput';
+export * from './MessageList';
+export * from './Reaction';
+export * from './Spinner';
+export * from './SuggestionsProvider';
+export * from './Thread';
diff --git a/src/utils/index.js b/src/utils/index.js
index decc8e66a4..87455018f6 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -1,5 +1,4 @@
-import { MentionsItem } from '../components/MentionsItem';
-import { CommandsItem } from '../components/CommandsItem';
+import { MentionsItem, CommandsItem } from '../components/AutoCompleteInput';
import debounce from 'lodash/debounce';
export { renderText } from './renderText';