mirror of
https://github.com/ecency/ecency-mobile.git
synced 2024-11-29 11:12:18 +03:00
Merge pull request #2795 from ecency/nt/video-n-background-upload
Nt/video n background upload
This commit is contained in:
commit
f26d792ea5
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -12,5 +12,6 @@
|
||||
},
|
||||
"workbench.colorCustomizations": {
|
||||
"editorUnnecessaryCode.border": "#dd7aab"
|
||||
}
|
||||
},
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
arguments=--init-script /var/folders/b6/ssclzcc529ld6mllp97qvcg80000gn/T/d146c9752a26f79b52047fb6dc6ed385d064e120494f96f08ca63a317c41f94c.gradle --init-script /var/folders/b6/ssclzcc529ld6mllp97qvcg80000gn/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle
|
||||
auto.sync=false
|
||||
arguments=--init-script /var/folders/4n/09fh21tj1nz5pqqzh5ky5tb80000gn/T/d146c9752a26f79b52047fb6dc6ed385d064e120494f96f08ca63a317c41f94c.gradle --init-script /var/folders/4n/09fh21tj1nz5pqqzh5ky5tb80000gn/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle
|
||||
auto.sync=true
|
||||
build.scans.enabled=false
|
||||
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
|
||||
connection.project.dir=
|
||||
eclipse.preferences.version=1
|
||||
gradle.user.home=
|
||||
java.home=/Library/Java/JavaVirtualMachines/jdk-12.0.1.jdk/Contents/Home
|
||||
java.home=/Library/Java/JavaVirtualMachines/jdk-17.0.5.jdk/Contents/Home
|
||||
jvm.arguments=
|
||||
offline.mode=false
|
||||
override.workspace.settings=true
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-12/"/>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17/"/>
|
||||
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
||||
<classpathentry kind="output" path="bin/default"/>
|
||||
</classpath>
|
||||
|
3
android/app/proguard-rules.pro
vendored
3
android/app/proguard-rules.pro
vendored
@ -14,3 +14,6 @@
|
||||
|
||||
# -keep class com.swmansion.reanimated.** { *; }
|
||||
# -keep class com.facebook.react.turbomodule.** { *; }
|
||||
|
||||
# config for rn background upload
|
||||
-keep class net.gotev.uploadservice.** { *; }
|
||||
|
@ -445,6 +445,8 @@ PODS:
|
||||
- glog
|
||||
- react-native-background-timer (2.4.1):
|
||||
- React-Core
|
||||
- react-native-background-upload (6.6.0):
|
||||
- React
|
||||
- react-native-camera (4.2.1):
|
||||
- React-Core
|
||||
- react-native-camera/RCT (= 4.2.1)
|
||||
@ -459,6 +461,8 @@ PODS:
|
||||
- react-native-config/App (= 1.5.1)
|
||||
- react-native-config/App (1.5.1):
|
||||
- React-Core
|
||||
- react-native-create-thumbnail (1.6.4):
|
||||
- React-Core
|
||||
- react-native-date-picker (4.2.9):
|
||||
- React-Core
|
||||
- react-native-fingerprint-scanner (6.0.0):
|
||||
@ -651,6 +655,9 @@ PODS:
|
||||
- React-RCTImage
|
||||
- RNSVG (12.5.1):
|
||||
- React-Core
|
||||
- RNTusClient (1.1.0):
|
||||
- React-Core
|
||||
- TUSKit (~> 1.4.2)
|
||||
- RNVectorIcons (6.7.0):
|
||||
- React
|
||||
- SDWebImage (5.11.1):
|
||||
@ -665,6 +672,7 @@ PODS:
|
||||
- TOCropViewController (2.6.1)
|
||||
- toolbar-android (0.2.1):
|
||||
- React
|
||||
- TUSKit (1.4.2)
|
||||
- Yoga (1.14.0)
|
||||
- YogaKit (1.18.1):
|
||||
- Yoga (~> 1.14)
|
||||
@ -726,9 +734,11 @@ DEPENDENCIES:
|
||||
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
|
||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||
- react-native-background-timer (from `../node_modules/react-native-background-timer`)
|
||||
- react-native-background-upload (from `../node_modules/react-native-background-upload`)
|
||||
- react-native-camera (from `../node_modules/react-native-camera`)
|
||||
- "react-native-cameraroll (from `../node_modules/@react-native-community/cameraroll`)"
|
||||
- react-native-config (from `../node_modules/react-native-config`)
|
||||
- react-native-create-thumbnail (from `../node_modules/react-native-create-thumbnail`)
|
||||
- react-native-date-picker (from `../node_modules/react-native-date-picker`)
|
||||
- react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`)
|
||||
- react-native-flipper (from `../node_modules/react-native-flipper`)
|
||||
@ -777,6 +787,7 @@ DEPENDENCIES:
|
||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||
- RNScreens (from `../node_modules/react-native-screens`)
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
- RNTusClient (from `../node_modules/react-native-tus-client`)
|
||||
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
|
||||
- TcpSockets (from `../node_modules/react-native-tcp`)
|
||||
- "toolbar-android (from `../node_modules/@react-native-community/toolbar-android`)"
|
||||
@ -816,6 +827,7 @@ SPEC REPOS:
|
||||
- SDWebImageWebPCoder
|
||||
- SocketRocket
|
||||
- TOCropViewController
|
||||
- TUSKit
|
||||
- YogaKit
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@ -879,12 +891,16 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/logger"
|
||||
react-native-background-timer:
|
||||
:path: "../node_modules/react-native-background-timer"
|
||||
react-native-background-upload:
|
||||
:path: "../node_modules/react-native-background-upload"
|
||||
react-native-camera:
|
||||
:path: "../node_modules/react-native-camera"
|
||||
react-native-cameraroll:
|
||||
:path: "../node_modules/@react-native-community/cameraroll"
|
||||
react-native-config:
|
||||
:path: "../node_modules/react-native-config"
|
||||
react-native-create-thumbnail:
|
||||
:path: "../node_modules/react-native-create-thumbnail"
|
||||
react-native-date-picker:
|
||||
:path: "../node_modules/react-native-date-picker"
|
||||
react-native-fingerprint-scanner:
|
||||
@ -981,6 +997,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-screens"
|
||||
RNSVG:
|
||||
:path: "../node_modules/react-native-svg"
|
||||
RNTusClient:
|
||||
:path: "../node_modules/react-native-tus-client"
|
||||
RNVectorIcons:
|
||||
:path: "../node_modules/react-native-vector-icons"
|
||||
TcpSockets:
|
||||
@ -1049,9 +1067,11 @@ SPEC CHECKSUMS:
|
||||
React-jsinspector: 1c34fea1868136ecde647bc11fae9266d4143693
|
||||
React-logger: e9f407f9fdf3f3ce7749ae6f88affe63e8446019
|
||||
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
|
||||
react-native-background-upload: 7c608537f87106c93530a3a19a853afd55466823
|
||||
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
||||
react-native-cameraroll: e2917a5e62da9f10c3d525e157e25e694d2d6dfa
|
||||
react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8
|
||||
react-native-create-thumbnail: e022bcdcba8a0b4529a50d3fa1a832ec921be39d
|
||||
react-native-date-picker: c063a8967058c58a02d7d0e1d655f0453576fb0d
|
||||
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
||||
react-native-flipper: c33a4995958ef12a2b2f8290d63bed7adeed7634
|
||||
@ -1100,6 +1120,7 @@ SPEC CHECKSUMS:
|
||||
RNReanimated: 6668b0587bebd4b15dd849b99e5a9c70fc12ed95
|
||||
RNScreens: 4830eb40e0793b38849965cd27f4f3a7d7bc65c1
|
||||
RNSVG: d7d7bc8229af3842c9cfc3a723c815a52cdd1105
|
||||
RNTusClient: b90393226531c118c4716a2b71128e3b9d9c77ee
|
||||
RNVectorIcons: 368d6d8b8301224e5ffb6254191f4f8876c2be0d
|
||||
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
||||
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
||||
@ -1107,6 +1128,7 @@ SPEC CHECKSUMS:
|
||||
TcpSockets: 4ef55305239923b343ed0a378b1fac188b1373b0
|
||||
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
|
||||
toolbar-android: 2a73856e98b750d7e71ce4644d3f41cc98211719
|
||||
TUSKit: 4bcc2fe13e1b4d6c3bfbaca57d64e64c1be31201
|
||||
Yoga: 92d086bb705a41cc588599b51db726ba7b1d341c
|
||||
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
|
||||
|
||||
|
@ -82,7 +82,7 @@
|
||||
"domain-browser": "^1.1.1",
|
||||
"events": "^1.0.0",
|
||||
"hive-uri": "^0.2.5",
|
||||
"hivesigner": "^3.2.7",
|
||||
"hivesigner": "^3.3.4",
|
||||
"https-browserify": "~0.0.0",
|
||||
"intl": "^1.2.5",
|
||||
"jsc-android": "^241213.1.0",
|
||||
@ -104,10 +104,12 @@
|
||||
"react-native-animatable": "^1.3.3",
|
||||
"react-native-autoheight-webview": "^1.5.8",
|
||||
"react-native-background-timer": "^2.4.1",
|
||||
"react-native-background-upload": "^6.6.0",
|
||||
"react-native-bootsplash": "^4.3.2",
|
||||
"react-native-camera": "^4.2.1",
|
||||
"react-native-chart-kit": "^6.11.0",
|
||||
"react-native-config": "^1.5.1",
|
||||
"react-native-create-thumbnail": "^1.6.4",
|
||||
"react-native-crypto": "^2.2.0",
|
||||
"react-native-date-picker": "^4.2.0",
|
||||
"react-native-device-info": "^10.7.0",
|
||||
@ -154,6 +156,7 @@
|
||||
"react-native-svg": "^12.1.1",
|
||||
"react-native-swiper": "^1.6.0-rc.3",
|
||||
"react-native-tcp": "^4.0.0",
|
||||
"react-native-tus-client": "^1.1.0",
|
||||
"react-native-udp": "^4.1.4",
|
||||
"react-native-unique-id": "^2.0.0",
|
||||
"react-native-vector-icons": "^6.6.0",
|
||||
@ -212,6 +215,7 @@
|
||||
"prettier": "^2.0.2",
|
||||
"prettier-eslint": "^9.0.1",
|
||||
"react-native-codegen": "^0.0.13",
|
||||
"react-query-native-devtools": "^4.0.0",
|
||||
"react-test-renderer": "18.1.0",
|
||||
"reactotron-react-native": "^5.0.3",
|
||||
"reactotron-redux": "^3.1.3",
|
||||
|
3726
patches/react-native-create-thumbnail+1.6.4.patch
Normal file
3726
patches/react-native-create-thumbnail+1.6.4.patch
Normal file
File diff suppressed because it is too large
Load Diff
3715
patches/react-native-tus-client+1.1.0.patch
Normal file
3715
patches/react-native-tus-client+1.1.0.patch
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,6 @@ import { debounce, isArray } from 'lodash';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { FlatList, Text, View } from 'react-native';
|
||||
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import styles from './styles';
|
||||
|
||||
@ -12,6 +11,7 @@ import { lookupAccounts } from '../../providers/hive/dhive';
|
||||
import { setBeneficiaries as setBeneficiariesAction } from '../../redux/actions/editorActions';
|
||||
import { TEMP_BENEFICIARIES_ID } from '../../redux/constants/constants';
|
||||
import { Beneficiary } from '../../redux/reducers/editorReducer';
|
||||
import { BENEFICIARY_SRC_ENCODER } from '../../providers/speak/constants';
|
||||
|
||||
interface BeneficiarySelectionContentProps {
|
||||
draftId: string;
|
||||
@ -20,6 +20,7 @@ interface BeneficiarySelectionContentProps {
|
||||
label?: string;
|
||||
labelStyle?: string;
|
||||
powerDownBeneficiaries?: Beneficiary[];
|
||||
encodingBeneficiaries?: Beneficiary[];
|
||||
handleSaveBeneficiary?: (beneficiaries: Beneficiary[]) => void;
|
||||
handleRemoveBeneficiary?: (beneficiary: Beneficiary) => void;
|
||||
}
|
||||
@ -31,6 +32,7 @@ const BeneficiarySelectionContent = ({
|
||||
setDisableDone,
|
||||
powerDown,
|
||||
powerDownBeneficiaries,
|
||||
encodingBeneficiaries,
|
||||
handleSaveBeneficiary,
|
||||
handleRemoveBeneficiary,
|
||||
}: BeneficiarySelectionContentProps) => {
|
||||
@ -59,10 +61,8 @@ const BeneficiarySelectionContent = ({
|
||||
}, [powerDownBeneficiaries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (draftId) {
|
||||
readTempBeneficiaries();
|
||||
}
|
||||
}, [draftId]);
|
||||
initBeneficiaries();
|
||||
}, [draftId, encodingBeneficiaries]);
|
||||
|
||||
useEffect(() => {
|
||||
setDisableDone(newEditable);
|
||||
@ -88,25 +88,27 @@ const BeneficiarySelectionContent = ({
|
||||
}
|
||||
};
|
||||
|
||||
const readTempBeneficiaries = async () => {
|
||||
if (beneficiariesMap) {
|
||||
const savedBeneficiareis = beneficiariesMap[draftId || TEMP_BENEFICIARIES_ID];
|
||||
const tempBeneficiaries =
|
||||
savedBeneficiareis && savedBeneficiareis.length
|
||||
? [DEFAULT_BENEFICIARY, ...beneficiariesMap[draftId || TEMP_BENEFICIARIES_ID]]
|
||||
: [DEFAULT_BENEFICIARY];
|
||||
const initBeneficiaries = async () => {
|
||||
const _draftId = draftId || TEMP_BENEFICIARIES_ID;
|
||||
|
||||
if (isArray(tempBeneficiaries) && tempBeneficiaries.length > 0) {
|
||||
// weight correction algorithm.
|
||||
let othersWeight = 0;
|
||||
tempBeneficiaries.forEach((item, index) => {
|
||||
if (index > 0) {
|
||||
othersWeight += item.weight;
|
||||
}
|
||||
});
|
||||
tempBeneficiaries[0].weight = 10000 - othersWeight;
|
||||
setBeneficiaries(tempBeneficiaries);
|
||||
}
|
||||
let savedBeneficiareis: Beneficiary[] = [DEFAULT_BENEFICIARY, ...(encodingBeneficiaries || [])];
|
||||
|
||||
if (beneficiariesMap && beneficiariesMap[_draftId]) {
|
||||
const _cachedBenef = beneficiariesMap[_draftId];
|
||||
const _filteredBenef = _cachedBenef.filter((bene) => bene.src !== BENEFICIARY_SRC_ENCODER);
|
||||
savedBeneficiareis = [...savedBeneficiareis, ..._filteredBenef];
|
||||
}
|
||||
|
||||
if (savedBeneficiareis?.length > 1) {
|
||||
// weight correction algorithm.
|
||||
let othersWeight = 0;
|
||||
savedBeneficiareis.forEach((item, index) => {
|
||||
if (index > 0) {
|
||||
othersWeight += item.weight;
|
||||
}
|
||||
});
|
||||
savedBeneficiareis[0].weight = 10000 - othersWeight;
|
||||
setBeneficiaries(savedBeneficiareis);
|
||||
}
|
||||
};
|
||||
|
||||
@ -344,7 +346,7 @@ const BeneficiarySelectionContent = ({
|
||||
wrapperStyle={styles.usernameFormInputWrapper}
|
||||
/>
|
||||
</View>
|
||||
{!_isCurrentUser ? (
|
||||
{!_isCurrentUser && item.src !== BENEFICIARY_SRC_ENCODER ? (
|
||||
<IconButton
|
||||
name="close"
|
||||
iconType="MaterialCommunityIcons"
|
||||
|
@ -11,11 +11,12 @@ import Animated, { EasingNode, Extrapolate } from 'react-native-reanimated';
|
||||
import { IconButton, UploadsGalleryModal } from '../..';
|
||||
import styles from '../styles/editorToolbarStyles';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import { MediaInsertData } from '../../uploadsGalleryModal/container/uploadsGalleryModal';
|
||||
import { MediaInsertData, Modes } from '../../uploadsGalleryModal/container/uploadsGalleryModal';
|
||||
import Formats from './formats/formats';
|
||||
|
||||
type Props = {
|
||||
insertedMediaUrls: string[];
|
||||
draftId?: string;
|
||||
postBody: string;
|
||||
paramFiles: any[];
|
||||
isEditing: boolean;
|
||||
isPreviewActive: boolean;
|
||||
@ -28,7 +29,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export const EditorToolbar = ({
|
||||
insertedMediaUrls,
|
||||
draftId,
|
||||
postBody,
|
||||
paramFiles,
|
||||
isEditing,
|
||||
isPreviewActive,
|
||||
@ -46,7 +48,6 @@ export const EditorToolbar = ({
|
||||
const extensionHeight = useRef(0);
|
||||
|
||||
const [isExtensionVisible, setIsExtensionVisible] = useState(false);
|
||||
|
||||
const [isKeyboardVisible, setKeyboardVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@ -78,13 +79,28 @@ export const EditorToolbar = ({
|
||||
</View>
|
||||
);
|
||||
|
||||
const _showUploadsExtension = () => {
|
||||
if (isExtensionVisible && uploadsGalleryModalRef.current) {
|
||||
_hideExtension();
|
||||
} else if (uploadsGalleryModalRef.current) {
|
||||
uploadsGalleryModalRef.current.toggleModal(true);
|
||||
_revealExtension();
|
||||
const _showUploadsExtension = (mode: Modes) => {
|
||||
if (!uploadsGalleryModalRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _curMode = uploadsGalleryModalRef.current.getMode();
|
||||
|
||||
if (!isExtensionVisible || _curMode !== mode) {
|
||||
uploadsGalleryModalRef.current.toggleModal(true, mode);
|
||||
_revealExtension();
|
||||
return;
|
||||
}
|
||||
|
||||
_hideExtension();
|
||||
};
|
||||
|
||||
const _showImageUploads = () => {
|
||||
_showUploadsExtension(Modes.MODE_IMAGE);
|
||||
};
|
||||
|
||||
const _showVideoUploads = () => {
|
||||
_showUploadsExtension(Modes.MODE_VIDEO);
|
||||
};
|
||||
|
||||
// handles extension closing
|
||||
@ -183,8 +199,9 @@ export const EditorToolbar = ({
|
||||
>
|
||||
{isExtensionVisible && <View style={styles.indicator} />}
|
||||
<UploadsGalleryModal
|
||||
draftId={draftId}
|
||||
ref={uploadsGalleryModalRef}
|
||||
insertedMediaUrls={insertedMediaUrls}
|
||||
postBody={postBody}
|
||||
isPreviewActive={isPreviewActive}
|
||||
paramFiles={paramFiles}
|
||||
isEditing={isEditing}
|
||||
@ -243,14 +260,23 @@ export const EditorToolbar = ({
|
||||
iconType="MaterialCommunityIcons"
|
||||
name="text-short"
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={_showUploadsExtension}
|
||||
onPress={_showImageUploads}
|
||||
style={styles.rightIcons}
|
||||
size={20}
|
||||
size={18}
|
||||
iconStyle={styles.icon}
|
||||
iconType="FontAwesome"
|
||||
name="image"
|
||||
/>
|
||||
<IconButton
|
||||
onPress={_showVideoUploads}
|
||||
style={styles.rightIcons}
|
||||
size={26}
|
||||
iconStyle={styles.icon}
|
||||
iconType="MaterialCommunityIcons"
|
||||
name="video-outline"
|
||||
/>
|
||||
<View style={styles.clearButtonWrapper}>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
MediaInsertData,
|
||||
MediaInsertStatus,
|
||||
Modes,
|
||||
} from '../../../uploadsGalleryModal/container/uploadsGalleryModal';
|
||||
import { replaceBetween } from './utils';
|
||||
|
||||
@ -23,13 +24,14 @@ export default async ({ text, selection, setTextAndSelection, items }: Args) =>
|
||||
// calclulate change of cursor position
|
||||
|
||||
const imagePrefix = '!';
|
||||
|
||||
const placeholderPrefix = 'Uploading... ';
|
||||
|
||||
let newText = text;
|
||||
let newSelection = selection;
|
||||
|
||||
const _insertFormatedString = (text, value) => {
|
||||
const formatedText = `\n${imagePrefix}[${text}](${value})\n`;
|
||||
const _insertFormatedString = (text, value, mode) => {
|
||||
const formatedText = `\n${mode === Modes.MODE_VIDEO ? '' : imagePrefix}[${text}](${value})\n`;
|
||||
newText = replaceBetween(newText, newSelection, formatedText);
|
||||
const newIndex = newText && newText.indexOf(value, newSelection.start) + value.length + 2;
|
||||
newSelection = {
|
||||
@ -80,7 +82,7 @@ export default async ({ text, selection, setTextAndSelection, items }: Args) =>
|
||||
// means placeholder is preset is needs replacing
|
||||
_replaceFormatedString(_placeholder, item.url);
|
||||
} else if (item.url) {
|
||||
_insertFormatedString(item.text, item.url);
|
||||
_insertFormatedString(item.text, item.url, item.mode);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
import React, { useState, useRef, useEffect, useCallback, Fragment } from 'react';
|
||||
import { postBodySummary, renderPostBody } from '@ecency/render-helper';
|
||||
import { debounce, get } from 'lodash';
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
KeyboardAvoidingView,
|
||||
Text,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { renderPostBody, postBodySummary } from '@ecency/render-helper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { get, debounce } from 'lodash';
|
||||
import Animated, { BounceInRight } from 'react-native-reanimated';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Icon } from '../../icon';
|
||||
|
||||
// Utils
|
||||
@ -21,34 +21,34 @@ import { toggleAccountsBottomSheet } from '../../../redux/actions/uiAction';
|
||||
|
||||
// Components
|
||||
import {
|
||||
InsertLinkModal,
|
||||
Modal,
|
||||
PostBody,
|
||||
TextInput,
|
||||
UserAvatar,
|
||||
TitleArea,
|
||||
SnippetsModal,
|
||||
SummaryArea,
|
||||
TagArea,
|
||||
TagInput,
|
||||
SummaryArea,
|
||||
Modal,
|
||||
SnippetsModal,
|
||||
TextInput,
|
||||
TitleArea,
|
||||
Tooltip,
|
||||
InsertLinkModal,
|
||||
UserAvatar,
|
||||
} from '../../index';
|
||||
|
||||
// Styles
|
||||
import styles from '../styles/markdownEditorStyles';
|
||||
import applySnippet from '../children/formats/applySnippet';
|
||||
import { MainButton } from '../../mainButton';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import { walkthrough } from '../../../redux/constants/walkthroughConstants';
|
||||
import isAndroidOreo from '../../../utils/isAndroidOreo';
|
||||
import { OptionsModal } from '../../atoms';
|
||||
import { walkthrough } from '../../../redux/constants/walkthroughConstants';
|
||||
import { MainButton } from '../../mainButton';
|
||||
import { MediaInsertData } from '../../uploadsGalleryModal/container/uploadsGalleryModal';
|
||||
import { EditorToolbar } from '../children/editorToolbar';
|
||||
import { extractImageUrls } from '../../../utils/editor';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import applySnippet from '../children/formats/applySnippet';
|
||||
import styles from '../styles/markdownEditorStyles';
|
||||
|
||||
// const MIN_BODY_INPUT_HEIGHT = 300;
|
||||
|
||||
const MarkdownEditorView = ({
|
||||
draftId,
|
||||
paramFiles,
|
||||
draftBody,
|
||||
intl,
|
||||
@ -79,8 +79,6 @@ const MarkdownEditorView = ({
|
||||
const [isSnippetsOpen, setIsSnippetsOpen] = useState(false);
|
||||
const [showDraftLoadButton, setShowDraftLoadButton] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [insertedMediaUrls, setInsertedMediaUrls] = useState([]);
|
||||
// const [isDraftUpdated, setIsDraftupdated] = useState(false);
|
||||
|
||||
const inputRef = useRef<any>(null);
|
||||
const clearRef = useRef<any>(null);
|
||||
@ -192,10 +190,6 @@ const MarkdownEditorView = ({
|
||||
setIsEditing(false);
|
||||
handleBodyChange(bodyTextRef.current);
|
||||
handleFormUpdate('body', bodyTextRef.current);
|
||||
const urls = extractImageUrls({ body: bodyTextRef.current });
|
||||
if (urls.length !== insertedMediaUrls.length) {
|
||||
setInsertedMediaUrls(urls);
|
||||
}
|
||||
}, 500),
|
||||
[],
|
||||
);
|
||||
@ -437,7 +431,8 @@ const MarkdownEditorView = ({
|
||||
{_renderFloatingDraftButton()}
|
||||
|
||||
<EditorToolbar
|
||||
insertedMediaUrls={insertedMediaUrls}
|
||||
draftId={draftId}
|
||||
postBody={bodyTextRef.current}
|
||||
isPreviewActive={isPreviewActive}
|
||||
paramFiles={paramFiles}
|
||||
setIsUploading={setIsUploading}
|
||||
|
@ -4,7 +4,7 @@ import { Alert, KeyboardAvoidingView, Platform, View } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { TextInput } from '..';
|
||||
import { Snippet } from '../../models';
|
||||
import { useSnippetsMutation } from '../../providers/queries';
|
||||
import { editorQueries } from '../../providers/queries';
|
||||
import { TextButton } from '../buttons';
|
||||
import Modal from '../modal';
|
||||
import styles from './snippetEditorModalStyles';
|
||||
@ -19,7 +19,7 @@ const SnippetEditorModal = ({}, ref) => {
|
||||
const titleInputRef = useRef(null);
|
||||
const bodyInputRef = useRef(null);
|
||||
|
||||
const snippetsMutation = useSnippetsMutation();
|
||||
const snippetsMutation = editorQueries.useSnippetsMutation();
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const [body, setBody] = useState('');
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Alert, Text, View } from 'react-native';
|
||||
import { useSnippetDeleteMutation } from '../../providers/queries';
|
||||
import { editorQueries } from '../../providers/queries';
|
||||
import IconButton from '../iconButton';
|
||||
import styles from './snippetsModalStyles';
|
||||
|
||||
@ -15,7 +15,7 @@ interface SnippetItemProps {
|
||||
|
||||
const SnippetItem = ({ id, title, body, index, onEditPress }: SnippetItemProps) => {
|
||||
const intl = useIntl();
|
||||
const snippetsDeleteMutation = useSnippetDeleteMutation();
|
||||
const snippetsDeleteMutation = editorQueries.useSnippetDeleteMutation();
|
||||
|
||||
const _onRemovePress = () => {
|
||||
// asks for remvoe confirmation and run remove routing upon confirming
|
||||
|
@ -10,7 +10,7 @@ import SnippetEditorModal, {
|
||||
import SnippetItem from './snippetItem';
|
||||
import { Snippet } from '../../models';
|
||||
import { useAppSelector } from '../../hooks';
|
||||
import { useSnippetsQuery } from '../../providers/queries';
|
||||
import { editorQueries } from '../../providers/queries';
|
||||
|
||||
interface SnippetsModalProps {
|
||||
handleOnSelect: (snippetText: string) => void;
|
||||
@ -22,7 +22,7 @@ const SnippetsModal = ({ handleOnSelect }: SnippetsModalProps) => {
|
||||
|
||||
const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn);
|
||||
|
||||
const snippetsQuery = useSnippetsQuery();
|
||||
const snippetsQuery = editorQueries.useSnippetsQuery();
|
||||
|
||||
// render list item for snippet and handle actions;
|
||||
const _renderItem = ({ item, index }: { item: Snippet; index: number }) => {
|
||||
|
@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { proxifyImageSrc } from '@ecency/render-helper';
|
||||
import { ActivityIndicator, Platform, Text, TouchableOpacity, View } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { default as AnimatedView, ZoomIn } from 'react-native-reanimated';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Icon } from '../..';
|
||||
import styles from './uploadsGalleryModalStyles';
|
||||
import { MediaItem } from '../../../providers/ecency/ecency.types';
|
||||
import { ThreeSpeakStatus } from '../../../providers/speak/speak.types';
|
||||
|
||||
interface Props {
|
||||
item: MediaItem;
|
||||
insertedMediaUrls: string[];
|
||||
isDeleteMode: boolean;
|
||||
isDeleting: boolean;
|
||||
deleteIds: string[];
|
||||
isExpandedMode: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export const MediaPreviewItem = ({
|
||||
item,
|
||||
insertedMediaUrls,
|
||||
isDeleteMode,
|
||||
isDeleting,
|
||||
deleteIds,
|
||||
isExpandedMode,
|
||||
onPress,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const thumbUrl =
|
||||
item.thumbUrl || proxifyImageSrc(item.url, 600, 500, Platform.OS === 'ios' ? 'match' : 'webp');
|
||||
let isInsertedTimes = 0;
|
||||
insertedMediaUrls?.forEach((url) => (isInsertedTimes += url === item.url ? 1 : 0));
|
||||
const isToBeDeleted = deleteIds.indexOf(item._id) >= 0;
|
||||
const transformStyle = {
|
||||
transform: isToBeDeleted ? [{ scaleX: 0.7 }, { scaleY: 0.7 }] : [],
|
||||
};
|
||||
|
||||
const _renderStatus = () =>
|
||||
item.speakData && (
|
||||
<View style={{ ...styles.statusContainer, right: isExpandedMode ? 8 : 0 }}>
|
||||
<Text style={styles.statusText}>
|
||||
{intl.formatMessage({ id: `uploads_modal.${item.speakData?.status}` })}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const _renderMinus = () =>
|
||||
isDeleteMode && (
|
||||
<AnimatedView.View entering={ZoomIn} style={styles.minusContainer}>
|
||||
<Icon
|
||||
color={EStyleSheet.value('$pureWhite')}
|
||||
iconType="MaterialCommunityIcons"
|
||||
name="minus"
|
||||
size={20}
|
||||
/>
|
||||
</AnimatedView.View>
|
||||
);
|
||||
|
||||
const _renderCounter = () =>
|
||||
isInsertedTimes > 0 &&
|
||||
!isDeleteMode && (
|
||||
<AnimatedView.View entering={ZoomIn} style={styles.counterContainer}>
|
||||
<Text style={styles.counterText}>{isInsertedTimes}</Text>
|
||||
</AnimatedView.View>
|
||||
);
|
||||
|
||||
const _renderLoading = () =>
|
||||
(item.speakData?.status === ThreeSpeakStatus.PREPARING ||
|
||||
item.speakData?.status === ThreeSpeakStatus.ENCODING) && (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} disabled={isDeleting}>
|
||||
<View style={transformStyle}>
|
||||
<FastImage
|
||||
source={{ uri: thumbUrl }}
|
||||
style={isExpandedMode ? styles.gridMediaItem : styles.mediaItem}
|
||||
/>
|
||||
{_renderCounter()}
|
||||
{_renderStatus()}
|
||||
{_renderMinus()}
|
||||
{_renderLoading()}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
@ -0,0 +1,261 @@
|
||||
import React, { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { View, Text, TouchableOpacity, Image, Alert } from 'react-native';
|
||||
import ActionSheet from 'react-native-actions-sheet';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import { Video as VideoType } from 'react-native-image-crop-picker';
|
||||
import Video from 'react-native-video';
|
||||
import { createThumbnail } from 'react-native-create-thumbnail';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import ImagePicker, { Options } from 'react-native-image-crop-picker';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import * as Progress from 'react-native-progress';
|
||||
import { useIntl } from 'react-intl';
|
||||
import styles from '../styles/speakUploaderModal.styles';
|
||||
import { MainButton } from '../../mainButton';
|
||||
import { uploadFile, uploadVideoInfo } from '../../../providers/speak/speak';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import QUERIES from '../../../providers/queries/queryKeys';
|
||||
import Icon from '../../icon';
|
||||
import getWindowDimensions from '../../../utils/getWindowDimensions';
|
||||
import { TextButton } from '../../buttons';
|
||||
|
||||
interface Props {
|
||||
setIsUploading: (flag: boolean) => void;
|
||||
isUploading: boolean;
|
||||
}
|
||||
|
||||
export const SpeakUploaderModal = forwardRef(({ setIsUploading, isUploading }: Props, ref) => {
|
||||
const intl = useIntl();
|
||||
const sheetModalRef = useRef();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
const pinHash = useAppSelector((state) => state.application.pin);
|
||||
|
||||
const [selectedThumb, setSelectedThumb] = useState(null);
|
||||
const [availableThumbs, setAvailableThumbs] = useState([]);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
|
||||
const [selectedVido, setSelectedVideo] = useState<VideoType | null>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
showUploader: async (_video: VideoType) => {
|
||||
if (sheetModalRef.current) {
|
||||
sheetModalRef.current.setModalVisible(true);
|
||||
|
||||
if (_video) {
|
||||
if (!_video.filename) {
|
||||
_video.filename = _video.path.split('/').pop();
|
||||
}
|
||||
setSelectedVideo(_video);
|
||||
setSelectedThumb(null);
|
||||
setUploadProgress(0);
|
||||
|
||||
const thumbs = [];
|
||||
const _diff = _video.duration / 5;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const _thumb = await createThumbnail({
|
||||
url: _video.sourceURL || _video.path,
|
||||
timeStamp: i * _diff,
|
||||
});
|
||||
thumbs.push(_thumb);
|
||||
}
|
||||
|
||||
setAvailableThumbs(thumbs);
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const _startUpload = async () => {
|
||||
if (!selectedVido || isUploading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
try {
|
||||
const { filename, size, duration } = selectedVido;
|
||||
|
||||
const _onProgress = (progress) => {
|
||||
console.log('Upload progress', progress);
|
||||
setUploadProgress(progress);
|
||||
};
|
||||
|
||||
const videoId = await uploadFile(selectedVido, _onProgress);
|
||||
|
||||
let thumbId: any = '';
|
||||
if (selectedThumb) {
|
||||
thumbId = await uploadFile(selectedThumb);
|
||||
}
|
||||
|
||||
console.log('updating video information', videoId, thumbId);
|
||||
|
||||
const response = await uploadVideoInfo(
|
||||
currentAccount,
|
||||
pinHash,
|
||||
filename,
|
||||
size,
|
||||
videoId,
|
||||
thumbId,
|
||||
duration,
|
||||
);
|
||||
|
||||
queryClient.invalidateQueries([QUERIES.MEDIA.GET_VIDEOS]);
|
||||
|
||||
if (sheetModalRef.current) {
|
||||
sheetModalRef.current.setModalVisible(false);
|
||||
}
|
||||
|
||||
console.log('response after updating video information', response);
|
||||
} catch (err) {
|
||||
console.warn('Video upload failed', err);
|
||||
}
|
||||
|
||||
setIsUploading(false);
|
||||
};
|
||||
|
||||
const _onClosePress = () => {
|
||||
sheetModalRef.current?.setModalVisible(false);
|
||||
};
|
||||
|
||||
const _handleOpenImagePicker = () => {
|
||||
const _options: Options = {
|
||||
includeBase64: true,
|
||||
mediaType: 'photo',
|
||||
smartAlbums: ['UserLibrary', 'Favorites', 'PhotoStream', 'Panoramas', 'Bursts'],
|
||||
};
|
||||
|
||||
ImagePicker.openPicker(_options)
|
||||
.then((items) => {
|
||||
if (items && !Array.isArray(items)) {
|
||||
items = [items];
|
||||
}
|
||||
setSelectedThumb(items[0]);
|
||||
})
|
||||
.catch((e) => {
|
||||
Alert.alert('Fail', `Thumb selection failed, ${e.message}`);
|
||||
});
|
||||
};
|
||||
|
||||
const _renderThumbSelection = () => {
|
||||
const _renderThumb = (uri, onPress) => (
|
||||
<TouchableOpacity onPress={onPress} disabled={isUploading}>
|
||||
<Image source={uri && { uri }} style={styles.thumbnail} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const _renderThumbItem = ({ item }) => {
|
||||
const _onPress = () => {
|
||||
setSelectedThumb(item);
|
||||
};
|
||||
|
||||
return _renderThumb(item.path || '', _onPress);
|
||||
};
|
||||
|
||||
const _renderHeader = () => (
|
||||
<View style={styles.selectedThumbContainer}>
|
||||
<>
|
||||
{_renderThumb(selectedThumb?.path || '', _handleOpenImagePicker)}
|
||||
<Icon
|
||||
iconType="MaterialCommunityIcons"
|
||||
style={{ position: 'absolute', top: 16, left: 8 }}
|
||||
name="pencil"
|
||||
color={EStyleSheet.value('$iconColor')}
|
||||
size={20}
|
||||
/>
|
||||
</>
|
||||
|
||||
<View style={styles.thumbSeparator} />
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.imageContainer}>
|
||||
<Text style={styles.label}>{intl.formatMessage({ id: 'uploads_modal.select_thumb' })}</Text>
|
||||
<FlashList
|
||||
horizontal={true}
|
||||
ListHeaderComponent={_renderHeader}
|
||||
data={availableThumbs.slice()}
|
||||
renderItem={_renderThumbItem}
|
||||
keyExtractor={(item, index) => item.path + index}
|
||||
estimatedItemSize={128}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const _renderUploadProgress = () => {
|
||||
return (
|
||||
<Progress.Bar
|
||||
style={{ alignSelf: 'center', marginBottom: 12, borderWidth: 0 }}
|
||||
progress={uploadProgress}
|
||||
color={EStyleSheet.value('$primaryBlue')}
|
||||
unfilledColor={EStyleSheet.value('$primaryLightBackground')}
|
||||
width={getWindowDimensions().width - 40}
|
||||
indeterminate={uploadProgress === 1 && isUploading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const _renderActionPanel = () => {
|
||||
return (
|
||||
<View style={styles.actionPanel}>
|
||||
<TextButton
|
||||
text={intl.formatMessage({ id: 'alert.close' })}
|
||||
onPress={_onClosePress}
|
||||
textStyle={styles.btnTxtClose}
|
||||
style={styles.btnClose}
|
||||
/>
|
||||
<MainButton
|
||||
style={{}}
|
||||
onPress={_startUpload}
|
||||
text={intl.formatMessage({
|
||||
id: `uploads_modal.${isUploading ? 'uploading' : 'start_upload'}`,
|
||||
})}
|
||||
isDisable={isUploading}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const _renderFormContent = () => {
|
||||
return (
|
||||
<View style={styles.contentContainer}>
|
||||
{!!selectedVido && (
|
||||
<Video
|
||||
source={{
|
||||
uri: selectedVido?.sourceURL || selectedVido?.path,
|
||||
}}
|
||||
repeat={true}
|
||||
resizeMode="contain"
|
||||
fullscreen={false}
|
||||
paused={isUploading}
|
||||
style={styles.mediaPlayer}
|
||||
volume={0}
|
||||
/>
|
||||
)}
|
||||
|
||||
{_renderThumbSelection()}
|
||||
{_renderUploadProgress()}
|
||||
{_renderActionPanel()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionSheet
|
||||
ref={sheetModalRef}
|
||||
gestureEnabled={true}
|
||||
closeOnTouchBackdrop={true}
|
||||
hideUnderlay
|
||||
containerStyle={styles.sheetContent}
|
||||
indicatorColor={EStyleSheet.value('$iconColor')}
|
||||
>
|
||||
{_renderFormContent()}
|
||||
</ActionSheet>
|
||||
);
|
||||
});
|
@ -1,54 +1,57 @@
|
||||
import { proxifyImageSrc } from '@ecency/render-helper';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Keyboard,
|
||||
Platform,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { ActivityIndicator, Alert, Keyboard, Text, TouchableOpacity, View } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import Animated, {
|
||||
default as AnimatedView,
|
||||
EasingNode,
|
||||
SlideInRight,
|
||||
SlideOutRight,
|
||||
ZoomIn,
|
||||
EasingNode,
|
||||
} from 'react-native-reanimated';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Icon, IconButton } from '../..';
|
||||
import { UploadedMedia } from '../../../models';
|
||||
import { MediaItem } from '../../../providers/ecency/ecency.types';
|
||||
import { editorQueries, speakQueries } from '../../../providers/queries';
|
||||
import { MediaPreviewItem } from './mediaPreviewItem';
|
||||
import styles, {
|
||||
COMPACT_HEIGHT,
|
||||
EXPANDED_HEIGHT,
|
||||
MAX_HORIZONTAL_THUMBS,
|
||||
} from './uploadsGalleryModalStyles';
|
||||
import { useMediaDeleteMutation } from '../../../providers/queries';
|
||||
import { ThreeSpeakStatus } from '../../../providers/speak/speak.types';
|
||||
import { toastNotification } from '../../../redux/actions/uiAction';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import { Modes } from '../container/uploadsGalleryModal';
|
||||
|
||||
type Props = {
|
||||
mode: Modes;
|
||||
insertedMediaUrls: string[];
|
||||
mediaUploads: any[];
|
||||
mediaUploads: MediaItem[];
|
||||
isAddingToUploads: boolean;
|
||||
insertMedia: (map: Map<number, boolean>) => void;
|
||||
handleOpenGallery: (addToUploads?: boolean) => void;
|
||||
handleOpenSpeakUploader: () => void;
|
||||
handleOpenCamera: () => void;
|
||||
};
|
||||
|
||||
const UploadsGalleryContent = ({
|
||||
mode,
|
||||
insertedMediaUrls,
|
||||
mediaUploads,
|
||||
isAddingToUploads,
|
||||
insertMedia,
|
||||
handleOpenGallery,
|
||||
handleOpenCamera,
|
||||
handleOpenSpeakUploader,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const deleteMediaMutation = useMediaDeleteMutation();
|
||||
const deleteMediaMutation = editorQueries.useMediaDeleteMutation();
|
||||
const speakMutations = speakQueries.useSpeakMutations();
|
||||
|
||||
const allowSpkPublishing = useAppSelector((state) => state.editor.allowSpkPublishing);
|
||||
|
||||
const [deleteIds, setDeleteIds] = useState<string[]>([]);
|
||||
const [isDeleteMode, setIsDeleteMode] = useState(false);
|
||||
@ -56,7 +59,10 @@ const UploadsGalleryContent = ({
|
||||
|
||||
const animatedHeightRef = useRef(new Animated.Value(COMPACT_HEIGHT));
|
||||
|
||||
const isDeleting = deleteMediaMutation.isLoading;
|
||||
const isDeleting =
|
||||
mode === Modes.MODE_IMAGE
|
||||
? deleteMediaMutation.isLoading
|
||||
: speakMutations.deleteVideoMutation.isLoading;
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpandedMode) {
|
||||
@ -65,12 +71,28 @@ const UploadsGalleryContent = ({
|
||||
}, [isExpandedMode]);
|
||||
|
||||
const _deleteMedia = async () => {
|
||||
deleteMediaMutation.mutate(deleteIds, {
|
||||
const _options = {
|
||||
onSettled: () => {
|
||||
setIsDeleteMode(false);
|
||||
setDeleteIds([]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
switch (mode) {
|
||||
case Modes.MODE_VIDEO:
|
||||
const _permlinks: string[] = [];
|
||||
deleteIds.forEach((_id) => {
|
||||
const mediaItem = mediaUploads.find((item) => item._id === _id);
|
||||
if (mediaItem?.speakData) {
|
||||
_permlinks.push(mediaItem.speakData.permlink);
|
||||
}
|
||||
});
|
||||
speakMutations.deleteVideoMutation.mutate(_permlinks, _options);
|
||||
break;
|
||||
default:
|
||||
deleteMediaMutation.mutate(deleteIds, _options);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const _onDeletePress = async () => {
|
||||
@ -101,60 +123,72 @@ const UploadsGalleryContent = ({
|
||||
};
|
||||
|
||||
// render list item for snippet and handle actions;
|
||||
const _renderItem = ({ item, index }: { item: UploadedMedia; index: number }) => {
|
||||
const _renderItem = ({ item, index }: { item: MediaItem; index: number }) => {
|
||||
// avoid rendering unpublihsed videos in allow publishing state is false
|
||||
if (
|
||||
!allowSpkPublishing &&
|
||||
item.speakData &&
|
||||
item.speakData.status !== ThreeSpeakStatus.PUBLISHED
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const _onPress = () => {
|
||||
if (isDeleteMode) {
|
||||
const idIndex = deleteIds.indexOf(item._id);
|
||||
const deleteId = item._id;
|
||||
|
||||
const idIndex = deleteIds.indexOf(deleteId);
|
||||
if (idIndex >= 0) {
|
||||
deleteIds.splice(idIndex, 1);
|
||||
} else {
|
||||
deleteIds.push(item._id);
|
||||
deleteIds.push(deleteId);
|
||||
}
|
||||
setDeleteIds([...deleteIds]);
|
||||
} else {
|
||||
insertMedia(new Map([[index, true]]));
|
||||
let insertError: Error | null = null;
|
||||
if (item.speakData) {
|
||||
switch (item.speakData.status) {
|
||||
case ThreeSpeakStatus.READY:
|
||||
// check if a ready video is already inserted
|
||||
insertedMediaUrls.forEach((url) => {
|
||||
const _mediaItem = mediaUploads.find(
|
||||
(item) => item.url === url && item.speakData?.status === ThreeSpeakStatus.READY,
|
||||
);
|
||||
if (_mediaItem) {
|
||||
insertError = new Error('Can only have on unpublised speak speak per post');
|
||||
}
|
||||
});
|
||||
break;
|
||||
case ThreeSpeakStatus.PREPARING:
|
||||
case ThreeSpeakStatus.ENCODING:
|
||||
// interupt video insertion is it's still under processing
|
||||
insertError = new Error('Please wait while video is being processed');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Skipping corner check for published video');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!insertError) {
|
||||
insertMedia(new Map([[index, true]]));
|
||||
} else {
|
||||
dispatch(toastNotification(insertError.message));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const thumbUrl = proxifyImageSrc(item.url, 600, 500, Platform.OS === 'ios' ? 'match' : 'webp');
|
||||
let isInsertedTimes = 0;
|
||||
insertedMediaUrls?.forEach((url) => (isInsertedTimes += url === item.url ? 1 : 0));
|
||||
const isToBeDeleted = deleteIds.indexOf(item._id) >= 0;
|
||||
const transformStyle = {
|
||||
transform: isToBeDeleted ? [{ scaleX: 0.7 }, { scaleY: 0.7 }] : [],
|
||||
};
|
||||
|
||||
const _renderMinus = () =>
|
||||
isDeleteMode && (
|
||||
<AnimatedView.View entering={ZoomIn} style={styles.minusContainer}>
|
||||
<Icon
|
||||
color={EStyleSheet.value('$pureWhite')}
|
||||
iconType="MaterialCommunityIcons"
|
||||
name="minus"
|
||||
size={20}
|
||||
/>
|
||||
</AnimatedView.View>
|
||||
);
|
||||
|
||||
const _renderCounter = () =>
|
||||
isInsertedTimes > 0 &&
|
||||
!isDeleteMode && (
|
||||
<AnimatedView.View entering={ZoomIn} style={styles.counterContainer}>
|
||||
<Text style={styles.counterText}>{isInsertedTimes}</Text>
|
||||
</AnimatedView.View>
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={_onPress} disabled={isDeleting}>
|
||||
<View style={transformStyle}>
|
||||
<FastImage
|
||||
source={{ uri: thumbUrl }}
|
||||
style={isExpandedMode ? styles.gridMediaItem : styles.mediaItem}
|
||||
/>
|
||||
{_renderCounter()}
|
||||
{_renderMinus()}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<MediaPreviewItem
|
||||
item={item}
|
||||
insertedMediaUrls={insertedMediaUrls}
|
||||
deleteIds={deleteIds}
|
||||
isDeleteMode={isDeleteMode}
|
||||
isDeleting={isDeleting}
|
||||
isExpandedMode={isExpandedMode}
|
||||
onPress={_onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -188,11 +222,25 @@ const UploadsGalleryContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
const _renderSelectButtons = (
|
||||
<>
|
||||
{_renderSelectButton(
|
||||
mode === Modes.MODE_VIDEO ? 'video-box' : 'image',
|
||||
'Gallery',
|
||||
handleOpenGallery,
|
||||
)}
|
||||
{_renderSelectButton('camera', 'Camera', handleOpenCamera)}
|
||||
</>
|
||||
);
|
||||
|
||||
const _renderHeaderContent = () => (
|
||||
<View style={{ ...styles.buttonsContainer, paddingVertical: isExpandedMode ? 8 : 0 }}>
|
||||
<View style={styles.selectButtonsContainer}>
|
||||
{_renderSelectButton('image', 'Gallery', handleOpenGallery)}
|
||||
{_renderSelectButton('camera', 'Camera', handleOpenCamera)}
|
||||
{mode === Modes.MODE_IMAGE
|
||||
? _renderSelectButtons
|
||||
: isAddingToUploads
|
||||
? _renderSelectButton('progress-upload', 'Uploading', handleOpenSpeakUploader)
|
||||
: _renderSelectButtons}
|
||||
</View>
|
||||
<View style={styles.pillBtnContainer}>
|
||||
<IconButton
|
||||
@ -205,6 +253,7 @@ const UploadsGalleryContent = ({
|
||||
handleOpenGallery(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
style={{
|
||||
...styles.uploadsActionBtn,
|
||||
|
@ -180,7 +180,7 @@ export default EStyleSheet.create({
|
||||
|
||||
minusContainer: {
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
top: 12,
|
||||
left: 16,
|
||||
backgroundColor: '$primaryRed',
|
||||
borderRadius: 16,
|
||||
@ -189,7 +189,7 @@ export default EStyleSheet.create({
|
||||
|
||||
counterContainer: {
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
top: 12,
|
||||
left: 16,
|
||||
backgroundColor: '$primaryLightBackground',
|
||||
borderRadius: 16,
|
||||
@ -205,6 +205,24 @@ export default EStyleSheet.create({
|
||||
fontSize: 16,
|
||||
} as TextStyle,
|
||||
|
||||
statusContainer: {
|
||||
backgroundColor: '$primaryBlue',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 8,
|
||||
right: 0,
|
||||
borderBottomLeftRadius: 16,
|
||||
borderBottomRightRadius: 16,
|
||||
padding: 2,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
} as ViewStyle,
|
||||
|
||||
statusText: {
|
||||
color: '$pureWhite',
|
||||
fontSize: 14,
|
||||
} as TextStyle,
|
||||
|
||||
checkStyle: {
|
||||
backgroundColor: '$white',
|
||||
} as ViewStyle,
|
||||
@ -222,4 +240,14 @@ export default EStyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
} as ViewStyle,
|
||||
|
||||
loadingContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
} as ViewStyle,
|
||||
});
|
||||
|
@ -1,23 +1,34 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Alert, AlertButton } from 'react-native';
|
||||
import ImagePicker, { Image } from 'react-native-image-crop-picker';
|
||||
import ImagePicker, { Image, Options, Video } from 'react-native-image-crop-picker';
|
||||
import RNHeicConverter from 'react-native-heic-converter';
|
||||
import { openSettings } from 'react-native-permissions';
|
||||
import bugsnapInstance from '../../../config/bugsnag';
|
||||
import { getImages } from '../../../providers/ecency/ecency';
|
||||
import UploadsGalleryContent from '../children/uploadsGalleryContent';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks';
|
||||
import { delay, extractFilenameFromPath } from '../../../utils/editor';
|
||||
import {
|
||||
delay,
|
||||
extract3SpeakIds,
|
||||
extractFilenameFromPath,
|
||||
extractImageUrls,
|
||||
} from '../../../utils/editor';
|
||||
import showLoginAlert from '../../../utils/showLoginAlert';
|
||||
import { useMediaQuery, useMediaUploadMutation } from '../../../providers/queries';
|
||||
import { editorQueries, speakQueries } from '../../../providers/queries';
|
||||
import { showActionModal } from '../../../redux/actions/uiAction';
|
||||
import { MediaItem } from '../../../providers/ecency/ecency.types';
|
||||
import { SpeakUploaderModal } from '../children/speakUploaderModal';
|
||||
|
||||
export interface UploadsGalleryModalRef {
|
||||
showModal: () => void;
|
||||
}
|
||||
|
||||
export enum Modes {
|
||||
MODE_IMAGE = 0,
|
||||
MODE_VIDEO = 1,
|
||||
}
|
||||
|
||||
export enum MediaInsertStatus {
|
||||
UPLOADING = 'UPLOADING',
|
||||
READY = 'READY',
|
||||
@ -29,12 +40,13 @@ export interface MediaInsertData {
|
||||
filename?: string;
|
||||
text: string;
|
||||
status: MediaInsertStatus;
|
||||
mode: Modes;
|
||||
}
|
||||
|
||||
interface UploadsGalleryModalProps {
|
||||
insertedMediaUrls: string[];
|
||||
draftId?: string;
|
||||
postBody: string;
|
||||
paramFiles: any[];
|
||||
username: string;
|
||||
isEditing: boolean;
|
||||
isPreviewActive: boolean;
|
||||
allowMultiple?: boolean;
|
||||
@ -46,9 +58,9 @@ interface UploadsGalleryModalProps {
|
||||
export const UploadsGalleryModal = forwardRef(
|
||||
(
|
||||
{
|
||||
insertedMediaUrls,
|
||||
draftId,
|
||||
postBody,
|
||||
paramFiles,
|
||||
username,
|
||||
isEditing,
|
||||
isPreviewActive,
|
||||
allowMultiple,
|
||||
@ -61,33 +73,42 @@ export const UploadsGalleryModal = forwardRef(
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const mediaQuery = useMediaQuery();
|
||||
const mediaUploadMutation = useMediaUploadMutation();
|
||||
const imageUploadsQuery = editorQueries.useMediaQuery();
|
||||
const videoUploadsQuery = speakQueries.useVideoUploadsQuery();
|
||||
|
||||
const mediaUploadMutation = editorQueries.useMediaUploadMutation();
|
||||
|
||||
const pendingInserts = useRef<MediaInsertData[]>([]);
|
||||
const speakUploaderRef = useRef<SpeakUploaderModal>();
|
||||
|
||||
const [mediaUploads, setMediaUploads] = useState([]);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [isAddingToUploads, setIsAddingToUploads] = useState(false);
|
||||
const [mode, setMode] = useState<Modes>(Modes.MODE_IMAGE);
|
||||
const [mediaUrls, setMediaUrls] = useState<string[]>([]);
|
||||
|
||||
const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn);
|
||||
|
||||
const mediaUploadsQuery = mode === Modes.MODE_VIDEO ? videoUploadsQuery : imageUploadsQuery;
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
toggleModal: (value: boolean) => {
|
||||
toggleModal: (value: boolean, _mode: Modes = mode) => {
|
||||
if (!isLoggedIn) {
|
||||
showLoginAlert({ intl });
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === showModal) {
|
||||
if (value === showModal && _mode === mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
_getMediaUploads();
|
||||
_getMediaUploads(_mode);
|
||||
}
|
||||
|
||||
setMode(_mode);
|
||||
setShowModal(value);
|
||||
},
|
||||
getMode: () => mode,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
@ -122,21 +143,55 @@ export const UploadsGalleryModal = forwardRef(
|
||||
}, [isEditing]);
|
||||
|
||||
useEffect(() => {
|
||||
_getMediaUploads(); // get media uploads when there is new update
|
||||
}, [mediaQuery.data]);
|
||||
_getMediaUploads(mode); // get media uploads when there is new update
|
||||
}, [mediaUploadsQuery.data, mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
let _urls: string[] = [];
|
||||
if (mode === Modes.MODE_VIDEO) {
|
||||
const _vidIds = extract3SpeakIds({ body: postBody });
|
||||
_urls = _vidIds.map((id) => {
|
||||
const mediaItem = mediaUploadsQuery.data.find((item) => item._id === id);
|
||||
return mediaItem?.url;
|
||||
});
|
||||
} else {
|
||||
_urls = extractImageUrls({ body: postBody });
|
||||
}
|
||||
setMediaUrls(_urls);
|
||||
}
|
||||
}, [postBody, showModal, mode]);
|
||||
|
||||
const _handleOpenImagePicker = (addToUploads?: boolean) => {
|
||||
ImagePicker.openPicker({
|
||||
includeBase64: true,
|
||||
multiple: allowMultiple || true,
|
||||
mediaType: 'photo',
|
||||
smartAlbums: ['UserLibrary', 'Favorites', 'PhotoStream', 'Panoramas', 'Bursts'],
|
||||
})
|
||||
.then((images) => {
|
||||
if (images && !Array.isArray(images)) {
|
||||
images = [images];
|
||||
const _vidMode = mode === Modes.MODE_VIDEO;
|
||||
|
||||
if (_vidMode && isAddingToUploads) {
|
||||
speakUploaderRef.current.showUploader();
|
||||
return;
|
||||
}
|
||||
|
||||
const _options: Options = _vidMode
|
||||
? {
|
||||
mediaType: 'video',
|
||||
smartAlbums: ['UserLibrary', 'Favorites', 'Videos'],
|
||||
}
|
||||
: {
|
||||
includeBase64: true,
|
||||
multiple: allowMultiple || true,
|
||||
mediaType: 'photo',
|
||||
smartAlbums: ['UserLibrary', 'Favorites', 'PhotoStream', 'Panoramas', 'Bursts'],
|
||||
};
|
||||
|
||||
ImagePicker.openPicker(_options)
|
||||
.then((items) => {
|
||||
if (items && !Array.isArray(items)) {
|
||||
items = [items];
|
||||
}
|
||||
if (_vidMode) {
|
||||
_handleVideoSelection(items[0]);
|
||||
} else {
|
||||
_handleMediaOnSelected(items, !addToUploads);
|
||||
}
|
||||
_handleMediaOnSelected(images, !addToUploads);
|
||||
})
|
||||
.catch((e) => {
|
||||
_handleMediaOnSelectFailure(e);
|
||||
@ -144,12 +199,29 @@ export const UploadsGalleryModal = forwardRef(
|
||||
};
|
||||
|
||||
const _handleOpenCamera = () => {
|
||||
ImagePicker.openCamera({
|
||||
includeBase64: true,
|
||||
mediaType: 'photo',
|
||||
})
|
||||
.then((image) => {
|
||||
_handleMediaOnSelected([image], true);
|
||||
const _vidMode = mode === Modes.MODE_VIDEO;
|
||||
|
||||
if (_vidMode && isAddingToUploads) {
|
||||
speakUploaderRef.current.showUploader();
|
||||
return;
|
||||
}
|
||||
|
||||
const _options: Options = _vidMode
|
||||
? {
|
||||
mediaType: 'video',
|
||||
}
|
||||
: {
|
||||
includeBase64: true,
|
||||
mediaType: 'photo',
|
||||
};
|
||||
|
||||
ImagePicker.openCamera(_options)
|
||||
.then((media) => {
|
||||
if (_vidMode) {
|
||||
_handleVideoSelection(media);
|
||||
} else {
|
||||
_handleMediaOnSelected([media], true);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
_handleMediaOnSelectFailure(e);
|
||||
@ -299,6 +371,12 @@ export const UploadsGalleryModal = forwardRef(
|
||||
}
|
||||
};
|
||||
|
||||
const _handleVideoSelection = (video: Video) => {
|
||||
// show video upload modal,
|
||||
// allow thumbnail selection and uplaods
|
||||
speakUploaderRef.current.showUploader(video);
|
||||
};
|
||||
|
||||
const _handleMediaOnSelectFailure = (error) => {
|
||||
let title = intl.formatMessage({ id: 'alert.something_wrong' });
|
||||
let body = error.message || JSON.stringify(error);
|
||||
@ -336,6 +414,15 @@ export const UploadsGalleryModal = forwardRef(
|
||||
);
|
||||
};
|
||||
|
||||
const _handleOpenSpeakUploader = () => {
|
||||
speakUploaderRef.current.showUploader();
|
||||
};
|
||||
|
||||
const _setIsSpeakUploading = (flag: boolean) => {
|
||||
setIsUploading(flag);
|
||||
setIsAddingToUploads(flag);
|
||||
};
|
||||
|
||||
const _handleMediaInsertion = (data: MediaInsertData) => {
|
||||
if (isEditing) {
|
||||
pendingInserts.current.push(data);
|
||||
@ -345,14 +432,9 @@ export const UploadsGalleryModal = forwardRef(
|
||||
};
|
||||
|
||||
// fetch images from server
|
||||
const _getMediaUploads = async () => {
|
||||
const _getMediaUploads = async (_mode: Modes = mode) => {
|
||||
try {
|
||||
if (username) {
|
||||
console.log(`getting images for: ${username}`);
|
||||
const images = await getImages();
|
||||
console.log('images received', images);
|
||||
setMediaUploads(images || []);
|
||||
}
|
||||
mediaUploadsQuery.refetch();
|
||||
} catch (err) {
|
||||
console.warn('Failed to get images');
|
||||
}
|
||||
@ -365,30 +447,46 @@ export const UploadsGalleryModal = forwardRef(
|
||||
|
||||
map.forEach((value, index) => {
|
||||
console.log(index);
|
||||
const item = mediaUploads[index];
|
||||
const item: MediaItem = mediaUploadsQuery.data[index];
|
||||
data.push({
|
||||
url: item.url,
|
||||
text: '',
|
||||
url: mode === Modes.MODE_VIDEO ? item.speakData?._id || '' : item.url,
|
||||
text: mode === Modes.MODE_VIDEO ? `3speak` : '',
|
||||
status: MediaInsertStatus.READY,
|
||||
mode,
|
||||
});
|
||||
});
|
||||
|
||||
handleMediaInsert(data);
|
||||
};
|
||||
|
||||
const data = mediaUploadsQuery.data.slice();
|
||||
|
||||
if (isPreviewActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
!isPreviewActive &&
|
||||
showModal && (
|
||||
<UploadsGalleryContent
|
||||
insertedMediaUrls={insertedMediaUrls}
|
||||
mediaUploads={mediaQuery.data.slice()}
|
||||
isAddingToUploads={isAddingToUploads}
|
||||
getMediaUploads={_getMediaUploads}
|
||||
insertMedia={_insertMedia}
|
||||
handleOpenCamera={_handleOpenCamera}
|
||||
handleOpenGallery={_handleOpenImagePicker}
|
||||
<>
|
||||
{showModal && (
|
||||
<UploadsGalleryContent
|
||||
mode={mode}
|
||||
draftId={draftId}
|
||||
insertedMediaUrls={mediaUrls}
|
||||
mediaUploads={data}
|
||||
isAddingToUploads={isAddingToUploads}
|
||||
getMediaUploads={_getMediaUploads}
|
||||
insertMedia={_insertMedia}
|
||||
handleOpenCamera={_handleOpenCamera}
|
||||
handleOpenGallery={_handleOpenImagePicker}
|
||||
handleOpenSpeakUploader={_handleOpenSpeakUploader}
|
||||
/>
|
||||
)}
|
||||
<SpeakUploaderModal
|
||||
ref={speakUploaderRef}
|
||||
isUploading={isAddingToUploads}
|
||||
setIsUploading={_setIsSpeakUploading}
|
||||
/>
|
||||
)
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -0,0 +1,98 @@
|
||||
import { Dimensions, TextStyle, ViewStyle } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
const SCREEN_WIDTH = Dimensions.get('screen').width;
|
||||
|
||||
export default EStyleSheet.create({
|
||||
modalStyle: {
|
||||
backgroundColor: '$primaryBackgroundColor',
|
||||
margin: 0,
|
||||
paddingTop: 32,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
|
||||
sheetContent: {
|
||||
backgroundColor: '$primaryBackgroundColor',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 999,
|
||||
},
|
||||
contentContainer: {
|
||||
paddingBottom: 40,
|
||||
},
|
||||
imageContainer: {
|
||||
paddingHorizontal: 16,
|
||||
marginVertical: 20,
|
||||
},
|
||||
selectedThumbContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
thumbSeparator: {
|
||||
width: 6,
|
||||
marginTop: 12,
|
||||
marginRight: 12,
|
||||
flex: 1,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '$iconColor',
|
||||
},
|
||||
thumbnail: {
|
||||
marginTop: 10,
|
||||
width: 128,
|
||||
height: 72,
|
||||
resizeMode: 'cover',
|
||||
borderRadius: 8,
|
||||
marginRight: 12,
|
||||
backgroundColor: '$primaryLightBackground',
|
||||
},
|
||||
label: {
|
||||
color: '$primaryDarkGray',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
flexGrow: 1,
|
||||
textAlign: 'left',
|
||||
},
|
||||
titleBox: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
titleInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 5,
|
||||
padding: 8,
|
||||
width: '80%',
|
||||
marginTop: 10,
|
||||
color: '$primaryDarkGray',
|
||||
},
|
||||
actionPanel: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
paddingHorizontal: 16,
|
||||
} as ViewStyle,
|
||||
btnTxtClose: {
|
||||
color: '$iconColor',
|
||||
fontSize: 16,
|
||||
} as TextStyle,
|
||||
btnClose: {
|
||||
marginRight: 12,
|
||||
} as ViewStyle,
|
||||
uploadButton: {
|
||||
marginBottom: 24,
|
||||
paddingHorizontal: 16,
|
||||
alignSelf: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
mediaPlayer: {
|
||||
width: SCREEN_WIDTH,
|
||||
height: SCREEN_WIDTH / 1.77,
|
||||
backgroundColor: 'black',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
@ -515,6 +515,7 @@
|
||||
"scheduled_for":"Scheduled For",
|
||||
"scheduled_immediate":"Immediate",
|
||||
"scheduled_later":"Later",
|
||||
"schedule_video_unsupported":"Scheduling video posts not available",
|
||||
"settings_title":"Post Options",
|
||||
"done":"DONE",
|
||||
"draft_save_title":"Saving Draft",
|
||||
@ -557,10 +558,20 @@
|
||||
"btn_add":"Image",
|
||||
"btn_insert":"INSERT",
|
||||
"btn_delete":"DELETE",
|
||||
"confirm_delete":"Are you sure you want to delete images from your uploads",
|
||||
"confirm_delete":"Are you sure you want to delete selected items from your uploads",
|
||||
"message_failed":"Failed to upload image",
|
||||
"delete_failed":"Failed to delete image",
|
||||
"failed_count":"Failed to upload {failedCount} of {totalCount} selected image(s)"
|
||||
"failed_count":"Failed to upload {failedCount} of {totalCount} selected image(s)",
|
||||
"publish_manual":"Ready",
|
||||
"published":"Published",
|
||||
"encoding_ipfs":"Encoding",
|
||||
"encoding_preparing":"Preparing",
|
||||
"deleted":"Deleted",
|
||||
"start_upload":"START UPLOAD",
|
||||
"uploading":"UPLOADING",
|
||||
"select_thumb":"Select Thumbnail",
|
||||
"warn_vid_already_added":"",
|
||||
"warn_vid_in_process":""
|
||||
},
|
||||
"pincode": {
|
||||
"enter_text": "Enter PIN to unlock",
|
||||
@ -582,6 +593,8 @@
|
||||
"continue": "Continue",
|
||||
"okay":"Okay",
|
||||
"done":"Done",
|
||||
"notice":"Notice!",
|
||||
"close":"CLOSE",
|
||||
"move_question": "Are you sure to move to drafts?",
|
||||
"success_shared": "Success! Content submitted!",
|
||||
"success_moved": "Moved to draft",
|
||||
|
@ -15,6 +15,12 @@ import { initQueryClient } from './providers/queries';
|
||||
|
||||
const queryClientProviderProps = initQueryClient();
|
||||
|
||||
if (__DEV__) {
|
||||
import('react-query-native-devtools').then(({ addPlugin }) => {
|
||||
addPlugin({ queryClient: queryClientProviderProps.client });
|
||||
});
|
||||
}
|
||||
|
||||
const _renderApp = ({ locale }) => (
|
||||
<PersistQueryClientProvider {...queryClientProviderProps}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
|
@ -43,6 +43,7 @@ const RootStack = createNativeStackNavigator();
|
||||
const MainStack = createNativeStackNavigator();
|
||||
|
||||
const MainStackNavigator = () => {
|
||||
// TODO: remove initialRoute before PR
|
||||
return (
|
||||
<MainStack.Navigator screenOptions={{ headerShown: false, animation: 'slide_from_right' }}>
|
||||
<MainStack.Screen name={ROUTES.DRAWER.MAIN} component={DrawerNavigator} />
|
||||
@ -88,7 +89,6 @@ export const StackNavigator = ({ initRoute }) => {
|
||||
screenOptions={{ headerShown: false, animation: 'slide_from_bottom' }}
|
||||
>
|
||||
<RootStack.Screen name={ROUTES.STACK.MAIN} component={MainStackNavigator} />
|
||||
|
||||
<RootStack.Screen name={ROUTES.SCREENS.REGISTER} component={Register} />
|
||||
<RootStack.Screen name={ROUTES.SCREENS.LOGIN} component={Login} />
|
||||
<RootStack.Screen name={ROUTES.SCREENS.WELCOME} component={WelcomeScreen} />
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { QuoteItem } from '../../redux/reducers/walletReducer';
|
||||
import { ThreeSpeakVideo } from '../speak/speak.types';
|
||||
|
||||
export interface ReceivedVestingShare {
|
||||
delegator: string;
|
||||
@ -10,8 +11,10 @@ export interface ReceivedVestingShare {
|
||||
export interface MediaItem {
|
||||
_id: string;
|
||||
url: string;
|
||||
thumbUrl: string;
|
||||
created: string;
|
||||
timestamp: number;
|
||||
speakData?: ThreeSpeakVideo;
|
||||
}
|
||||
|
||||
export interface Snippet {
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Image } from 'react-native-image-crop-picker';
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks';
|
||||
import { toastNotification } from '../../redux/actions/uiAction';
|
||||
import Upload, { UploadOptions } from 'react-native-background-upload';
|
||||
import Config from 'react-native-config';
|
||||
import { Platform } from 'react-native';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks';
|
||||
import { toastNotification } from '../../../redux/actions/uiAction';
|
||||
import {
|
||||
addFragment,
|
||||
addImage,
|
||||
@ -11,11 +14,11 @@ import {
|
||||
getFragments,
|
||||
getImages,
|
||||
updateFragment,
|
||||
uploadImage,
|
||||
} from '../ecency/ecency';
|
||||
import { MediaItem, Snippet } from '../ecency/ecency.types';
|
||||
import { signImage } from '../hive/dhive';
|
||||
import QUERIES from './queryKeys';
|
||||
} from '../../ecency/ecency';
|
||||
import { MediaItem, Snippet } from '../../ecency/ecency.types';
|
||||
import { signImage } from '../../hive/dhive';
|
||||
import QUERIES from '../queryKeys';
|
||||
import bugsnapInstance from '../../../config/bugsnag';
|
||||
|
||||
interface SnippetMutationVars {
|
||||
id: string | null;
|
||||
@ -84,14 +87,66 @@ export const useMediaUploadMutation = () => {
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
const pinCode = useAppSelector((state) => state.application.pin);
|
||||
|
||||
const _uploadMedia = async ({ media }: MediaUploadVars) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
signImage(media, currentAccount, pinCode)
|
||||
.then((sign) => {
|
||||
const _options: UploadOptions = {
|
||||
url: `${Config.NEW_IMAGE_API}/hs/${sign}`,
|
||||
path: Platform.select({
|
||||
ios: `file://${media.path}`,
|
||||
android: media.path.replace('file://', ''),
|
||||
}),
|
||||
method: 'POST',
|
||||
type: 'multipart',
|
||||
maxRetries: 2, // set retry count (Android only). Default 2
|
||||
headers: {
|
||||
Authorization: Config.NEW_IMAGE_API, // Config.NEW_IMAGE_API
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
field: 'uploaded_media',
|
||||
// Below are options only supported on Android
|
||||
notification: {
|
||||
enabled: true,
|
||||
},
|
||||
useUtf8Charset: true,
|
||||
};
|
||||
|
||||
console.log('Upload starting');
|
||||
return Upload.startUpload(_options);
|
||||
})
|
||||
.then((uploadId) => {
|
||||
Upload.addListener('progress', uploadId, (data) => {
|
||||
console.log(`Progress: ${data.progress}%`, data);
|
||||
});
|
||||
Upload.addListener('error', uploadId, (data) => {
|
||||
console.log(`Error`, data);
|
||||
throw data.error;
|
||||
});
|
||||
Upload.addListener('cancelled', uploadId, (data) => {
|
||||
console.log(`Cancelled!`, data);
|
||||
throw new Error('Upload Cancelled');
|
||||
});
|
||||
Upload.addListener('completed', uploadId, (data) => {
|
||||
// data includes responseCode: number and responseBody: Object
|
||||
console.log('Completed!', data);
|
||||
const _respData = JSON.parse(data.responseBody);
|
||||
resolve(_respData);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('Meida Upload Failed', err);
|
||||
bugsnapInstance.notify('Media upload failed', err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return useMutation<Image, undefined, MediaUploadVars>(
|
||||
async ({ media }) => {
|
||||
console.log('uploading media', media);
|
||||
const sign = await signImage(media, currentAccount, pinCode);
|
||||
return uploadImage(media, currentAccount.name, sign);
|
||||
(vars) => {
|
||||
return _uploadMedia(vars);
|
||||
},
|
||||
{
|
||||
retry: 3,
|
||||
onSuccess: (response, { addToUploads }) => {
|
||||
if (addToUploads && response && response.url) {
|
||||
console.log('adding image to gallery', response.url);
|
4
src/providers/queries/editorQueries/index.ts
Normal file
4
src/providers/queries/editorQueries/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import * as editorQueries from './editorQueries';
|
||||
import * as speakQueries from './speakQueries';
|
||||
|
||||
export { editorQueries, speakQueries };
|
234
src/providers/queries/editorQueries/speakQueries.ts
Normal file
234
src/providers/queries/editorQueries/speakQueries.ts
Normal file
@ -0,0 +1,234 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks';
|
||||
import { showActionModal, toastNotification } from '../../../redux/actions/uiAction';
|
||||
import { MediaItem } from '../../ecency/ecency.types';
|
||||
import {
|
||||
deleteVideo,
|
||||
getAllVideoStatuses,
|
||||
markAsPublished,
|
||||
updateSpeakVideoInfo,
|
||||
} from '../../speak/speak';
|
||||
import QUERIES from '../queryKeys';
|
||||
import { extract3SpeakIds } from '../../../utils/editor';
|
||||
import { ThreeSpeakStatus, ThreeSpeakVideo } from '../../speak/speak.types';
|
||||
import bugsnapInstance from '../../../config/bugsnag';
|
||||
|
||||
/**
|
||||
* fetches and caches speak video uploads
|
||||
* @returns query instance with data as array of videos as MediaItem[]
|
||||
*/
|
||||
export const useVideoUploadsQuery = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
const pinHash = useAppSelector((state) => state.application.pin);
|
||||
|
||||
const _fetchVideoUploads = async () => getAllVideoStatuses(currentAccount, pinHash);
|
||||
|
||||
const _setRefetchInterval = (data: MediaItem[] | undefined) => {
|
||||
if (data) {
|
||||
const hasPendingItem = data.find(
|
||||
(item) =>
|
||||
item.speakData?.status === ThreeSpeakStatus.PREPARING ||
|
||||
item.speakData?.status === ThreeSpeakStatus.ENCODING,
|
||||
);
|
||||
|
||||
if (hasPendingItem) {
|
||||
return 1000;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return useQuery<MediaItem[]>([QUERIES.MEDIA.GET_VIDEOS], _fetchVideoUploads, {
|
||||
initialData: [],
|
||||
refetchInterval: _setRefetchInterval,
|
||||
onError: () => {
|
||||
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useSpeakContentBuilder = () => {
|
||||
const dispatch = useDispatch();
|
||||
const videoUploads = useVideoUploadsQuery();
|
||||
const videoPublishMetaRef = useRef<ThreeSpeakVideo | null>(null);
|
||||
const thumbUrlsRef = useRef<string[]>([]);
|
||||
|
||||
const build = (body: string) => {
|
||||
let _newBody = body;
|
||||
videoPublishMetaRef.current = null;
|
||||
thumbUrlsRef.current = [];
|
||||
const _ids = extract3SpeakIds({ body });
|
||||
const thumbUrls: string[] = [];
|
||||
|
||||
_ids.forEach((id) => {
|
||||
const mediaItem: MediaItem | undefined = videoUploads.data.find((item) => item._id === id);
|
||||
if (mediaItem) {
|
||||
// check if video is unpublished, set unpublish video meta
|
||||
if (mediaItem.speakData?.status !== ThreeSpeakStatus.PUBLISHED) {
|
||||
if (!videoPublishMetaRef.current) {
|
||||
videoPublishMetaRef.current = mediaItem.speakData;
|
||||
} else {
|
||||
dispatch(
|
||||
showActionModal({
|
||||
title: 'Fail',
|
||||
body: 'Can have only one unpublished video per post',
|
||||
}),
|
||||
);
|
||||
throw new Error('Fail');
|
||||
}
|
||||
}
|
||||
|
||||
// replace 3speak with actual data
|
||||
const _toReplaceStr = `[3speak](${id})`;
|
||||
const _replacement = `<center>[![](${mediaItem.thumbUrl})](${mediaItem.url})</center>`;
|
||||
_newBody = _newBody.replace(_toReplaceStr, _replacement);
|
||||
|
||||
thumbUrls.push(mediaItem.thumbUrl);
|
||||
}
|
||||
});
|
||||
|
||||
thumbUrlsRef.current = thumbUrls;
|
||||
|
||||
return _newBody;
|
||||
};
|
||||
|
||||
return {
|
||||
build,
|
||||
videoPublishMetaRef,
|
||||
thumbUrlsRef,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSpeakMutations = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
const pinCode = useAppSelector((state) => state.application.pin);
|
||||
|
||||
// mark as published mutations id is options, if no id is provided program marks all notifications as read;
|
||||
const _mutationFn = async (id: string) => {
|
||||
try {
|
||||
const response = await markAsPublished(currentAccount, pinCode, id);
|
||||
console.log('Speak video marked as published', response);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
bugsnapInstance.notify(err);
|
||||
}
|
||||
};
|
||||
|
||||
const _options: UseMutationOptions<number, unknown, string | undefined, void> = {
|
||||
retry: 3,
|
||||
delay: 5000,
|
||||
onMutate: async (videoId) => {
|
||||
// TODO: find a way to optimise mutations by avoiding too many loops
|
||||
console.log('on mutate data', videoId);
|
||||
|
||||
// update query data
|
||||
const videosCache: MediaItem[] | undefined = queryClient.getQueryData([
|
||||
QUERIES.MEDIA.GET_VIDEOS,
|
||||
]);
|
||||
console.log('query data', videosCache);
|
||||
|
||||
if (!videosCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _vidIndex = videosCache.findIndex((item) => item._id === videoId);
|
||||
|
||||
if (_vidIndex) {
|
||||
const spkData = videosCache[_vidIndex].speakData;
|
||||
if (spkData) {
|
||||
spkData.status = ThreeSpeakStatus.PUBLISHED;
|
||||
}
|
||||
}
|
||||
|
||||
queryClient.setQueryData([QUERIES.MEDIA.GET_VIDEOS], videosCache);
|
||||
},
|
||||
|
||||
onSuccess: async (status, _id) => {
|
||||
console.log('on success data', status);
|
||||
queryClient.invalidateQueries([QUERIES.MEDIA.GET_VIDEOS]);
|
||||
},
|
||||
onError: () => {
|
||||
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
|
||||
},
|
||||
};
|
||||
|
||||
// update info mutation
|
||||
const _updateInfoMutationFn = async ({ id, title, body, tags }) => {
|
||||
try {
|
||||
// TODO: update information
|
||||
const response = await updateSpeakVideoInfo(currentAccount, pinCode, body, id, title, tags);
|
||||
console.log('Speak video marked as published', response);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
bugsnapInstance.notify(err);
|
||||
}
|
||||
};
|
||||
|
||||
const _updateInfoOptions = {
|
||||
retry: 3,
|
||||
onSuccess: async (status, _data) => {
|
||||
console.log('on success data', status);
|
||||
queryClient.invalidateQueries([QUERIES.MEDIA.GET_VIDEOS]);
|
||||
},
|
||||
onError: () => {
|
||||
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
|
||||
},
|
||||
};
|
||||
|
||||
// delete mutation
|
||||
const _deleteMutationFn = async (permlinks: string[]) => {
|
||||
try {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const i in permlinks) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await deleteVideo(currentAccount, pinCode, permlinks[i]);
|
||||
}
|
||||
console.log('deleted speak videos', permlinks);
|
||||
return true;
|
||||
} catch (err) {
|
||||
bugsnapInstance.notify(err);
|
||||
}
|
||||
};
|
||||
|
||||
const _deleteVideoOptions = {
|
||||
retry: 3,
|
||||
onSuccess: async (status, permlinks) => {
|
||||
console.log('Success media deletion', status, permlinks);
|
||||
const data: MediaItem[] | undefined = queryClient.getQueryData([QUERIES.MEDIA.GET_VIDEOS]);
|
||||
if (data) {
|
||||
const _newData = data.filter((item) => !permlinks.includes(item.speakData?.permlink));
|
||||
queryClient.setQueryData([QUERIES.MEDIA.GET_VIDEOS], _newData);
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries([QUERIES.MEDIA.GET_VIDEOS]);
|
||||
},
|
||||
onError: (err) => {
|
||||
console.warn('delete failing', err);
|
||||
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
|
||||
},
|
||||
};
|
||||
|
||||
// init mutations
|
||||
const markAsPublishedMutation = useMutation(_mutationFn, _options);
|
||||
const updateInfoMutation = useMutation(_updateInfoMutationFn, _updateInfoOptions);
|
||||
const deleteVideoMutation = useMutation(_deleteMutationFn, _deleteVideoOptions);
|
||||
|
||||
return {
|
||||
markAsPublishedMutation,
|
||||
updateInfoMutation,
|
||||
deleteVideoMutation,
|
||||
};
|
||||
};
|
@ -13,6 +13,7 @@ const QUERIES = {
|
||||
},
|
||||
MEDIA: {
|
||||
GET: 'QUERY_GET_UPLOADS',
|
||||
GET_VIDEOS: 'QUERY_GET_VIDEO_UPLOADS',
|
||||
},
|
||||
WALLET: {
|
||||
GET: 'QUERY_GET_ASSETS',
|
||||
|
24
src/providers/speak/constants.ts
Normal file
24
src/providers/speak/constants.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export const BASE_URL_SPEAK_STUDIO = 'https://studio.3speak.tv';
|
||||
|
||||
export const BASE_URL_SPEAK_UPLOAD = 'https://uploads.3speak.tv/files';
|
||||
|
||||
export const BASE_URL_SPEAK_WATCH = 'https://3speak.tv/watch';
|
||||
|
||||
export const PATH_MOBILE = 'mobile';
|
||||
export const PATH_LOGIN = 'login';
|
||||
export const PATH_API = 'api';
|
||||
|
||||
export const DEFAULT_SPEAK_BENEFICIARIES = [
|
||||
{
|
||||
account: 'spk.beneficiary',
|
||||
src: 'ENCODER_PAY',
|
||||
weight: 900,
|
||||
},
|
||||
{
|
||||
account: 'threespeakleader',
|
||||
src: 'ENCODER_PAY',
|
||||
weight: 100,
|
||||
},
|
||||
];
|
||||
|
||||
export const BENEFICIARY_SRC_ENCODER = 'ENCODER_PAY';
|
13
src/providers/speak/converters.ts
Normal file
13
src/providers/speak/converters.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { MediaItem } from '../ecency/ecency.types';
|
||||
import { BASE_URL_SPEAK_WATCH } from './constants';
|
||||
|
||||
export const convertVideoUpload = (data) => {
|
||||
return {
|
||||
_id: data._id,
|
||||
url: `${BASE_URL_SPEAK_WATCH}?v=${data.owner}/${data.permlink}`,
|
||||
thumbUrl: data.thumbUrl,
|
||||
created: data.created,
|
||||
timestamp: 0,
|
||||
speakData: data,
|
||||
} as MediaItem;
|
||||
};
|
229
src/providers/speak/speak.ts
Normal file
229
src/providers/speak/speak.ts
Normal file
@ -0,0 +1,229 @@
|
||||
import axios from 'axios';
|
||||
import hs from 'hivesigner';
|
||||
import { Image, Video } from 'react-native-image-crop-picker';
|
||||
import { Upload } from 'react-native-tus-client';
|
||||
import { Platform } from 'react-native';
|
||||
import { getDigitPinCode } from '../hive/dhive';
|
||||
import { ThreeSpeakVideo } from './speak.types';
|
||||
import { decryptKey } from '../../utils/crypto';
|
||||
import { convertVideoUpload } from './converters';
|
||||
import { BASE_URL_SPEAK_STUDIO, PATH_API, PATH_LOGIN, PATH_MOBILE } from './constants';
|
||||
|
||||
const tusEndPoint = 'https://uploads.3speak.tv/files/';
|
||||
|
||||
const speakApi = axios.create({
|
||||
baseURL: `${BASE_URL_SPEAK_STUDIO}/${PATH_MOBILE}`,
|
||||
});
|
||||
|
||||
export const threespeakAuth = async (currentAccount: any, pinHash: string) => {
|
||||
try {
|
||||
const response = await speakApi.get(
|
||||
`${PATH_LOGIN}?username=${currentAccount.username}&hivesigner=true`,
|
||||
{
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
const memo_string = response.data.memo;
|
||||
const memoDecoded = await getDecodedMemo(currentAccount.local, pinHash, memo_string);
|
||||
|
||||
return memoDecoded.replace('#', '');
|
||||
} catch (err) {
|
||||
console.error(new Error('[3Speak auth] Failed to login'));
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadVideoInfo = async (
|
||||
currentAccount: any,
|
||||
pinHash: string,
|
||||
oFilename: string,
|
||||
fileSize: number,
|
||||
videoId: string,
|
||||
thumbnailId: string,
|
||||
duration: string,
|
||||
) => {
|
||||
const token = await threespeakAuth(currentAccount, pinHash);
|
||||
try {
|
||||
const { data } = await speakApi.post<ThreeSpeakVideo>(
|
||||
`${PATH_API}/upload_info?app=ecency`,
|
||||
{
|
||||
filename: videoId,
|
||||
oFilename,
|
||||
size: fileSize,
|
||||
duration,
|
||||
thumbnail: thumbnailId,
|
||||
isReel: false,
|
||||
owner: currentAccount.username,
|
||||
},
|
||||
{
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllVideoStatuses = async (currentAccount: any, pinHash: string) => {
|
||||
const token = await threespeakAuth(currentAccount, pinHash);
|
||||
try {
|
||||
const response = await speakApi.get<ThreeSpeakVideo[]>(`${PATH_API}/my-videos`, {
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const mediaItems = response.data.map(convertVideoUpload);
|
||||
|
||||
return mediaItems;
|
||||
} catch (err) {
|
||||
console.error(new Error('[3Speak video] Failed to get videos'));
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// TOOD: use api during post publishing
|
||||
export const updateSpeakVideoInfo = async (
|
||||
currentAccount: any,
|
||||
pinHash: string,
|
||||
postBody: string,
|
||||
videoId: string,
|
||||
title: string,
|
||||
tags: string[],
|
||||
isNsfwC?: boolean,
|
||||
) => {
|
||||
const token = await threespeakAuth(currentAccount, pinHash);
|
||||
|
||||
const data = {
|
||||
videoId,
|
||||
title,
|
||||
description: postBody,
|
||||
isNsfwContent: isNsfwC || false,
|
||||
tags_v2: tags,
|
||||
};
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
|
||||
try {
|
||||
await speakApi.post(`${PATH_API}/update_info`, data, { headers });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const markAsPublished = async (currentAccount: any, pinHash: string, videoId: string) => {
|
||||
const token = await threespeakAuth(currentAccount, pinHash);
|
||||
const data = {
|
||||
videoId,
|
||||
};
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
|
||||
speakApi
|
||||
.post(`${PATH_API}/my-videos/iPublished`, data, { headers })
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
};
|
||||
|
||||
// 'https://studio.3speak.tv/mobile/api/video/${permlink}/delete
|
||||
|
||||
export const deleteVideo = async (currentAccount: any, pinHash: string, permlink: string) => {
|
||||
const token = await threespeakAuth(currentAccount, pinHash);
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
|
||||
speakApi
|
||||
.get(`${PATH_API}/video/${permlink}/delete`, { headers })
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadFile = (media: Video | Image, onProgress?: (progress: number) => void) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const _path = Platform.select({
|
||||
ios: media.path,
|
||||
android: media.path.replace('file://', ''),
|
||||
});
|
||||
|
||||
if (!_path) {
|
||||
throw new Error('failed to create apporpriate path');
|
||||
}
|
||||
|
||||
const upload = new Upload(_path, {
|
||||
endpoint: tusEndPoint, // use your tus server endpoint instead
|
||||
metadata: {
|
||||
filename: media.filename || media.path.split('/').pop(),
|
||||
filetype: media.mime,
|
||||
},
|
||||
onError: (error) => console.log('error', error),
|
||||
onSuccess: () => {
|
||||
console.log('Upload completed. File url:', upload.url);
|
||||
const _videoId = upload.url.replace(tusEndPoint, '');
|
||||
resolve(_videoId);
|
||||
},
|
||||
|
||||
onProgress: (uploaded, total) => {
|
||||
if (onProgress) {
|
||||
onProgress(uploaded / total);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
upload.start();
|
||||
} catch (error) {
|
||||
console.warn('Image upload failed', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getDecodedMemo = async (local, pinHash, encryptedMemo) => {
|
||||
try {
|
||||
const digitPinCode = getDigitPinCode(pinHash);
|
||||
const token = decryptKey(local.accessToken, digitPinCode);
|
||||
|
||||
const client = new hs.Client({
|
||||
accessToken: token,
|
||||
});
|
||||
|
||||
const { memoDecoded } = await client.decode(encryptedMemo);
|
||||
|
||||
if (!memoDecoded) {
|
||||
throw new Error('Decode failed');
|
||||
}
|
||||
|
||||
return memoDecoded;
|
||||
} catch (err) {
|
||||
console.warn('Failed to decode memo key', err);
|
||||
}
|
||||
};
|
60
src/providers/speak/speak.types.ts
Normal file
60
src/providers/speak/speak.types.ts
Normal file
@ -0,0 +1,60 @@
|
||||
export enum ThreeSpeakStatus {
|
||||
PUBLISHED = 'published',
|
||||
READY = 'publish_manual',
|
||||
DELETED = 'deleted',
|
||||
ENCODING = 'encoding_ipfs',
|
||||
PREPARING = 'encoding_preparing',
|
||||
}
|
||||
|
||||
export interface ThreeSpeakVideo {
|
||||
app: string;
|
||||
beneficiaries: string; // e.g. "[{\"account\":\"actifit-he\",\"weight\":100,\"src\":\"ENCODER_PAY\"}]"
|
||||
category: string;
|
||||
community: unknown | null;
|
||||
created: string; // e.g. "2023-06-21T12:02:10.421Z"
|
||||
declineRewards: boolean;
|
||||
description: string;
|
||||
donations: boolean;
|
||||
duration: number;
|
||||
encoding: Record<number, boolean>;
|
||||
encodingProgress: number;
|
||||
encoding_price_steem: string;
|
||||
filename: string;
|
||||
firstUpload: boolean;
|
||||
fromMobile: boolean;
|
||||
height: unknown;
|
||||
hive: string;
|
||||
indexed: boolean;
|
||||
is3CJContent: boolean;
|
||||
isNsfwContent: boolean;
|
||||
isReel: boolean;
|
||||
isVOD: boolean;
|
||||
job_id: string;
|
||||
language: string;
|
||||
local_filename: string;
|
||||
lowRc: boolean;
|
||||
needsBlockchainUpdate: boolean;
|
||||
originalFilename: string;
|
||||
owner: string;
|
||||
paid: boolean;
|
||||
permlink: string;
|
||||
postToHiveBlog: boolean;
|
||||
publish_type: string;
|
||||
reducedUpvote: boolean;
|
||||
rewardPowerup: boolean;
|
||||
size: number;
|
||||
status: ThreeSpeakStatus;
|
||||
tags_v2: unknown[];
|
||||
thumbUrl: string;
|
||||
thumbnail: string;
|
||||
title: string;
|
||||
updateSteem: boolean;
|
||||
upload_type: string;
|
||||
upvoteEligible: boolean;
|
||||
video_v2: string;
|
||||
views: number;
|
||||
votePercent: number;
|
||||
width: unknown;
|
||||
__v: number;
|
||||
_id: string;
|
||||
}
|
@ -1,4 +1,8 @@
|
||||
import { SET_BENEFICIARIES, REMOVE_BENEFICIARIES } from '../constants/constants';
|
||||
import {
|
||||
SET_BENEFICIARIES,
|
||||
REMOVE_BENEFICIARIES,
|
||||
SET_ALLOW_SPK_PUBLISHING,
|
||||
} from '../constants/constants';
|
||||
import { Beneficiary } from '../reducers/editorReducer';
|
||||
|
||||
export const setBeneficiaries = (draftId: string, benficiaries: Beneficiary[]) => ({
|
||||
@ -15,3 +19,8 @@ export const removeBeneficiaries = (draftId: string) => ({
|
||||
},
|
||||
type: REMOVE_BENEFICIARIES,
|
||||
});
|
||||
|
||||
export const setAllowSpkPublishing = (allowSpkPublishing: boolean) => ({
|
||||
payload: allowSpkPublishing,
|
||||
type: SET_ALLOW_SPK_PUBLISHING,
|
||||
});
|
||||
|
@ -117,6 +117,7 @@ export const SET_OWN_PROFILE_TABS = 'SET_OWN_PROFILE_TABS';
|
||||
export const SET_BENEFICIARIES = 'SET_BENEFICIARIES';
|
||||
export const REMOVE_BENEFICIARIES = 'REMOVE_BENEFICIARIES';
|
||||
export const TEMP_BENEFICIARIES_ID = 'temp-beneficiaries';
|
||||
export const SET_ALLOW_SPK_PUBLISHING = 'SET_ALLOW_SPK_PUBLISHING';
|
||||
|
||||
// CACHE
|
||||
export const PURGE_EXPIRED_CACHE = 'PURGE_EXPIRED_CACHE';
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { REMOVE_BENEFICIARIES, SET_BENEFICIARIES } from '../constants/constants';
|
||||
import {
|
||||
REMOVE_BENEFICIARIES,
|
||||
SET_BENEFICIARIES,
|
||||
SET_ALLOW_SPK_PUBLISHING,
|
||||
} from '../constants/constants';
|
||||
|
||||
export interface Beneficiary {
|
||||
account: string;
|
||||
@ -11,10 +15,12 @@ interface State {
|
||||
beneficiariesMap: {
|
||||
[key: string]: Beneficiary[];
|
||||
};
|
||||
allowSpkPublishing: boolean;
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
beneficiariesMap: {},
|
||||
allowSpkPublishing: false,
|
||||
};
|
||||
|
||||
const editorReducer = (state = initialState, action) => {
|
||||
@ -30,6 +36,11 @@ const editorReducer = (state = initialState, action) => {
|
||||
return {
|
||||
...state, // spread operator in requried here, otherwise persist do not register change
|
||||
};
|
||||
case SET_ALLOW_SPK_PUBLISHING:
|
||||
return {
|
||||
...state,
|
||||
allowSpkPublishing: payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { View } from 'react-native';
|
||||
import Animated, { FlipInEasyX } from 'react-native-reanimated';
|
||||
@ -13,6 +13,9 @@ import {
|
||||
import styles from './postOptionsModalStyles';
|
||||
import ThumbSelectionContent from './thumbSelectionContent';
|
||||
import PostDescription from './postDescription';
|
||||
import { useSpeakContentBuilder } from '../../../providers/queries/editorQueries/speakQueries';
|
||||
import { DEFAULT_SPEAK_BENEFICIARIES } from '../../../providers/speak/constants';
|
||||
import { Beneficiary } from '../../../redux/reducers/editorReducer';
|
||||
|
||||
const REWARD_TYPES = [
|
||||
{
|
||||
@ -71,6 +74,7 @@ const PostOptionsModal = forwardRef(
|
||||
ref,
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const speakContentBuilder = useSpeakContentBuilder();
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [rewardTypeIndex, setRewardTypeIndex] = useState(0);
|
||||
@ -79,8 +83,28 @@ const PostOptionsModal = forwardRef(
|
||||
const [scheduledFor, setScheduledFor] = useState('');
|
||||
const [disableDone, setDisableDone] = useState(false);
|
||||
|
||||
// removed the useeffect causing index reset bug
|
||||
const { encodingBeneficiaries, videoThumbUrls } = useMemo(() => {
|
||||
let benefs: Beneficiary[] = [];
|
||||
if (body && showModal) {
|
||||
speakContentBuilder.build(body);
|
||||
const unpublishedMeta = speakContentBuilder.videoPublishMetaRef.current;
|
||||
if (unpublishedMeta) {
|
||||
const vidBeneficiaries = JSON.parse(unpublishedMeta.beneficiaries || '[]');
|
||||
benefs = [...DEFAULT_SPEAK_BENEFICIARIES, ...vidBeneficiaries];
|
||||
}
|
||||
|
||||
return {
|
||||
videoThumbUrls: speakContentBuilder.thumbUrlsRef.current,
|
||||
encodingBeneficiaries: benefs,
|
||||
};
|
||||
}
|
||||
return {
|
||||
videoThumbUrls: [],
|
||||
encodingBeneficiaries: benefs,
|
||||
};
|
||||
}, [showModal, body]);
|
||||
|
||||
// removed the useeffect causing index reset bug
|
||||
useEffect(() => {
|
||||
if (!scheduleLater) {
|
||||
handleScheduleChange(null);
|
||||
@ -196,6 +220,7 @@ const PostOptionsModal = forwardRef(
|
||||
<ThumbSelectionContent
|
||||
body={body}
|
||||
thumbUrl={thumbUrl}
|
||||
videoThumbUrls={videoThumbUrls}
|
||||
isUploading={isUploading}
|
||||
onThumbSelection={_handleThumbIndexSelection}
|
||||
/>
|
||||
@ -205,7 +230,11 @@ const PostOptionsModal = forwardRef(
|
||||
/>
|
||||
|
||||
{!isEdit && (
|
||||
<BeneficiarySelectionContent draftId={draftId} setDisableDone={setDisableDone} />
|
||||
<BeneficiarySelectionContent
|
||||
draftId={draftId}
|
||||
setDisableDone={setDisableDone}
|
||||
encodingBeneficiaries={encodingBeneficiaries}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import { getBottomSpace } from 'react-native-iphone-x-helper';
|
||||
|
||||
export default EStyleSheet.create({
|
||||
sheetContent: {
|
||||
|
@ -13,6 +13,7 @@ import { Icon } from '../../../components';
|
||||
interface ThumbSelectionContentProps {
|
||||
body: string;
|
||||
thumbUrl: string;
|
||||
videoThumbUrls: string[];
|
||||
isUploading: boolean;
|
||||
onThumbSelection: (url: string) => void;
|
||||
}
|
||||
@ -20,6 +21,7 @@ interface ThumbSelectionContentProps {
|
||||
const ThumbSelectionContent = ({
|
||||
body,
|
||||
thumbUrl,
|
||||
videoThumbUrls,
|
||||
onThumbSelection,
|
||||
isUploading,
|
||||
}: ThumbSelectionContentProps) => {
|
||||
@ -30,7 +32,7 @@ const ThumbSelectionContent = ({
|
||||
const [thumbIndex, setThumbIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const urls = extractImageUrls({ body });
|
||||
const urls = [...extractImageUrls({ body }), ...videoThumbUrls];
|
||||
|
||||
if (urls.length < 2) {
|
||||
setNeedMore(true);
|
||||
|
@ -10,8 +10,9 @@ import { isArray } from 'lodash';
|
||||
import { Buffer } from 'buffer';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
|
||||
import { postBodySummary } from '@ecency/render-helper';
|
||||
import { addDraft, updateDraft, getDrafts, addSchedule } from '../../../providers/ecency/ecency';
|
||||
import { toastNotification, setRcOffer } from '../../../redux/actions/uiAction';
|
||||
import { toastNotification, setRcOffer, showActionModal } from '../../../redux/actions/uiAction';
|
||||
import {
|
||||
postContent,
|
||||
getPurePost,
|
||||
@ -32,12 +33,16 @@ import {
|
||||
extractMetadata,
|
||||
makeJsonMetadataForUpdate,
|
||||
createPatch,
|
||||
extract3SpeakIds,
|
||||
} from '../../../utils/editor';
|
||||
// import { generateSignature } from '../../../utils/image';
|
||||
|
||||
// Component
|
||||
import EditorScreen from '../screen/editorScreen';
|
||||
import { removeBeneficiaries, setBeneficiaries } from '../../../redux/actions/editorActions';
|
||||
import {
|
||||
removeBeneficiaries,
|
||||
setAllowSpkPublishing,
|
||||
setBeneficiaries,
|
||||
} from '../../../redux/actions/editorActions';
|
||||
import { DEFAULT_USER_DRAFT_ID, TEMP_BENEFICIARIES_ID } from '../../../redux/constants/constants';
|
||||
import {
|
||||
deleteDraftCacheEntry,
|
||||
@ -50,7 +55,12 @@ import { useUserActivityMutation } from '../../../providers/queries/pointQueries
|
||||
import { PointActivityIds } from '../../../providers/ecency/ecency.types';
|
||||
import { usePostsCachePrimer } from '../../../providers/queries/postQueries/postQueries';
|
||||
import { PostTypes } from '../../../constants/postTypes';
|
||||
import { postBodySummary } from '@ecency/render-helper';
|
||||
import { speakQueries } from '../../../providers/queries';
|
||||
import {
|
||||
BENEFICIARY_SRC_ENCODER,
|
||||
DEFAULT_SPEAK_BENEFICIARIES,
|
||||
} from '../../../providers/speak/constants';
|
||||
import { ThreeSpeakVideo } from '../../../providers/speak/speak.types';
|
||||
|
||||
/*
|
||||
* Props Name Description Value
|
||||
@ -96,7 +106,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
// Component Life Cycle Functions
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
const { currentAccount, route, queryClient } = this.props;
|
||||
const { currentAccount, route, queryClient, dispatch } = this.props;
|
||||
const username = currentAccount && currentAccount.name ? currentAccount.name : '';
|
||||
let isReply;
|
||||
let draftId;
|
||||
@ -192,6 +202,9 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
this._requestKeyboardFocus();
|
||||
|
||||
this._appStateSub = AppState.addEventListener('change', this._handleAppStateChange);
|
||||
|
||||
// dispatch spk publishing status
|
||||
dispatch(setAllowSpkPublishing(!isReply && !isEdit));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<any>, prevState: Readonly<any>): void {
|
||||
@ -305,6 +318,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
const filteredBeneficiaries = draft.meta.beneficiaries.filter(
|
||||
(item) => item.account !== currentAccount.username,
|
||||
); // remove default beneficiary from array while saving
|
||||
|
||||
dispatch(setBeneficiaries(draft._id || TEMP_BENEFICIARIES_ID, filteredBeneficiaries));
|
||||
}
|
||||
}
|
||||
@ -398,18 +412,14 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
|
||||
_extractBeneficiaries = () => {
|
||||
const { draftId } = this.state;
|
||||
const { beneficiariesMap, currentAccount } = this.props;
|
||||
const { beneficiariesMap } = this.props;
|
||||
|
||||
return (
|
||||
beneficiariesMap[draftId || TEMP_BENEFICIARIES_ID] || [
|
||||
{ account: currentAccount.name, weight: 10000 },
|
||||
]
|
||||
);
|
||||
return beneficiariesMap[draftId || TEMP_BENEFICIARIES_ID] || [];
|
||||
};
|
||||
|
||||
_saveDraftToDB = async (fields, saveAsNew = false) => {
|
||||
const { isDraftSaved, draftId, thumbUrl, isReply, rewardType, postDescription } = this.state;
|
||||
const { currentAccount, dispatch, intl, queryClient } = this.props;
|
||||
const { currentAccount, dispatch, intl, queryClient, speakContentBuilder } = this.props;
|
||||
|
||||
try {
|
||||
// saves draft locallly
|
||||
@ -423,6 +433,8 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
return;
|
||||
}
|
||||
|
||||
speakContentBuilder.build(fields.body);
|
||||
|
||||
const beneficiaries = this._extractBeneficiaries();
|
||||
const postBodySummaryContent = postBodySummary(
|
||||
get(fields, 'body', ''),
|
||||
@ -450,13 +462,28 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
const _extractedMeta = await extractMetadata({
|
||||
body: draftField.body,
|
||||
thumbUrl,
|
||||
videoThumbUrls: speakContentBuilder.thumbUrlsRef.current,
|
||||
fetchRatios: false,
|
||||
});
|
||||
|
||||
// inject video meta for draft
|
||||
const speakIds = extract3SpeakIds({ body: draftField.body });
|
||||
const videos: any = {};
|
||||
const videosCache: any = queryClient.getQueryData([QUERIES.MEDIA.GET_VIDEOS]);
|
||||
|
||||
speakIds.forEach((_id) => {
|
||||
const videoItem = videosCache.find((item) => item._id === _id);
|
||||
if (videoItem?.speakData) {
|
||||
videos[_id] = videoItem.speakData;
|
||||
}
|
||||
});
|
||||
|
||||
const meta = Object.assign({}, _extractedMeta, {
|
||||
tags: draftField.tags,
|
||||
beneficiaries,
|
||||
rewardType,
|
||||
description: postDescription ? postDescription : postBodySummaryContent,
|
||||
description: postDescription || postBodySummaryContent,
|
||||
videos: Object.keys(videos).length > 0 && videos,
|
||||
});
|
||||
|
||||
const jsonMeta = makeJsonMetadata(meta, draftField.tags);
|
||||
@ -574,7 +601,13 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
}
|
||||
};
|
||||
|
||||
_submitPost = async ({ fields, scheduleDate }: { fields: any; scheduleDate?: string }) => {
|
||||
_submitPost = async ({
|
||||
fields: _fieldsBase,
|
||||
scheduleDate,
|
||||
}: {
|
||||
fields: any;
|
||||
scheduleDate?: string;
|
||||
}) => {
|
||||
const {
|
||||
currentAccount,
|
||||
dispatch,
|
||||
@ -582,27 +615,69 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
navigation,
|
||||
pinCode,
|
||||
userActivityMutation,
|
||||
// isDefaultFooter,
|
||||
speakContentBuilder,
|
||||
speakMutations,
|
||||
} = this.props;
|
||||
const { rewardType, isPostSending, thumbUrl, draftId, shouldReblog } = this.state;
|
||||
|
||||
const beneficiaries = this._extractBeneficiaries();
|
||||
const fields = Object.assign({}, _fieldsBase);
|
||||
let beneficiaries = this._extractBeneficiaries();
|
||||
let videoPublishMeta: ThreeSpeakVideo | undefined = undefined;
|
||||
|
||||
if (isPostSending) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentAccount) {
|
||||
// build speak video body
|
||||
try {
|
||||
fields.body = speakContentBuilder.build(fields.body);
|
||||
videoPublishMeta = speakContentBuilder.videoPublishMetaRef.current;
|
||||
|
||||
// verify and make video beneficiaries redundent
|
||||
beneficiaries = beneficiaries.filter((item) => item.src !== BENEFICIARY_SRC_ENCODER);
|
||||
if (videoPublishMeta) {
|
||||
const encoderBene = [
|
||||
...JSON.parse(videoPublishMeta.beneficiaries || '[]'),
|
||||
...DEFAULT_SPEAK_BENEFICIARIES,
|
||||
];
|
||||
beneficiaries = [...encoderBene, ...beneficiaries];
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('fail', err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (scheduleDate && videoPublishMeta) {
|
||||
dispatch(
|
||||
showActionModal({
|
||||
title: intl.formatMessage({ id: 'alert.notice' }),
|
||||
body: intl.formatMessage({ id: 'editor.schedule_video_unsupported' }),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isPostSending: true,
|
||||
});
|
||||
|
||||
const meta = await extractMetadata({ body: fields.body, thumbUrl, fetchRatios: true });
|
||||
// only require video meta for unpublished video, it will always be one
|
||||
const meta = await extractMetadata({
|
||||
body: fields.body,
|
||||
thumbUrl,
|
||||
videoThumbUrls: speakContentBuilder.thumbUrlsRef.current,
|
||||
fetchRatios: true,
|
||||
videoPublishMeta,
|
||||
});
|
||||
const _tags = fields.tags.filter((tag) => tag && tag !== ' ');
|
||||
|
||||
const jsonMeta = makeJsonMetadata(meta, _tags);
|
||||
|
||||
// TODO: check if permlink is available github: #314 https://github.com/ecency/ecency-mobile/pull/314
|
||||
let permlink = generatePermlink(fields.title || '');
|
||||
let permlink = videoPublishMeta
|
||||
? videoPublishMeta.permlink
|
||||
: generatePermlink(fields.title || '');
|
||||
|
||||
let dublicatePost;
|
||||
try {
|
||||
@ -629,6 +704,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
if (fields.tags.length === 0) {
|
||||
fields.tags = ['hive-125125'];
|
||||
}
|
||||
|
||||
this._setScheduledPost({
|
||||
author,
|
||||
permlink,
|
||||
@ -651,6 +727,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
)
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
|
||||
// track user activity for points
|
||||
userActivityMutation.mutate({
|
||||
pointsTy: PointActivityIds.POST,
|
||||
@ -673,6 +750,18 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
});
|
||||
}
|
||||
|
||||
// mark unpublished video as published on 3speak if that is the case
|
||||
if (videoPublishMeta) {
|
||||
console.log('marking inserted video as published');
|
||||
speakMutations.updateInfoMutation.mutate({
|
||||
id: videoPublishMeta._id,
|
||||
title: fields.title,
|
||||
body: fields.body,
|
||||
tags: fields.tags,
|
||||
});
|
||||
speakMutations.markAsPublishedMutation.mutate(videoPublishMeta._id);
|
||||
}
|
||||
|
||||
// post publish updates
|
||||
dispatch(deleteDraftCacheEntry(DEFAULT_USER_DRAFT_ID + currentAccount.name));
|
||||
|
||||
@ -711,8 +800,14 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
};
|
||||
|
||||
_submitReply = async (fields) => {
|
||||
const { currentAccount, pinCode, dispatch, userActivityMutation, draftsCollection } =
|
||||
this.props;
|
||||
const {
|
||||
currentAccount,
|
||||
pinCode,
|
||||
dispatch,
|
||||
userActivityMutation,
|
||||
draftsCollection,
|
||||
speakContentBuilder,
|
||||
} = this.props;
|
||||
const { isPostSending } = this.state;
|
||||
|
||||
if (isPostSending) {
|
||||
@ -726,6 +821,8 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
|
||||
const { post } = this.state;
|
||||
|
||||
fields.body = speakContentBuilder.build(fields.body);
|
||||
|
||||
const _prefix = `re-${post.author.replace(/\./g, '')}`;
|
||||
const permlink = generateUniquePermlink(_prefix);
|
||||
|
||||
@ -790,7 +887,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
};
|
||||
|
||||
_submitEdit = async (fields) => {
|
||||
const { currentAccount, pinCode, dispatch, postCachePrimer } = this.props;
|
||||
const { currentAccount, pinCode, dispatch, postCachePrimer, speakContentBuilder } = this.props;
|
||||
const { post, isEdit, isPostSending, thumbUrl, isReply } = this.state;
|
||||
|
||||
if (isPostSending) {
|
||||
@ -801,6 +898,10 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
this.setState({
|
||||
isPostSending: true,
|
||||
});
|
||||
|
||||
// build speak video body
|
||||
fields.body = speakContentBuilder.build(fields.body);
|
||||
|
||||
const { tags, body, title } = fields;
|
||||
const {
|
||||
markdownBody: oldBody,
|
||||
@ -817,7 +918,12 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
newBody = patch;
|
||||
}
|
||||
|
||||
const meta = await extractMetadata({ body: fields.body, thumbUrl, fetchRatios: true });
|
||||
const meta = await extractMetadata({
|
||||
body: fields.body,
|
||||
videoThumbUrls: speakContentBuilder.thumbUrlsRef.current,
|
||||
thumbUrl,
|
||||
fetchRatios: true,
|
||||
});
|
||||
|
||||
let jsonMeta = {};
|
||||
|
||||
@ -1216,6 +1322,8 @@ const mapStateToProps = (state) => ({
|
||||
|
||||
const mapQueriesToProps = () => ({
|
||||
queryClient: useQueryClient(),
|
||||
speakContentBuilder: speakQueries.useSpeakContentBuilder(),
|
||||
speakMutations: speakQueries.useSpeakMutations(),
|
||||
userActivityMutation: useUserActivityMutation(),
|
||||
postCachePrimer: usePostsCachePrimer(),
|
||||
});
|
||||
|
@ -495,6 +495,7 @@ class EditorScreen extends Component {
|
||||
/>
|
||||
)}
|
||||
<MarkdownEditor
|
||||
draftId={draftId}
|
||||
paramFiles={paramFiles}
|
||||
componentID="body"
|
||||
draftBody={fields && fields.body}
|
||||
|
@ -4,6 +4,7 @@ import { Image } from 'react-native';
|
||||
import VersionNumber from 'react-native-version-number';
|
||||
import getSlug from 'speakingurl';
|
||||
import { PostTypes } from '../constants/postTypes';
|
||||
import { ThreeSpeakVideo } from '../providers/speak/speak.types';
|
||||
|
||||
export const getWordsCount = (text) =>
|
||||
text && typeof text === 'string' ? text.replace(/^\s+|\s+$/g, '').split(/\s+/).length : 0;
|
||||
@ -185,6 +186,20 @@ export const extractImageUrls = ({ body, urls }: { body?: string; urls?: string[
|
||||
return imgUrls;
|
||||
};
|
||||
|
||||
export const extract3SpeakIds = ({ body }) => {
|
||||
if (!body) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const regex = /\[3speak]\((.*?)\)/g;
|
||||
const matches = [...body.matchAll(regex)];
|
||||
|
||||
const ids = matches.map((match) => match[1]);
|
||||
console.log(ids);
|
||||
|
||||
return ids;
|
||||
};
|
||||
|
||||
export const extractFilenameFromPath = ({
|
||||
path,
|
||||
mimeType,
|
||||
@ -214,20 +229,24 @@ export const extractFilenameFromPath = ({
|
||||
export const extractMetadata = async ({
|
||||
body,
|
||||
thumbUrl,
|
||||
videoThumbUrls,
|
||||
fetchRatios,
|
||||
postType,
|
||||
videoPublishMeta,
|
||||
}: {
|
||||
body: string;
|
||||
thumbUrl?: string;
|
||||
videoThumbUrls: string[];
|
||||
fetchRatios?: boolean;
|
||||
postType?: PostTypes;
|
||||
videoPublishMeta?: ThreeSpeakVideo;
|
||||
}) => {
|
||||
// NOTE: keepting regex to extract usernames as reference for later usage if any
|
||||
// const userReg = /(^|\s)(@[a-z][-.a-z\d]+[a-z\d])/gim;
|
||||
|
||||
const out = {};
|
||||
const out: any = {};
|
||||
const mUrls = extractUrls(body);
|
||||
const matchedImages = extractImageUrls({ urls: mUrls });
|
||||
const matchedImages = [...extractImageUrls({ urls: mUrls }), ...(videoThumbUrls || [])];
|
||||
|
||||
if (matchedImages.length) {
|
||||
if (thumbUrl) {
|
||||
@ -257,6 +276,41 @@ export const extractMetadata = async ({
|
||||
);
|
||||
}
|
||||
|
||||
// insert three speak meta
|
||||
if (videoPublishMeta) {
|
||||
out.video = {
|
||||
info: {
|
||||
platform: '3speak',
|
||||
title: videoPublishMeta.title,
|
||||
author: videoPublishMeta.owner,
|
||||
permlink: videoPublishMeta.permlink,
|
||||
duration: videoPublishMeta.duration,
|
||||
filesize: videoPublishMeta.size,
|
||||
file: videoPublishMeta.filename,
|
||||
lang: videoPublishMeta.language,
|
||||
firstUpload: videoPublishMeta.firstUpload,
|
||||
ipfs: null,
|
||||
ipfsThumbnail: null,
|
||||
video_v2: videoPublishMeta.video_v2,
|
||||
sourceMap: [
|
||||
{
|
||||
type: 'video',
|
||||
url: videoPublishMeta.video_v2,
|
||||
format: 'm3u8',
|
||||
},
|
||||
{
|
||||
type: 'thumbnail',
|
||||
url: videoPublishMeta.thumbUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
content: {
|
||||
description: videoPublishMeta.description,
|
||||
tags: videoPublishMeta.tags_v2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// setting post type, primary usecase for separating waves from other posts
|
||||
out.type = postType || PostTypes.POST;
|
||||
|
||||
|
35
yarn.lock
35
yarn.lock
@ -5073,6 +5073,11 @@ flatted@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
|
||||
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
|
||||
|
||||
flatted@^3.2.4:
|
||||
version "3.2.9"
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf"
|
||||
integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==
|
||||
|
||||
flow-parser@0.*:
|
||||
version "0.201.0"
|
||||
resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.201.0.tgz#d2005d4dae6fddf60d30f9ae0fb49a13c9c51cfe"
|
||||
@ -5551,10 +5556,10 @@ hive-uri@^0.2.5:
|
||||
resolved "https://registry.yarnpkg.com/hive-uri/-/hive-uri-0.2.5.tgz#11f94d43b87d22c2a9c018e6ed41d303d3e0c770"
|
||||
integrity sha512-oSFqa39/Frs8XaA6oAT1W/5KtkInfp8u0Yv58YGok1klu7M8rPYMg3v9m/0rdvrRLSheAYwip88l60at2kPtYA==
|
||||
|
||||
hivesigner@^3.2.7:
|
||||
version "3.2.9"
|
||||
resolved "https://registry.yarnpkg.com/hivesigner/-/hivesigner-3.2.9.tgz#c0b670d7c47fe04516b8986fbe2c8b5f645eaaeb"
|
||||
integrity sha512-VHJTW4FD6O8ZBdFWDkOLOnC2s5rjIOoDK9Pz4RK+mbs9/l1AyF2K6Z+bVZGzSluRsc7fjThweLg4EuPEVbEQYw==
|
||||
hivesigner@^3.3.4:
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/hivesigner/-/hivesigner-3.3.4.tgz#5ab19f25821eacb6683d86ee098e3868f028497e"
|
||||
integrity sha512-n+fpsmc3FvkwXwhSTHJgChq97YeL2bEcJz/Awn9o+boFnIxqrxg4Y3i4sMMR8j+kJnwDV9c5JIMF4zQVxPSPJQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.9"
|
||||
cross-fetch "^3.0.6"
|
||||
@ -8926,6 +8931,11 @@ react-native-background-timer@^2.4.1:
|
||||
resolved "https://registry.yarnpkg.com/react-native-background-timer/-/react-native-background-timer-2.4.1.tgz#a3bc1cafa8c1e3aeefd0611de120298b67978a0f"
|
||||
integrity sha512-TE4Kiy7jUyv+hugxDxitzu38sW1NqjCk4uE5IgU2WevLv7sZacaBc6PZKOShNRPGirLl1NWkaG3LDEkdb9Um5g==
|
||||
|
||||
react-native-background-upload@^6.6.0:
|
||||
version "6.6.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-background-upload/-/react-native-background-upload-6.6.0.tgz#7c081f4260d16e7a416522c3622b81938ad799c8"
|
||||
integrity sha512-adfOJmeO3GmPmc53cdHYWp5eTGKagk/AhkraSoQ89BHRwBWrtRQ29NN9sm2CLezBYrSQWg0W47C8TaowtYZ1LQ==
|
||||
|
||||
react-native-blob-util@^0.16.0:
|
||||
version "0.16.4"
|
||||
resolved "https://registry.yarnpkg.com/react-native-blob-util/-/react-native-blob-util-0.16.4.tgz#eeb0e28f6fa6ecb357c10f154be3d2e66c010f62"
|
||||
@ -8984,6 +8994,11 @@ react-native-config@^1.5.1:
|
||||
resolved "https://registry.yarnpkg.com/react-native-config/-/react-native-config-1.5.1.tgz#73c94f511493e9b7ff9350cdf351d203a1b05acc"
|
||||
integrity sha512-g1xNgt1tV95FCX+iWz6YJonxXkQX0GdD3fB8xQtR1GUBEqweB9zMROW77gi2TygmYmUkBI7LU4pES+zcTyK4HA==
|
||||
|
||||
react-native-create-thumbnail@^1.6.4:
|
||||
version "1.6.4"
|
||||
resolved "https://registry.yarnpkg.com/react-native-create-thumbnail/-/react-native-create-thumbnail-1.6.4.tgz#90f5b0a587de6e3738a7632fe3d9a9624ed83581"
|
||||
integrity sha512-JWuKXswDXtqUPfuqh6rjCVMvTSSG3kUtwvSK/YdaNU0i+nZKxeqHmt/CO2+TyI/WSUFynGVmWT1xOHhCZAFsRQ==
|
||||
|
||||
react-native-crypto-js@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-crypto-js/-/react-native-crypto-js-1.0.0.tgz#e677e022e147f41b35614416c92d655f87e2450a"
|
||||
@ -9348,6 +9363,11 @@ react-native-tcp@^4.0.0:
|
||||
process "^0.11.9"
|
||||
util "^0.10.3"
|
||||
|
||||
react-native-tus-client@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-tus-client/-/react-native-tus-client-1.1.0.tgz#5d1ba7e79a1b0e131426cb9ae0a502fb695730c4"
|
||||
integrity sha512-rs5/JNbCWLoQZE3x/faa6+mSXAz21q75lLGC5yyJe6L4jylhBHNbJUsapvnpo7mbcOMvuai0aoTcSzGyqQ9kVQ==
|
||||
|
||||
react-native-udp@^4.1.4:
|
||||
version "4.1.7"
|
||||
resolved "https://registry.yarnpkg.com/react-native-udp/-/react-native-udp-4.1.7.tgz#9fa90b772b44c991605e8191444dd2ca3259cb58"
|
||||
@ -9467,6 +9487,13 @@ react-navigation-redux-helpers@^4.0.1:
|
||||
dependencies:
|
||||
invariant "^2.2.2"
|
||||
|
||||
react-query-native-devtools@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-query-native-devtools/-/react-query-native-devtools-4.0.0.tgz#97d484d024503d484652f6e64a07c188f99ae702"
|
||||
integrity sha512-TKwYO0tcw744c2aI1AGL2zoLycx3mr6Apl/3p9EIxh204kWGBpF7OhPQY7OKSdPvgw1jG6wlKzfAj3asgCSZGg==
|
||||
dependencies:
|
||||
flatted "^3.2.4"
|
||||
|
||||
react-redux@^8.0.4:
|
||||
version "8.0.5"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd"
|
||||
|
Loading…
Reference in New Issue
Block a user