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 ? ( +