DiscoverPlaces Raport #10 – dodawanie wiadomości, wybór zdjęcia oraz wideo

Pora na kolejny raport odnośnie tworzenia projektu DiscoverPlaces, w których  przedstawione zostanie tworzenie komponentu MessageCreator do tworzenia nowych wiadomości oraz komponentów pomocniczych do załączania zdjęć oraz wideo. Opisane zostaną również problemy wynikłe w czasie pracy.

Dodawanie wiadomości ze zdjęciem lub filmem

Według założeń projektu zarówno w wiadomości jak i w komentarzu ma być możliwość załączenia zdjęcia lub wideo. Dlatego w myśl zasady DRY wygodnie będzie utworzyć dwa osobne komponenty, które następnie będzie można wykorzystać podczas tworzenia wiadomości oraz komentarza.

Wspomogłem się tutaj paczką React Native Image Picker. Jej instalacja jest bardzo prosta, jedyne co trzeba zrobić ręcznie to wstawić odpowiednie uprawnienia, ale dokumentacja doskonale to opisuje. Poniżej wstawione zostały dwa komponenty odpowiedzialne za wybór zdjęcia oraz wideo:

import React, { Component } from 'react';
import { Button } from 'react-native-elements';
import ImagePicker from 'react-native-image-picker';

export default class PhotoPicker extends Component
{
  constructor(props) {
    super(props);
    this.state = {
      onPick: props.onPick
    };
  }

  selectPhoto() {
    const options = {
      quality: 1.0,
      storageOptions: {
        skipBackup: true
      }
    };

    ImagePicker.showImagePicker(options, (response) => {
      console.log('Response = ', response);

      if (response.didCancel) {
        console.log('User cancelled photo picker');
      }
      else if (response.error) {
        console.log('ImagePicker Error: ', response.error);
      }
      else if (response.customButton) {
        console.log('User tapped custom button: ', response.customButton);
      }
      else {
        const source = {
          uri: response.uri,
          name: response.fileName,
          type: response.type
        };

        this.props.onPick('photo', source);
      }
    });
  }

  render () {
    return (
      <Button
        buttonStyle={{marginTop: 10, marginBottom: 5}}
        title="Add photo"
        icon={{name: 'camera', type: 'font-awesome'}}
        backgroundColor="blue"
        onPress={() => this.selectPhoto()}
      />
    );
  }
}

 

import React, { Component } from 'react';
import { Button } from 'react-native-elements';
import ImagePicker from 'react-native-image-picker';

export default class PhotoPicker extends Component
{
  constructor(props) {
    super(props);
    this.state = {
      onPick: props.onPick
    };
  }

  selectVideo () {
    const options = {
      title: 'Video Picker',
      takePhotoButtonTitle: 'Take Video...',
      mediaType: 'video',
      videoQuality: 'medium'
    };

    ImagePicker.showImagePicker(options, (response) => {
      console.log('Response = ', response);

      if (response.didCancel) {
        console.log('User cancelled video picker');
      }
      else if (response.error) {
        console.log('ImagePicker Error: ', response.error);
      }
      else if (response.customButton) {
        console.log('User tapped custom button: ', response.customButton);
      }
      else {
        const source = {
          uri: response.uri,
          name: 'video.mp4',
          type: 'video/mp4'
        };

        this.props.onPick('video', source);
      }
    });
  }

  render () {
    return (
      <Button
        buttonStyle={{marginTop: 5, marginBottom: 10}}
        title="Add video"
        icon={{name: 'video-camera', type: 'font-awesome'}}
        backgroundColor="brown"
        onPress={() => this.selectVideo()}
      />
    );
  }
}

Komponent MessageCreator wygląda w ten sposób:

import React, { Component } from 'react';
import { View, Picker, Text } from 'react-native';
import { FormLabel, FormInput, Button } from 'react-native-elements';
import { createMessage } from './../../config/api';
import update from 'immutability-helper';
import PhotoPicker from './../Utils/PhotoPicker';
import VideoPicker from './../Utils/VideoPicker';

const AVAILABLE_SCOPES  = [1,2,5];
const HTTP_CREATED = 201;

export default class Creator extends Component
{
  constructor (props) {
    super(props);

    this.state = {
      valid: false,
      message: {
        content: null,
        photo: null,
        video: null,
        latitude: 0,
        longitude: 0,
        scope: 1
      }
    };
  }

  updateField (property, value) {
    if (!this.state.message.hasOwnProperty(property)) {
      return;
    }

    const message = update(this.state.message, {[property]: {
      $set: value
    }});

    this.setState({
      message: message
    });
    this.validate();
  }

