Show imagem attachments (#20)

* attach and offline search

* Update serverNew.js
This commit is contained in:
Guilherme Gazzo 2017-08-15 16:28:46 -03:00 committed by Diego Sampaio
parent 94bcab0d76
commit 2d9bbb87ae
12 changed files with 648 additions and 3197 deletions

View File

@ -7,8 +7,12 @@ import Markdown from 'react-native-easy-markdown';
import moment from 'moment'; import moment from 'moment';
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor'; import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
import Card from './message/card';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
content: {
flexGrow: 1
},
message: { message: {
padding: 12, padding: 12,
paddingTop: 6, paddingTop: 6,
@ -68,7 +72,9 @@ export default class Message extends React.PureComponent {
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
Message_TimeFormat: PropTypes.string.isRequired Message_TimeFormat: PropTypes.string.isRequired
} }
attachments() {
return this.props.item.attachments.length ? <Card data={this.props.item.attachments[0]} /> : null;
}
render() { render() {
const { item } = this.props; const { item } = this.props;
@ -102,13 +108,14 @@ export default class Message extends React.PureComponent {
<Text style={styles.avatarInitials}>{initials}</Text> <Text style={styles.avatarInitials}>{initials}</Text>
<CachedImage style={styles.avatar} source={{ uri: avatar }} /> <CachedImage style={styles.avatar} source={{ uri: avatar }} />
</View> </View>
<View style={styles.texts}> <View style={[styles.content]}>
<View style={styles.usernameView}> <View style={styles.usernameView}>
<Text onPress={this._onPress} style={styles.username}> <Text onPress={this._onPress} style={styles.username}>
{username} {username}
</Text> </Text>
{aliasUsername}<Text style={styles.time}>{time}</Text> {aliasUsername}<Text style={styles.time}>{time}</Text>
</View> </View>
{this.attachments()}
<Markdown> <Markdown>
{msg} {msg}
</Markdown> </Markdown>

View File

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import { View, TextInput, StyleSheet } from 'react-native'; import { View, TextInput, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import ImagePicker from 'react-native-image-picker'; import ImagePicker from 'react-native-image-picker';
import RNFetchBlob from 'react-native-fetch-blob';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -63,7 +62,7 @@ export default class MessageBox extends React.PureComponent {
}; };
ImagePicker.showImagePicker(options, (response) => { ImagePicker.showImagePicker(options, (response) => {
console.log('Response = ', response); // console.log('Response = ', response);
if (response.didCancel) { if (response.didCancel) {
console.log('User cancelled image picker'); console.log('User cancelled image picker');
@ -76,29 +75,10 @@ export default class MessageBox extends React.PureComponent {
name: response.fileName, name: response.fileName,
size: response.fileSize, size: response.fileSize,
type: response.type || 'image/jpeg', type: response.type || 'image/jpeg',
rid: this.props.rid,
// description: '', // description: '',
store: 'Uploads' store: 'Uploads'
}; };
RocketChat.sendFileMessage(this.props.rid, fileInfo, response.data);
RocketChat.ufsCreate(fileInfo).then((result) => {
RNFetchBlob.fetch('POST', result.url, {
'Content-Type': 'application/octet-stream'
}, response.data).then(() => {
RocketChat.ufsComplete(result.fileId, fileInfo.store, result.token)
.then((completeRresult) => {
RocketChat.sendFileMessage(completeRresult.rid, {
_id: completeRresult._id,
type: completeRresult.type,
size: completeRresult.size,
name: completeRresult.name,
url: completeRresult.path
});
});
}).catch((err) => {
console.log({ err });
});
});
} }
}); });
} }

View File

@ -0,0 +1,76 @@
import PropTypes from 'prop-types';
import React from 'react';
import Meteor from 'react-native-meteor';
import { CachedImage } from 'react-native-img-cache';
import { Text, TouchableOpacity } from 'react-native';
import { Navigation } from 'react-native-navigation';
import {
Card,
CardImage,
// CardTitle,
CardContent
// CardAction
} from 'react-native-card-view';
import RocketChat from '../../lib/rocketchat';
const close = () => Navigation.dismissModal({
animationType: 'slide-down'
});
const CustomButton = ({ text }) => (
<TouchableOpacity onPress={close}>
<Text style={{ color: 'blue' }}>{text}</Text>
</TouchableOpacity>
);
Navigation.registerComponent('CustomButton', () => CustomButton);
export default class Cards extends React.PureComponent {
static propTypes = {
data: PropTypes.object.isRequired
}
constructor() {
super();
const user = Meteor.user();
this.state = {};
RocketChat.getUserToken().then((token) => {
this.setState({ img: `${ RocketChat.currentServer }${ this.props.data.image_url }?rc_uid=${ user._id }&rc_token=${ token }` });
});
}
_onPressButton() {
Navigation.showModal({
screen: 'Photo',
title: this.props.data.title, // title of the screen as appears in the nav bar (optional)
passProps: { image: this.state.img },
// navigatorStyle: {}, // override the navigator style for the screen, see "Styling the navigator" below (optional)
navigatorButtons: {
leftButtons: [{
id: 'custom-button',
component: 'CustomButton',
passProps: {
text: 'close'
}
}]
}, // override the nav buttons for the screen, see "Adding buttons to the navigator" below (optional)
animationType: 'slide-up' // 'none' / 'slide-up' , appear animation for the modal (optional, default 'slide-up')
});
}
render() {
return this.state.img ? (
<TouchableOpacity onPress={() => this._onPressButton()}>
<Card>
<CardImage style={{ width: 256, height: 256 }}>
<CachedImage
style={{ width: 256, height: 256 }}
source={{ uri: encodeURI(this.state.img) }}
/>
</CardImage>
<CardContent>
<Text style={[{ fontSize: 12, alignSelf: 'center', fontStyle: 'italic' }]}>{this.props.data.title}</Text>
<Text style={{ alignSelf: 'center', fontWeight: 'bold' }}>{this.props.data.description}</Text>
</CardContent>
</Card>
</TouchableOpacity>
) :
<Text style={[{ fontSize: 12, alignSelf: 'center', fontStyle: 'italic' }]}>{this.props.data.title}</Text>;
}
}

View File

@ -55,6 +55,26 @@ const usersSchema = {
} }
}; };
const attachment = {
name: 'attachment',
properties: {
description: { type: 'string', optional: true },
image_size: { type: 'int', optional: true },
image_type: { type: 'string', optional: true },
image_url: { type: 'string', optional: true },
title: { type: 'string', optional: true },
title_link: { type: 'string', optional: true },
title_link_download: { type: 'bool', optional: true },
type: { type: 'string', optional: true }
}
};
const messagesSchema = { const messagesSchema = {
name: 'messages', name: 'messages',
primaryKey: '_id', primaryKey: '_id',
@ -71,6 +91,7 @@ const messagesSchema = {
parseUrls: { type: 'bool', optional: true }, parseUrls: { type: 'bool', optional: true },
groupable: { type: 'bool', optional: true }, groupable: { type: 'bool', optional: true },
avatar: { type: 'string', optional: true }, avatar: { type: 'string', optional: true },
attachments: { type: 'list', objectType: 'attachment' },
_updatedAt: { type: 'date', optional: true }, _updatedAt: { type: 'date', optional: true },
temp: { type: 'bool', optional: true } temp: { type: 'bool', optional: true }
} }
@ -136,9 +157,8 @@ const messagesSchema = {
// Realm.clearTestState(); // Realm.clearTestState();
const realm = new Realm({ const realm = new Realm({
schema: [settingsSchema, serversSchema, subscriptionSchema, messagesSchema, usersSchema] schema: [settingsSchema, serversSchema, subscriptionSchema, messagesSchema, usersSchema, attachment]
}); });
export default realm; export default realm;
// realm.write(() => { // realm.write(() => {

View File

@ -3,11 +3,13 @@ import Random from 'react-native-meteor/lib/Random';
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { hashPassword } from 'react-native-meteor/lib/utils'; import { hashPassword } from 'react-native-meteor/lib/utils';
import RNFetchBlob from 'react-native-fetch-blob';
import reduxStore from '../lib/createStore'; import reduxStore from '../lib/createStore';
import settingsType from '../constants/settings'; import settingsType from '../constants/settings';
import realm from './realm'; import realm from './realm';
import * as actions from '../actions'; import * as actions from '../actions';
export { Accounts } from 'react-native-meteor'; export { Accounts } from 'react-native-meteor';
const call = (method, ...params) => new Promise((resolve, reject) => { const call = (method, ...params) => new Promise((resolve, reject) => {
@ -220,47 +222,32 @@ const RocketChat = {
Meteor.subscribe('stream-room-messages', rid, false); Meteor.subscribe('stream-room-messages', rid, false);
}, },
sendMessage(rid, msg) { getMessage(rid, msg = {}) {
const _id = Random.id(); const _id = Random.id();
const user = Meteor.user(); const user = Meteor.user();
const message = {
_id,
rid,
msg,
ts: new Date(),
_updatedAt: new Date(),
temp: true,
_server: { id: RocketChat.currentServer },
u: {
_id: user._id,
username: user.username
}
};
realm.write(() => { realm.write(() => {
// write('messages', { realm.create('messages', message, true);
// _id, // write('messages', message, true);
// rid,
// msg,
// ts: new Date(),
// _updatedAt: new Date(),
// temp: true,
// _server: { id: RocketChat.currentServer },
// u: {
// _id: user._id,
// username: user.username
// }
// });
realm.create('messages', {
_id,
rid,
msg,
ts: new Date(),
_updatedAt: new Date(),
temp: true,
_server: { id: RocketChat.currentServer },
u: {
_id: user._id,
username: user.username
}
}, true);
});
return new Promise((resolve, reject) => {
Meteor.call('sendMessage', { _id, rid, msg }, (error, result) => {
if (error) {
return reject(error);
}
return resolve(result);
});
}); });
return message;
},
sendMessage(rid, msg) {
const tempMessage = this.getMessage(rid, msg);
return call('sendMessage', { _id: tempMessage._id, rid, msg });
}, },
spotlight(search, usernames) { spotlight(search, usernames) {
@ -307,7 +294,8 @@ const RocketChat = {
"description":"" "description":""
"store":"fileSystem" "store":"fileSystem"
*/ */
ufsCreate(fileInfo) { _ufsCreate(fileInfo) {
// return call('ufsCreate', fileInfo);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Meteor.call('ufsCreate', fileInfo, (error, result) => { Meteor.call('ufsCreate', fileInfo, (error, result) => {
if (error) { if (error) {
@ -319,7 +307,7 @@ const RocketChat = {
}, },
// ["ZTE8CKHJt7LATv7Me","fileSystem","e8E96b2819" // ["ZTE8CKHJt7LATv7Me","fileSystem","e8E96b2819"
ufsComplete(fileId, store, token) { _ufsComplete(fileId, store, token) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Meteor.call('ufsComplete', fileId, store, token, (error, result) => { Meteor.call('ufsComplete', fileId, store, token, (error, result) => {
if (error) { if (error) {
@ -340,9 +328,9 @@ const RocketChat = {
"url":"/ufs/fileSystem/ZTE8CKHJt7LATv7Me/yXfExLErmNR5eNPx7.png" "url":"/ufs/fileSystem/ZTE8CKHJt7LATv7Me/yXfExLErmNR5eNPx7.png"
} }
*/ */
sendFileMessage(rid, message) { _sendFileMessage(rid, data, msg = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Meteor.call('sendFileMessage', rid, null, message, (error, result) => { Meteor.call('sendFileMessage', rid, null, data, msg, (error, result) => {
if (error) { if (error) {
return reject(error); return reject(error);
} }
@ -350,6 +338,33 @@ const RocketChat = {
}); });
}); });
}, },
async sendFileMessage(rid, fileInfo, data) {
const placeholder = RocketChat.getMessage(rid, 'Sending an image');
try {
const result = await RocketChat._ufsCreate({ ...fileInfo, rid });
await RNFetchBlob.fetch('POST', result.url, {
'Content-Type': 'application/octet-stream'
}, data);
const completeRresult = await RocketChat._ufsComplete(result.fileId, fileInfo.store, result.token);
return await RocketChat._sendFileMessage(completeRresult.rid, {
_id: completeRresult._id,
type: completeRresult.type,
size: completeRresult.size,
name: completeRresult.name,
url: completeRresult.path
});
} catch (e) {
return e;
} finally {
realm.write(() => {
const msg = realm.objects('messages').filtered('_id = $0', placeholder._id);
realm.delete(msg);
});
}
},
logout() { logout() {
return AsyncStorage.clear(); return AsyncStorage.clear();

View File

@ -6,11 +6,13 @@ import NewServerView from './views/serverNew';
import ListServerView from './views/serverList'; import ListServerView from './views/serverList';
import RoomsListView from './views/roomsList'; import RoomsListView from './views/roomsList';
import RoomView from './views/room'; import RoomView from './views/room';
import PhotoView from './views/Photo';
import CreateChannel from './views/CreateChannel'; import CreateChannel from './views/CreateChannel';
import store from './lib/createStore'; import store from './lib/createStore';
Navigation.registerComponent('Rooms', () => RoomsListView, store, Provider); Navigation.registerComponent('Rooms', () => RoomsListView, store, Provider);
Navigation.registerComponent('Room', () => RoomView, store, Provider); Navigation.registerComponent('Room', () => RoomView, store, Provider);
Navigation.registerComponent('Photo', () => PhotoView, store, Provider);
Navigation.registerComponent('ListServer', () => ListServerView, store, Provider); Navigation.registerComponent('ListServer', () => ListServerView, store, Provider);
Navigation.registerComponent('Login', () => LoginView, store, Provider); Navigation.registerComponent('Login', () => LoginView, store, Provider);
Navigation.registerComponent('NewServer', () => NewServerView, store, Provider); Navigation.registerComponent('NewServer', () => NewServerView, store, Provider);

36
app/views/Photo.js Normal file
View File

@ -0,0 +1,36 @@
import React from 'react';
import { ScrollView, View } from 'react-native';
import { CachedImage } from 'react-native-img-cache';
import PropTypes from 'prop-types';
const styles = {
imageWrapper: {
flex: 1,
alignItems: 'stretch',
backgroundColor: '#000'
},
image: {
flex: 1
}
};
export default class extends React.PureComponent {
static propTypes = {
image: PropTypes.string.isRequired
}
render() {
const { image } = this.props;
return (
<View style={styles.imageWrapper}>
<ScrollView contentContainerStyle={styles.imageWrapper} maximumZoomScale={1.5}>
<CachedImage
style={{ ...styles.image }}
source={{ uri: encodeURI(image) }}
mutable
resizeMode={'contain'}
/>
</ScrollView>
</View>
);
}
}

View File

@ -209,6 +209,7 @@ export default class RoomView extends React.Component {
onEndReached={this.onEndReached} onEndReached={this.onEndReached}
dataSource={this.state.dataSource} dataSource={this.state.dataSource}
renderRow={item => this.renderItem({ item })} renderRow={item => this.renderItem({ item })}
initialListSize={10}
/> />
{this.renderFooter()} {this.renderFooter()}
</KeyboardView> </KeyboardView>

View File

@ -174,53 +174,56 @@ export default class RoomsListView extends React.Component {
searchText: text, searchText: text,
searching: searchText !== '' searching: searchText !== ''
}); });
if (searchText === '') {
if (searchText !== '') { return this.setState({
const data = this.state.data.filtered('name CONTAINS[c] $0', searchText).slice();
const dataSource = [];
const usernames = [];
data.forEach((sub) => {
dataSource.push(sub);
if (sub.t === 'd') {
usernames.push(sub.name);
}
});
if (dataSource.length < 7) {
RocketChat.spotlight(searchText, usernames)
.then((results) => {
results.users.forEach((user) => {
dataSource.push({
...user,
name: user.username,
t: 'd',
search: true
});
});
results.rooms.forEach((room) => {
dataSource.push({
...room,
search: true
});
});
this.setState({
dataSource: ds.cloneWithRows(dataSource)
});
});
} else {
this.setState({
dataSource: ds.cloneWithRows(dataSource)
});
}
} else {
this.setState({
dataSource: ds.cloneWithRows(this.state.data) dataSource: ds.cloneWithRows(this.state.data)
}); });
} }
const data = this.state.data.filtered('name CONTAINS[c] $0', searchText).slice();
const usernames = [];
const dataSource = data.map((sub) => {
if (sub.t === 'd') {
usernames.push(sub.name);
}
return sub;
});
if (dataSource.length < 7) {
if (this.oldPromise) {
this.oldPromise();
}
Promise.race([
RocketChat.spotlight(searchText, usernames),
new Promise((resolve, reject) => this.oldPromise = reject)
])
.then((results) => {
results.users.forEach((user) => {
dataSource.push({
...user,
name: user.username,
t: 'd',
search: true
});
});
results.rooms.forEach((room) => {
dataSource.push({
...room,
search: true
});
});
this.setState({
dataSource: ds.cloneWithRows(dataSource)
});
}, () => console.log('spotlight stopped'))
.then(() => delete this.oldPromise);
}
this.setState({
dataSource: ds.cloneWithRows(dataSource)
});
} }
setInitialData = (props = this.props) => { setInitialData = (props = this.props) => {

View File

@ -199,9 +199,7 @@ export default class NewServerView extends React.Component {
} }
} }
url = url.replace(/\/+$/, ''); return url.replace(/\/+$/, '');
return url;
} }
renderValidation = () => { renderValidation = () => {

3480
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@
"react-native-auto-grow-textinput": "^1.2.0", "react-native-auto-grow-textinput": "^1.2.0",
"react-native-autogrow-input": "^0.2.1", "react-native-autogrow-input": "^0.2.1",
"react-native-autogrow-textinput": "^4.1.0", "react-native-autogrow-textinput": "^4.1.0",
"react-native-card-view": "0.0.3",
"react-native-console-time-polyfill": "0.0.6", "react-native-console-time-polyfill": "0.0.6",
"react-native-easy-markdown": "git+https://github.com/lappalj4/react-native-easy-markdown.git", "react-native-easy-markdown": "git+https://github.com/lappalj4/react-native-easy-markdown.git",
"react-native-fetch-blob": "^0.10.8", "react-native-fetch-blob": "^0.10.8",