UNPKG

react-native-chatbox

Version:

react component to implement a placed bottom chatbox which can pick emoji text or others

484 lines (458 loc) 16.2 kB
import React, {Component} from 'react'; import PropTypes from 'prop-types'; import { View, Text, StyleSheet, TouchableOpacity, Image, TextInput, TouchableHighlight, ScrollView, Keyboard, LayoutAnimation, UIManager, PermissionsAndroid, Platform, Alert, ViewPropTypes } from 'react-native'; import {getScreenSize, getPixel} from './utils/common'; import emoji from './config/emoji'; const EXPAND_PANEL_HEIGHT = 150; const INPUT_HEIGHT = 30; const MAX_INPUT_HEIGHT = 200; UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true); export default class ChatBox extends Component { constructor(props) { super(props) this.emojis = this.formatEmojiArr(this.props.emojis || emoji) this.state = { showRecord: true, showEmoji: false, showExtra: false, sendText: '', panelHeight: 0, inputHeight: INPUT_HEIGHT, hasPermission: false } } static propTypes = { emojis: PropTypes.object, containerStyle: ViewPropTypes.style, extraContainerStyle: ViewPropTypes.style, emojiContainerStyle: ViewPropTypes.style, onStartRecord: PropTypes.func.isRequired, onStopRecord: PropTypes.func.isRequired, onSendTextMessage: PropTypes.func.isRequired, extras: PropTypes.arrayOf(PropTypes.object) } static defaultProps = { containerStyle: {}, extraContainerStyle: {}, emojiContainerStyle: {}, extras: [] } componentWillMount() { this.checkPermission() .then(hasPermission => { this.setState({hasPermission}) if (!hasPermission) return }) } async checkPermission() { if (Platform.OS !== 'android') { return Promise.resolve(true) } const rationale = { title: '获取录音权限', message: '正请求获取麦克风权限用于录音' } try { const checkPermissonResult = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO) if(!checkPermissonResult) { const grantPermissionResult = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, rationale) return (grantPermissionResult === true || PermissionsAndroid.RESULTS.GRANTED) }else { return true } }catch(error) { Alert.alert(error) } } async startRecord() { if(!this.state.hasPermission) { alert('没有录音权限,请在设置打开') return } this.props.onStartRecord() } async stopRecord() { this.props.onStopRecord() } formatEmojiArr(emojiMap) { const size = 27 const srcArr = [], disArr = [] emojiMap.forEach(v => srcArr.push(v)) for (let i = 0; i < srcArr.length; i += size) { const emojis = srcArr.slice(i, i + size) disArr.push(emojis) } return disArr } onPickEmoji(emoji) { const sendText = this.state.sendText + emoji this.setState({sendText}) } renderLeft() { return ( this.state.showRecord ? <TouchableOpacity onPress={() => this.setState({showRecord: false, panelHeight: 0, showEmoji: false, showExtra: false})}> <Image source={require('./images/record.png')} style={styles.icon}/> </TouchableOpacity> : <TouchableOpacity onPress={() => { this.setState({showRecord: true}) setTimeout(() => { this.input.focus() }) }}> <Image source={require('./images/keyboard.png')} style={styles.icon}/> </TouchableOpacity> ) } renderCenter() { return ( this.state.showRecord ? <TextInput ref={el => this.input = el} style={[styles.input, {height: this.state.inputHeight}]} multiline={true} value={this.state.sendText} blurOnSubmit={false} numberOfLines={5} onContentSizeChange={event => { const height = event.nativeEvent.contentSize.height if(height > this.state.inputHeight && height < MAX_INPUT_HEIGHT) { this.setState({ inputHeight: height }) }else if(height < INPUT_HEIGHT){ this.setState({ inputHeight: INPUT_HEIGHT }) } }} underlineColorAndroid='transparent' onFocus={() => { LayoutAnimation.configureNext({ duration: 500, update: { type: LayoutAnimation.Types.spring } }) setTimeout(() => { this.setState({panelHeight: 0, showEmoji: false, showRecord: true, showExtra: false}) }) }} onChangeText={value => this.setState({sendText: value})}/> : <TouchableHighlight style={styles.recordButton} delayPressIn={10} onPressIn={() => this.startRecord()} onPressOut={() => this.stopRecord()}> <Text style={{fontSize: 16, color: '#fff'}}>按住录音</Text> </TouchableHighlight> ) } renderRight() { return ( <View style={styles.rightWrapper}> { !this.state.showEmoji ? <TouchableOpacity onPress={() => { setTimeout(() => { LayoutAnimation.configureNext({ duration: 500, update: { type: LayoutAnimation.Types.spring } }) this.setState({panelHeight: EXPAND_PANEL_HEIGHT, showEmoji: true, showRecord: true, showExtra: false}) }, 500) Keyboard.dismiss() }}> <Image source={require('./images/emotion.png')} style={[styles.icon, {marginRight: 5}]}/> </TouchableOpacity> : <TouchableOpacity onPress={() => { LayoutAnimation.configureNext({ duration: 500, update: { type: LayoutAnimation.Types.spring } }) this.setState({panelHeight: 0, showEmoji: false, showExtra: false}) }}> <Image source={require('./images/keyboard.png')} style={[styles.icon, {marginRight: 5}]}/> </TouchableOpacity> } { this.state.sendText === '' ? <TouchableOpacity onPress={() => { setTimeout(() => { LayoutAnimation.configureNext({ duration: 500, update: { type: LayoutAnimation.Types.spring } }) this.setState({panelHeight: EXPAND_PANEL_HEIGHT, showEmoji: false, showExtra: !this.state.showExtra}) }, 500) Keyboard.dismiss() }}> <Image source={require('./images/add.png')} style={styles.icon}/> </TouchableOpacity> : <TouchableOpacity style={styles.sendButton} onPress={() => { this.setState({sendText: ''}) this.props.onSendTextMessage(this.state.sendText) }}> <Text style={{color: '#fff', fontSize: 14}}>发送</Text> </TouchableOpacity> } </View> ) } renderEmojiContent() { return this.emojis.map((pageData, pageNum) => { return ( <View style={styles.page} key={pageNum}> {pageData.map((emoji, index) => this.renderEmojiItem(emoji, index))} {this.renderSwitchMenu(pageNum)} </View> ) }) } renderSwitchMenu(index) { const pages = this.emojis.length const menuWidth = 6 * pages + 8 * ( pages - 1) const menuStyle = { left: (getScreenSize().width - menuWidth) / 2, width: menuWidth } let items = [] for (let i = 0; i < pages; i++) { const itemStyle = i === index ? styles.switchItemCrt : styles.switchItemGrey items.push( <View style={[styles.switchItem, itemStyle]} key={i}></View> ) } return <View style={[styles.switchMenu, menuStyle]}>{items}</View> } renderEmojiItem(emoji, key) { return ( <TouchableOpacity style={styles.btn} key={key} onPress={() => this.onPickEmoji(emoji)}> <Text style={styles.emoji} allowFontScaling={false}> {emoji} </Text> </TouchableOpacity> ) } renderEmoji() { return ( this.state.showEmoji ? <View style={[{height: EXPAND_PANEL_HEIGHT}, this.props.emojiContainerStyle]}> <ScrollView style={[styles.emojiPanel, {height: this.state.panelHeight}]} horizontal={true} showsHorizontalScrollIndicator={false} pagingEnabled={true}> { this.renderEmojiContent() } </ScrollView> </View> : null ) } renderExtraItem() { const extraViews = this.props.extras.map((item, index) => ( <TouchableOpacity style={[styles.extraItemWrapper, item.extraStyle]} onPress={() => item.onExtraClick()} key={index}> <Image source={item.icon} style={[styles.extraIcon, item.extraIconStyle]}/> <Text style={item.textStyle}>{item.text}</Text> </TouchableOpacity> )) return extraViews } renderExtra() { return ( this.state.showExtra ? <View style={[{height: EXPAND_PANEL_HEIGHT}, this.props.extraContainerStyle]}> <ScrollView contentContainerStyle={styles.extraWrapper} horizontal={true}> { this.props.extras && this.props.extras.length > 0 ? this.renderExtraItem() : null } </ScrollView> </View> : null ) } render() { return ( <View> <View style={[styles.container, {height: this.state.inputHeight + 10}, this.props.containerStyle]}> { this.renderLeft() } { this.renderCenter() } { this.renderRight() } </View> { this.renderEmoji() } { this.renderExtra() } </View> ) } } const styles = StyleSheet.create({ container: { width: '100%', flexDirection: 'row', alignItems: 'center', backgroundColor: '#ccc', borderTopWidth: getPixel() * 2, borderTopColor: '#999', paddingHorizontal: 10 }, icon: { width: 30, height: 30 }, input: { flex: 1, backgroundColor: '#fff', borderRadius: 3, padding: 0, paddingLeft: 5, marginHorizontal: 5, borderColor: '#999', borderWidth: getPixel() * 2 }, recordButton: { flex: 1, height: 35, justifyContent: 'center', alignItems: 'center', marginHorizontal: 5, borderRadius: 3, borderWidth: getPixel() * 2, borderColor: '#000' }, rightWrapper: { flexDirection: 'row', alignItems: 'center' }, page: { flexDirection: 'row', flexWrap: 'wrap', paddingTop: 20, paddingHorizontal: 26, width: getScreenSize().width, height: 160, }, btn: { justifyContent: 'center', alignItems: 'center', marginBottom: 10, width: Math.floor((getScreenSize().width - 52) / 9) - 1, height: 30, }, emojiPanel: { width: getScreenSize().width }, emoji: { fontSize: 22 }, switchMenu: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', position: 'absolute', bottom: 15, height: 6 }, switchItem: { width: 6, height: 6, borderRadius: 3 }, switchItemCrt: { backgroundColor: '#666' }, switchItemGrey: { backgroundColor: '#ccc' }, sendButton: { width: 40, height: 30, borderWidth: getPixel() * 2, borderColor: '#000', borderRadius: 3, alignItems: 'center', justifyContent: 'center', backgroundColor: '#999' }, extraWrapper: { paddingTop: 10, paddingLeft: 10 }, extraItemWrapper: { marginRight: 10, alignItems: 'center' }, extraIcon: { width: 60, height: 60 } })