  handlePick (target, source) {
    const message = update(this.state.message, {[target]: {
      $set: source
    }});

    this.setState({
      message: message
    });
    this.validate();
  }

  submit () {
    this.validate();
    if (!this.state.valid) {
      return false;
    }

    navigator.geolocation.getCurrentPosition(position => {
      let message = this.state.message;
      message.latitude = position.coords.latitude;
      message.longitude = position.coords.longitude;

      createMessage(message)
        .then(response =>  {
          if (response.status === HTTP_CREATED) {
            this.props.navigation.navigate('MessageList');
          }
        });
    });
  }

  validate () {
    if (
      (
        (this.state.message.content && this.state.message.content !== '')
        || this.state.message.photo
        || this.state.message.video
      )
      && AVAILABLE_SCOPES.includes(this.state.message.scope)
    ) {
      this.setState({valid: true});
    } else {
      this.setState({valid: false});
    }
  }

  render () {
    return (
      <View>
        {!this.state.valid &&
          <Text
            style={{
              color: 'red',
              marginTop: 10,
              marginBottom: 10,
              textAlign: 'center'
            }}
          >
              You have to add content, photo or video
            </Text>
        }
        <FormLabel>Content</FormLabel>
        <FormInput
          onChangeText={(content) => this.updateField('content', content)}
          value={this.state.message.content}
        />
        <PhotoPicker
          onPick={this.handlePick.bind(this)}
        />
        <VideoPicker
          onPick={this.handlePick.bind(this)}
        />
        <Picker
          selectedValue={this.state.message.scope.toString()}
          onValueChange={(scope) => this.updateField('scope', parseInt(scope))}
        >
          <Picker.Item label="1km" value="1" />
          <Picker.Item label="2km" value="2" />
          <Picker.Item label="5km" value="5" />
        </Picker>
        <Button
          buttonStyle={{marginTop: 10, marginBottom: 10}}
          title="Add"
          icon={{name: 'plus', type: 'font-awesome'}}
          backgroundColor="green"
          large={true}
          onPress={this.submit.bind(this)}
        />
      </View>
    );
  }
}

Całość kodu można znaleźć na githubie.

Napotkane problemy

React Native Image Picker

Pierwszym problemem, na który się napotkałem to niedziałające nagrywanie wideo na Androidzie 7.0, winą obarczam paczkę, której używam. Póki co przerzuciłem się na testowanie na emulatorze z androidem 6.0, później pomyślę jak to rozwiązać.

Eslint

Eslint zgłasza błędy w przypadku wykorzystywania niezdefiniowanych zmiennych, niestety nie wykrywa takich elementów jak FormData czy navigator z React Native, więc zaktualizowałem config dodając:

"globals": {
      "FormData": false,
      "navigator": false
}

 React Native – Network request failed

Gdy przesyłamy pliki należy dodać również name oraz type, w przypadku obiektu wyglądającego w ten sposób:

let source = {uri: response.uri};

React Native wyrzuca błąd „Network request failed”.  Powinno to wyglądać następująco:

const source = {
 uri: response.uri,
 name: response.fileName,
 type: response.type
};

 Symfony – Unable to find template

Ten problem był o tyle ciekawy, że gdy przesyłałem dane jako form-data przez Postmana, wszystko działało jak należy, natomiast z poziomu kodu przesyłając FormData, Symfony wyrzuca warning Unable to find template, tak jakby chciało odpowiedzieć htmlem poprzez twiga. Na pierwszy rzut oka Requesty z Postmana i kodu niczym się nie różniły. Nie sprawdzałem jak wyglądają Requesty docierające do serwera, możliwe, że Postman dodaje sam jakieś nagłówki i tego nie wyświetla. Sprawę rozwiązałem dodając do kodu nagłówek 'Accept’: 'application/json’.

Devtools

Dla React Native powinny działać te same narzędzia co dla samego Reacta, czyli rozszerzenie do chrome oraz zewnętrzne dev-tools. Jednak w moim przypadku w przeglądarce jedyne co mogę to przechwycić console.log(), natomiast gdy przełączę na osobne dev-tools, to co prawda pokazuje mi cały Virtual Dom, aczkolwiek nie przechwytuje console.log(). Póki co szukałem jedynie na szybko jakichś informacji, nie miałem czasu na dokładniejsze sprawdzanie. Postaram się to jakoś rozwiązać i pewnie w kolejnym wpisie dam znać czy się udało.

Udostępnij: