diff --git a/mobile/src/API.js b/mobile/src/API.js
index 555f108..a3b06f0 100644
--- a/mobile/src/API.js
+++ b/mobile/src/API.js
@@ -137,7 +137,6 @@ class API extends EventEmitter {
this._apiURL = url
this._baseURL = parts[0] + "//" + parts[1]
- this._secure = parts[0] === "https:"
if (parts.length === 3) {
this._apiPath = "/" + parts[2]
@@ -155,8 +154,8 @@ class API extends EventEmitter {
return this._apiPath
}
- get secure() {
- return this._secure
+ get apiURL() {
+ return this._apiURL
}
get backend() {
@@ -178,21 +177,13 @@ class API extends EventEmitter {
}
}
- makeImageUrl(id, size) {
- if (id) {
- return this.apiPath + "/assets/" + id + "?access_token=" + this.token
- } else if (size && size.width && size.height) {
- return `${this.apiPath}/placeholders/${size.width}x${
- size.height
- }?access_token=${this.token}`
- } else {
- return null
- }
+ makeImageUrl(id) {
+ return this._apiURL + "/assets/" + id + ".jpg?access_token=" + this.token
}
makeAssetUrl(id) {
return id
- ? this.apiPath + "/assets/" + id + "?access_token=" + this.token
+ ? this._apiURL + "/assets/" + id + "?access_token=" + this.token
: null
}
@@ -218,10 +209,17 @@ class API extends EventEmitter {
headers.set("Authorization", "Bearer " + this.token)
}
if (method === "POST" || method === "PUT") {
- if (requestOptions.binary) {
- headers.set("Content-Type", "application/octet-stream")
- headers.set("Content-Length", requestOptions.binary.length)
- headers.set("Range", "byte " + requestOptions.binary.offset)
+ if (requestOptions.raw) {
+ const isBase64 = requestOptions.raw.base64
+ headers.set(
+ "Content-Type",
+ isBase64 ? "application/base64" : "application/octet-stream"
+ )
+ headers.set("Content-Length", requestOptions.raw.length)
+ headers.set(
+ "Content-Range",
+ (isBase64 ? "base64" : "byte") + " " + requestOptions.raw.offset
+ )
fetchOptions.body = requestBody
} else {
headers.set("Content-Type", "application/json")
@@ -238,7 +236,7 @@ class API extends EventEmitter {
.then((res) => {
return Promise.all([
Promise.resolve(res),
- requestOptions.binary && method === "GET" ? res.blob() : res.json(),
+ requestOptions.raw && method === "GET" ? res.blob() : res.json(),
])
})
.then((arr) => {
@@ -427,33 +425,29 @@ class API extends EventEmitter {
return promise
}
- upload(file, progressCallback) {
+ upload(data, progressCallback) {
return new Promise((resolve, reject) => {
const chunkSize = 32 * 1024
- let reader = new FileReader()
- const fileSize = file.size
- const numberOfChunks = Math.ceil(fileSize / chunkSize)
+ const uploadSize = data.length
+ const numberOfChunks = Math.ceil(uploadSize / chunkSize)
let chunk = 0
let uploadId = null
- reader.onload = (e) => {
- const buffer = e.target.result
- const bytesRead = buffer.byteLength
+ const uploadNextChunk = () => {
+ const start = chunk * chunkSize
+ const end = Math.min(uploadSize, start + chunkSize)
- this.post("/assets/upload/" + uploadId, buffer, {
- binary: { offset: chunk * chunkSize, length: bytesRead },
+ this.post("/assets/upload/" + uploadId, data.slice(start, end), {
+ raw: { base64: true, length: chunkSize, offset: start },
})
.then((uploadData) => {
chunk++
- if (!progressCallback(uploadData)) {
- return Promise.reject(new Error("Upload was canceled"))
- }
- if (chunk < numberOfChunks) {
- let start = chunk * chunkSize
- let end = Math.min(fileSize, start + chunkSize)
- reader.readAsArrayBuffer(file.slice(start, end))
- } else {
+ if (progressCallback && !progressCallback(uploadData)) {
+ reject(new Error("Upload was canceled"))
+ } else if (chunk >= numberOfChunks) {
resolve(uploadData)
+ } else {
+ uploadNextChunk()
}
})
.catch((err) => {
@@ -462,14 +456,14 @@ class API extends EventEmitter {
}
this.post("/assets/upload", {
- fileName: file.name,
- fileSize,
- contentType: file.type,
+ uploadSize,
+ contentType: "image/jpeg",
+ chunkContentType: "application/base64",
numberOfChunks,
})
.then((uploadData) => {
uploadId = uploadData.uploadId
- reader.readAsArrayBuffer(file.slice(0, chunkSize))
+ uploadNextChunk()
})
.catch((err) => {
reject(err)
diff --git a/mobile/src/Activity/Activity.js b/mobile/src/Activity/Activity.js
index 7f209a5..ef27acd 100644
--- a/mobile/src/Activity/Activity.js
+++ b/mobile/src/Activity/Activity.js
@@ -19,9 +19,9 @@ import {
BoundButton,
BoundOptionStrip,
BoundHeader,
- PhotoPanel,
+ BoundPhotoPanel,
} from "../ui"
-import { MessageModal } from "../Modal"
+import { MessageModal, WaitModal } from "../Modal"
import autobind from "autobind-decorator"
import KeyboardSpacer from "react-native-keyboard-spacer"
import { isIphoneX } from "react-native-iphone-x-helper"
@@ -70,6 +70,9 @@ export class Activity extends React.Component {
notes: {
isValid: (r, v) => v !== "",
},
+ photos: {
+ isValid: (r, v) => v && v.length > 0,
+ },
status: {
isValid: (r, v) => v !== "",
alwaysGet: true,
@@ -84,6 +87,7 @@ export class Activity extends React.Component {
super(props)
this.state = {
binder: new FormBinder({}, Activity.bindings),
+ waitModal: null,
messageModal: null,
}
@@ -180,8 +184,18 @@ export class Activity extends React.Component {
}
}
+ @autobind
+ handleUploadStarted() {
+ this.setState({ waitModal: { message: "Uploading Photo..." } })
+ }
+
+ @autobind
+ handleUploadEnded() {
+ this.setState({ waitModal: null })
+ }
+
render() {
- const { binder, messageModal, region } = this.state
+ const { binder, messageModal, waitModal, region } = this.state
return (
@@ -258,10 +272,19 @@ export class Activity extends React.Component {
-
+
{isIphoneX ? : null}
+
{
// NOTE: When working on the app, change this to the page you are working on
- return } />
+ return } />
}
diff --git a/mobile/src/Home/Home.js b/mobile/src/Home/Home.js
index ba1bd74..53f03c5 100644
--- a/mobile/src/Home/Home.js
+++ b/mobile/src/Home/Home.js
@@ -52,6 +52,8 @@ export class Home extends React.Component {
workItemDistance: -1,
}
+ this.watchId = null
+
ensurePermissions(
[
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
@@ -173,8 +175,20 @@ export class Home extends React.Component {
}
@autobind
- handleItemSelect(item) {
- this.props.history.push(`/activity?id=${item._id}`)
+ handleItemSelect(activity) {
+ this.props.history.push(`/activity?id=${activity._id}`)
+ }
+
+ @autobind
+ handleSectionSelect(workItem) {
+ const { latitude, longitude } = workItem.coordinate
+ const region = {
+ latitude,
+ longitude,
+ latitudeDelta: 0.01,
+ longitudeDelta: 0.01,
+ }
+ this.setState({ region })
}
@autobind
@@ -276,6 +290,7 @@ export class Home extends React.Component {
showsTraffic={false}
showsIndoors={false}
zoomControlEnabled={false}
+ showsMyLocationButton={false}
region={region}>
{sections.map((workItem, index) => (
(
-
-
-
- {workItemTypeText[workItem.workItemType].toUpperCase()}{" "}
- {pad(workItem.ticketNumber, 4)}
-
-
+ backgroundColor: "#F4F4F4",
+ }}
+ underlayColor="#EEEEEE"
+ onPress={() => this.handleSectionSelect(workItem)}>
+
+
+
+ {workItemTypeText[workItem.workItemType].toUpperCase()}{" "}
+ {pad(workItem.ticketNumber, 4)}
+
+
+
)}
keyExtractor={(item) => item._id}
renderItem={({ item: activity, section }) => {
diff --git a/mobile/src/WorkItem/WorkItem.js b/mobile/src/WorkItem/WorkItem.js
index c84f497..3a8da55 100644
--- a/mobile/src/WorkItem/WorkItem.js
+++ b/mobile/src/WorkItem/WorkItem.js
@@ -18,11 +18,10 @@ import {
BoundHeader,
Icon,
Header,
- PhotoButton,
BoundOptionStrip,
- PhotoPanel,
+ BoundPhotoPanel,
} from "../ui"
-import { MessageModal } from "../Modal"
+import { MessageModal, WaitModal } from "../Modal"
import autobind from "autobind-decorator"
import { ifIphoneX, isIphoneX } from "react-native-iphone-x-helper"
import KeyboardSpacer from "react-native-keyboard-spacer"
@@ -65,6 +64,9 @@ export class WorkItem extends React.Component {
isValid: true,
isReadOnly: true,
},
+ photos: {
+ isValid: (r, v) => v && v.length > 0,
+ },
details: {
isValid: (r, v) => v !== "",
},
@@ -87,6 +89,7 @@ export class WorkItem extends React.Component {
this.state = {
binder: new FormBinder({}, WorkItem.bindings),
messageModal: null,
+ waitModal: null,
region,
}
@@ -230,8 +233,18 @@ export class WorkItem extends React.Component {
})
}
+ @autobind
+ handleUploadStarted() {
+ this.setState({ waitModal: { message: "Uploading Photo..." } })
+ }
+
+ @autobind
+ handleUploadEnded() {
+ this.setState({ waitModal: null })
+ }
+
render() {
- const { binder, messageModal, region } = this.state
+ const { binder, messageModal, waitModal, region } = this.state
return (
@@ -304,10 +317,19 @@ export class WorkItem extends React.Component {
/>
-
+
{isIphoneX ? : null}
+
{
+ const { width, height } = Dimensions.get("window")
+
+ return {
+ screenWidth: Math.min(width, height),
+ screenHeight: Math.max(width, height),
+ }
+}
+
+export class BoundPhotoPanel extends Component {
+ static propTypes = {
+ onUploadStarted: PropTypes.func,
+ onUploadEnded: PropTypes.func,
+ }
+
+ constructor(props) {
+ super(props)
+
+ const { name, binder } = this.props
+
+ this.state = binder.getFieldState(name)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.binder !== this.props.binder) {
+ this.setState(nextProps.binder.getFieldState(nextProps.name))
+ }
+ }
+
+ @autobind
+ handlePhotoPress(index) {
+ const { onUploadStarted, onUploadEnded } = this.props
+
+ ImagePicker.showImagePicker(
+ {
+ title: "Select Photo",
+ storageOptions: {
+ skipBackup: true,
+ path: "photos",
+ },
+ },
+ (response) => {
+ if (!response.didCancel && !response.error) {
+ if (onUploadStarted) {
+ onUploadStarted()
+ }
+ api
+ .upload(response.data)
+ .then((uploadData) => {
+ if (onUploadEnded) {
+ onUploadEnded(true, uploadData)
+ }
+
+ const { binder, name } = this.props
+
+ if (binder) {
+ const value = binder.getFieldValue(name)
+ let newValue = value.slice(0)
+
+ newValue[index] = uploadData.assetId
+
+ this.setState(binder.updateFieldValue(name, newValue))
+ }
+ })
+ .catch((err) => {
+ if (onUploadEnded) {
+ onUploadEnded(false)
+ }
+ })
+ }
+ }
+ )
+ }
+
+ render() {
+ const { screenWidth, screenHeight } = getScreenPortraitDimensions()
+ const photoWidth = screenHeight / 4
+ const photoHeight = screenWidth / 4
+ const rowPadding = 10
+ const { value: assetIds } = this.state
+
+ const renderPhoto = (index) => {
+ const assetId = assetIds[index]
+
+ if (assetId) {
+ console.log(api.makeImageUrl(assetId))
+ }
+
+ return (
+ this.handlePhotoPress(index)}>
+ {!assetId && (
+
+ )}
+ {assetId && (
+
+ )}
+
+ )
+ }
+
+ const extraRowStyle = {
+ height: photoHeight + rowPadding,
+ paddingTop: rowPadding / 2,
+ paddingBottom: rowPadding / 2,
+ }
+
+ return (
+
+
+ Pictures:
+
+
+ {renderPhoto(0)}
+ {renderPhoto(1)}
+
+
+ {renderPhoto(2)}
+ {renderPhoto(3)}
+
+
+ )
+ }
+}
+
+const styles = StyleSheet.create({
+ photoRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ width: "100%",
+ },
+})
diff --git a/mobile/src/ui/PhotoPanel.js b/mobile/src/ui/PhotoPanel.js
deleted file mode 100644
index 3b47cf1..0000000
--- a/mobile/src/ui/PhotoPanel.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import React, { Component } from "react"
-import {
- StyleSheet,
- Text,
- View,
- Image,
- TouchableOpacity,
- Dimensions,
-} from "react-native"
-import { Icon } from "."
-import ImagePicker from "react-native-image-picker"
-import autobind from "autobind-decorator"
-
-const getScreenPortraitDimensions = () => {
- const { width, height } = Dimensions.get("window")
-
- return {
- screenWidth: Math.min(width, height),
- screenHeight: Math.max(width, height),
- }
-}
-
-export class PhotoPanel extends Component {
- @autobind
- handlePhotoPress() {
- ImagePicker.showImagePicker(
- {
- title: "Select Photo",
- storageOptions: {
- skipBackup: true,
- path: "photos",
- },
- },
- (response) => {
- console.log("Response = ", response)
-
- if (response.didCancel) {
- console.log("User cancelled image picker")
- } else if (response.error) {
- console.log("ImagePicker Error: ", response.error)
- } else if (response.customButton) {
- console.log("User tapped custom button: ", response.customButton)
- } else {
- // You can also display the image using data:
- // let source = { uri: 'data:image/jpeg;base64,' + response.data };
- console.log(response)
- }
- }
- )
- }
-
- render() {
- const { screenWidth, screenHeight } = getScreenPortraitDimensions()
- const photoWidth = screenHeight / 4
- const photoHeight = screenWidth / 4
- const numRows = 2
- const numCols = 2
- const rowPadding = 10
-
- return (
-
-
- Pictures:
-
- {Array.from(new Array(numRows), (x, i) => (
-
- {Array.from(new Array(numCols), (y, j) => (
-
-
-
- ))}
-
- ))}
-
- )
- }
-}
diff --git a/mobile/src/ui/index.js b/mobile/src/ui/index.js
index b2865c5..7a2c495 100644
--- a/mobile/src/ui/index.js
+++ b/mobile/src/ui/index.js
@@ -1,9 +1,9 @@
export { Icon } from "./Icon"
export { Header } from "./Header"
-export { PhotoPanel } from "./PhotoPanel"
export { OptionStrip } from "./OptionStrip"
export { BoundSwitch } from "./BoundSwitch"
export { BoundInput } from "./BoundInput"
export { BoundButton } from "./BoundButton"
export { BoundOptionStrip } from "./BoundOptionStrip"
export { BoundHeader } from "./BoundHeader"
+export { BoundPhotoPanel } from "./BoundPhotoPanel"
diff --git a/server/src/api/routes/AssetRoutes.js b/server/src/api/routes/AssetRoutes.js
index 1d03183..1b30792 100644
--- a/server/src/api/routes/AssetRoutes.js
+++ b/server/src/api/routes/AssetRoutes.js
@@ -65,7 +65,14 @@ export class AssetRoutes {
}
async getAsset(req, res, next) {
- const assetId = req.params._id
+ let assetId = req.params._id
+ const extIndex = assetId.indexOf(".")
+
+ if (extIndex !== -1) {
+ // TODO: Should really check the index against the requested extension...
+ assetId = assetId.slice(0, extIndex)
+ }
+
const file = await this.db.gridfs.findOneAsync({ _id: assetId })
if (!file) {
@@ -112,13 +119,17 @@ export class AssetRoutes {
chunkContentType,
} = req.body
- if (!fileName || !uploadSize || !numberOfChunks || !contentType) {
+ if (!uploadSize || !numberOfChunks || !contentType) {
throw createError.BadRequest(
- "Must specify fileName, uploadSize, numberOfChunks, contentType"
+ "Must specify uploadSize, numberOfChunks, contentType"
)
}
- fileName = uploadId + "-" + path.basename(fileName)
+ if (fileName) {
+ fileName = uploadId + "-" + path.basename(fileName)
+ } else {
+ fileName = uploadId
+ }
if (chunkContentType) {
if (
@@ -159,9 +170,11 @@ export class AssetRoutes {
const contentRange = req.get("Content-Range")
const contentLength = req.get("Content-Length")
- console.log(uploadData)
+ if (!uploadData) {
+ throw createError.BadRequest(`Bad upload id ${uploadId}`)
+ }
- if (contentType !== uploadData.chunkContentType) {
+ if (!contentType.startsWith(uploadData.chunkContentType)) {
throw createError.BadRequest(
`Content-Type ${contentType} does not match chunk type ${
uploadData.chunkContentType
@@ -211,7 +224,7 @@ export class AssetRoutes {
}
try {
- const [uploadedChunks] = await Promise.all([
+ const [, uploadedChunks] = await Promise.all([
this.rs.setrangeAsync(uploadDataId, offset, req.body),
this.rs.incrAsync(uploadCountId),
])