From cae19ce8960c683a2261aae7dc1f48acd0b77eeb Mon Sep 17 00:00:00 2001 From: natjms Date: Sun, 30 May 2021 09:50:50 -0300 Subject: [PATCH] Create interface for publishing new photos --- src/App.js | 2 + src/components/pages/profile/settings.js | 14 +- src/components/pages/publish.js | 238 +++++++++++++++++++++++ src/components/posts/post.js | 25 +-- src/interface/rendering.js | 19 ++ src/requests.js | 18 +- 6 files changed, 285 insertions(+), 31 deletions(-) create mode 100644 src/components/pages/publish.js diff --git a/src/App.js b/src/App.js index f5bd15c..4184d8c 100644 --- a/src/App.js +++ b/src/App.js @@ -13,6 +13,7 @@ import ViewCommentsJsx from "src/components/pages/view-comments.js"; import AuthenticateJsx from "src/components/pages/authenticate"; import FeedJsx from "src/components/pages/feed"; +import PublishJsx from "src/components/pages/publish"; import OlderPostsJsx from "src/components/pages/feed/older-posts"; import ProfileJsx, { ViewProfileJsx } from "src/components/pages/profile"; import DiscoverJsx from 'src/components/pages/discover'; @@ -32,6 +33,7 @@ const Stack = createStackNavigator({ Feed: { screen: FeedJsx, }, OlderPosts: { screen: OlderPostsJsx }, Discover: { screen: DiscoverJsx }, + Publish: { screen: PublishJsx }, Direct: { screen: DirectJsx }, Compose: { screen: ComposeJsx }, Conversation: { screen: ConversationJsx }, diff --git a/src/components/pages/profile/settings.js b/src/components/pages/profile/settings.js index 34ea97d..5b53b07 100644 --- a/src/components/pages/profile/settings.js +++ b/src/components/pages/profile/settings.js @@ -90,15 +90,18 @@ const SettingsJsx = (props) => { const _handleChangeProfilePhoto = async () => { await ImagePicker.getCameraRollPermissionsAsync() - const { base64, uri } = await ImagePicker.launchImageLibraryAsync({ + const { uri, type } = await ImagePicker.launchImageLibraryAsync({ allowsEditing: true, aspect: [1, 1], }); + const name = uri.split("/").slice(-1)[0]; + setState({...state, newAvatar: { - base64, uri, + type, + name, }, }); }; @@ -109,11 +112,10 @@ const SettingsJsx = (props) => { note: state.note, locked: state.locked, }; - if (state.newAvatar.base64) { - let blob = fetch(state.newAvatar.base64).then(res => res.blob()); - let filename = uri.split("/")[uri.split("/").length - 1]; - params.avatar = new File([blob], filename); + // In other words, if a picture has been selected... + if (state.newAvatar.name) { + params.avatar = state.newAvatar; } const newProfile = await fetch( diff --git a/src/components/pages/publish.js b/src/components/pages/publish.js new file mode 100644 index 0000000..09a6f18 --- /dev/null +++ b/src/components/pages/publish.js @@ -0,0 +1,238 @@ +import React, { useState, useEffect } from "react"; +import { + Dimensions, + View, + Image, + Text, + TextInput, +} from "react-native"; +import { Ionicons } from '@expo/vector-icons'; + +import { getAutoHeight } from "src/interface/rendering"; +import { ScreenWithTrayJsx } from "src/components/navigation/navigators"; +import { TouchableOpacity } from "react-native-gesture-handler"; + +import * as ImagePicker from 'expo-image-picker'; +import * as Permissions from "expo-permissions"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import * as requests from "src/requests"; + +const PublishJsx = ({ navigation }) => { + const [ state, setState ] = useState({ + loaded: false, + }); + + useEffect(() => { + let instance, accessToken; + + AsyncStorage + .multiGet([ + "@user_instance", + "@user_token", + ]) + .then(([ instancePair, tokenPair ]) => { + instance = instancePair[1]; + accessToken = JSON.parse(tokenPair[1]).access_token; + + return Permissions.askAsync(Permissions.CAMERA_ROLL); + }) + .then(({ granted }) => { + if (granted) { + return ImagePicker.launchImageLibraryAsync({ + allowsEditing: true, + }); + } else { + navigation.goBack(); + } + }) + .then((imageData) => { + if (!imageData.cancelled) { + return imageData; + } else { + navigation.goBack(); + } + }) + .then(({ uri, type, width, height }) => { + const name = uri.split("/").slice(-1)[0]; + const newWidth = SCREEN_WIDTH * (3/4); + + setState({...state, + loaded: true, + instance, + accessToken, + visibility: "public", + image: { + data: { + uri, + type, + name, + }, + width: newWidth, + height: getAutoHeight(width, height, newWidth), + }, + }); + }); + }, []); + + const _handlePublish = async () => { + const mediaAttachment = await requests.publishMediaAttachment( + state.instance, + state.accessToken, + { file: state.image.data } + ); + + console.warn(mediaAttachment); + if(mediaAttachment.type == "unknown") return; + + const params = { + status: state.caption, + media_ids: [mediaAttachment.id], + visibility: state.visibility, + }; + + const newStatus = await requests.publishStatus( + state.instance, + state.accessToken, + params + ); + + console.warn(newStatus); + navigation.navigate("Feed"); + }; + + const Selector = (props) => { + const color = props.active == props.visibility ? "black" : "#888"; + + return ( + setState({ ...state, visibility: props.visibility }) + }> + + + +   + { props.message } + + + + ); + }; + + return ( + <> + { state.loaded + ? + + + + + setState({ ...state, caption }) + } + style = { [ styles.form.input, { height: 100, } ] } /> + + Visibility + + + + + + + Publish + + + + + : <> + } + + ); +}; + +const SCREEN_WIDTH = Dimensions.get("window").width; +const styles = { + preview: { + container: { + paddingTop: 10, + }, + image: { + marginLeft: "auto", + marginRight: "auto", + }, + }, + + form: { + container: { + padding: 10, + }, + input: { + borderBottomWidth: 1, + borderBottomColor: "#888", + textAlignVertical: "top", + padding: 10, + }, + label: { + marginTop: 20, + fontSize: 15, + color: "#666", + }, + option: { + button: {}, + inner: { + marginTop: 10, + flexDirection: "row", + alignItems: "center", + }, + }, + button: { + container: { + width: SCREEN_WIDTH * (3/4), + marginTop: 30, + marginLeft: "auto", + marginRight: "auto", + padding: 20, + borderWidth: 1, + borderColor: "#666", + borderRadius: 10, + }, + label: { + textAlign: "center", + }, + }, + }, +}; + +export { PublishJsx as default }; diff --git a/src/components/posts/post.js b/src/components/posts/post.js index 857e517..c463fdc 100644 --- a/src/components/posts/post.js +++ b/src/components/posts/post.js @@ -8,7 +8,11 @@ import { ScrollView, } from "react-native"; -import { pluralize, timeToAge } from "src/interface/rendering" +import { + pluralize, + timeToAge, + getAutoHeight, +} from "src/interface/rendering"; import AsyncStorage from "@react-native-async-storage/async-storage"; import * as requests from "src/requests"; @@ -21,25 +25,6 @@ import ContextMenuJsx from "src/components/context-menu.js"; const SCREEN_WIDTH = Dimensions.get("window").width; const TEST_IMAGE = "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthumbnail.jpg"; -function getAutoHeight(w1, h1, w2) { - /* - Given the original dimensions and the new width, calculate what would - otherwise be the "auto" height of the image. - - Just so that nobody has to ever work out this algebra again: - - Let {w1, h1} = the width and height of the static image, - w2 = the new width, - h2 = the "auto" height of the scaled image of width w2: - - w1/h1 = w2/h2 - h2 * w1/h1 = w2 - h2 = w2 / w1/h1 - h2 = w2 * h1/w1 - */ - return w2 * (h1 / w1) -} - function getDimensionsPromises(uris) { return uris.map(attachment => new Promise(resolve => { Image.getSize(attachment.url, (width, height) => { diff --git a/src/interface/rendering.js b/src/interface/rendering.js index d8f55b2..ceb5a3a 100644 --- a/src/interface/rendering.js +++ b/src/interface/rendering.js @@ -22,6 +22,25 @@ export function pluralize(n, singular, plural) { } } +export function getAutoHeight(w1, h1, w2) { + /* + Given the original dimensions and the new width, calculate what would + otherwise be the "auto" height of the image. + + Just so that nobody has to ever work out this algebra again: + + Let {w1, h1} = the width and height of the static image, + w2 = the new width, + h2 = the "auto" height of the scaled image of width w2: + + w1/h1 = w2/h2 + h2 * w1/h1 = w2 + h2 = w2 / w1/h1 + h2 = w2 * h1/w1 + */ + return w2 * (h1 / w1) +} + export function timeToAge(time1, time2) { /* Output a friendly string to describe the age of a post, where `time1` and diff --git a/src/requests.js b/src/requests.js index 258aea4..7061771 100644 --- a/src/requests.js +++ b/src/requests.js @@ -39,14 +39,17 @@ export async function checkUnreadNotifications() { } } -export async function postForm(url, data = false, token = false) { +export async function postForm(url, data = false, token = false, contentType = false) { // Send a POST request with data formatted with FormData returning JSON + let headers = {}; + + if (token) headers["Authorization"] = `Bearer ${token}`; + if (contentType) headers["Content-Type"] = contentType; + const resp = await fetch(url, { method: "POST", - body: data ? objectToForm(data): {}, - headers: token - ? { "Authorization": `Bearer ${token}`, } - : {}, + body: data ? objectToForm(data) : undefined, + headers, }); return resp; @@ -123,6 +126,11 @@ export async function unblockAccount(domain, id, token) { return resp.json(); } +export async function publishMediaAttachment(domain, token, params) { + const resp = await postForm(`https://${domain}/api/v1/media`, params, token, "multipart/form-data"); + return resp.json(); +} + export async function publishStatus(domain, token, params) { const resp = await postForm(`https://${domain}/api/v1/statuses`, params, token); return resp.json();