mirror of
https://github.com/ecency/ecency-mobile.git
synced 2024-11-25 11:15:21 +03:00
Merge remote-tracking branch 'origin/development' into nt/comment-modal
# Conflicts: # src/screens/editor/container/editorContainer.tsx
This commit is contained in:
commit
c8f9b7c0f6
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
50
src/assets/svgs/hive-signer-icon.tsx
Normal file
50
src/assets/svgs/hive-signer-icon.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { SvgXml } from 'react-native-svg';
|
||||
|
||||
const xml = `
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg639"
|
||||
version="1.1"
|
||||
viewBox="0 0 110 131"
|
||||
height="131px"
|
||||
width="110px">
|
||||
<metadata
|
||||
id="metadata645">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>steemconnect</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs643" />
|
||||
<title
|
||||
id="title631">hivesigner</title>
|
||||
<g
|
||||
id="g637"
|
||||
fillRule="evenodd"
|
||||
fill="none"
|
||||
strokeWidth="1"
|
||||
stroke="none">
|
||||
<g
|
||||
style="fill:#e31337;fill-opacity:1"
|
||||
id="g635"
|
||||
fill="#FFFFFF">
|
||||
<path
|
||||
style="fill:#e31337;fill-opacity:1"
|
||||
id="path633"
|
||||
d="M98.5416667,69.9233063 L98.5416667,45.8870172 L0,45.8870172 L0,0 L110,0 L110,22.9435086 L98.5416667,22.9435086 L98.5416667,11.4717543 L11.4583333,11.4717543 L11.4583333,34.4152629 L110,34.4152629 L110,71.7557407 C109.814829,89.9952801 99.6177682,104.956632 82.7527664,116.860728 C76.7961987,121.065148 70.4063912,124.587232 64.0012261,127.457262 C61.7434474,128.468929 59.6383537,129.328634 57.7397369,130.039339 C56.5694641,130.477405 55.6341636,130.79975 55.217168,130.930175 L54.9939202,131 L54.770964,130.929247 C54.3038614,130.781016 53.4195598,130.475943 52.2525224,130.038871 C50.3537481,129.327755 48.2495815,128.46815 45.9926252,127.45661 C39.5881896,124.586219 33.1997658,121.06434 27.2442254,116.860148 C10.5407495,105.06867 0.379911508,90.2779437 0.0104347976,72.2720521 L0,72.2720521 L0,57.3587715 L11.4583333,57.3587715 L11.4583333,69.9233063 C11.4583333,83.7531618 18.6794796,95.3214487 31.0665488,104.779305 C35.6407321,108.271816 40.6776939,111.281674 45.8998806,113.824407 C49.3619815,115.510138 52.4791717,116.790221 55.00154,117.681377 C57.521117,116.791558 60.639999,115.510854 64.1022301,113.825035 C69.3240535,111.28244 74.3609709,108.272494 78.9351124,104.77987 C91.3219921,95.3217663 98.5416667,83.7531543 98.5416667,69.9233063 Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
export default () => <SvgXml xml={xml} width={20} height={20} />;
|
1
src/assets/svgs/index.ts
Normal file
1
src/assets/svgs/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as HiveSignerIcon } from './hive-signer-icon';
|
@ -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) {
|
||||
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;
|
||||
tempBeneficiaries.forEach((item, index) => {
|
||||
savedBeneficiareis.forEach((item, index) => {
|
||||
if (index > 0) {
|
||||
othersWeight += item.weight;
|
||||
}
|
||||
});
|
||||
tempBeneficiaries[0].weight = 10000 - othersWeight;
|
||||
setBeneficiaries(tempBeneficiaries);
|
||||
}
|
||||
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"
|
||||
|
@ -201,7 +201,7 @@ const FormInputView = ({
|
||||
) : value && value.length > 0 ? (
|
||||
<Icon
|
||||
iconType={iconType || 'MaterialIcons'}
|
||||
onPress={() => setValue('')}
|
||||
onPress={() => _handleOnChange('')}
|
||||
name={leftIconName}
|
||||
style={styles.icon}
|
||||
/>
|
||||
|
@ -103,6 +103,7 @@ import TransferAccountSelector from './transferAccountSelector/transferAccountSe
|
||||
import TransferAmountInputSection from './transferAmountInputSection/transferAmountInputSection';
|
||||
import TextBoxWithCopy from './textBoxWithCopy/textBoxWithCopy';
|
||||
import WebViewModal from './webViewModal/webViewModal';
|
||||
import OrDivider from './orDivider/orDividerView';
|
||||
|
||||
// Basic UI Elements
|
||||
import {
|
||||
@ -253,4 +254,5 @@ export {
|
||||
TransferAmountInputSection,
|
||||
TextBoxWithCopy,
|
||||
WebViewModal,
|
||||
OrDivider,
|
||||
};
|
||||
|
@ -4,7 +4,6 @@ export default EStyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
height: '$deviceHeight / 3',
|
||||
backgroundColor: '$primaryBackgroundColor',
|
||||
},
|
||||
safeArea: {
|
||||
@ -15,7 +14,7 @@ export default EStyleSheet.create({
|
||||
maxHeight: '$deviceHeight / 3',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '$primaryBackgroundColor',
|
||||
height: '$deviceHeight / 3.9',
|
||||
height: 120,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
@ -37,12 +36,10 @@ export default EStyleSheet.create({
|
||||
alignItems: 'center',
|
||||
},
|
||||
mascot: {
|
||||
width: '70%',
|
||||
height: '70%',
|
||||
width: '60%',
|
||||
},
|
||||
titleText: {
|
||||
alignSelf: 'center',
|
||||
marginTop: 20,
|
||||
marginLeft: 32,
|
||||
marginRight: 12,
|
||||
flex: 1,
|
||||
@ -54,9 +51,15 @@ export default EStyleSheet.create({
|
||||
backgroundColor: '$primaryBackgroundColor',
|
||||
paddingVertical: 8,
|
||||
},
|
||||
backIconContainer: {
|
||||
marginLeft: 20,
|
||||
},
|
||||
backIcon: {
|
||||
fontSize: 24,
|
||||
color: '$iconColor',
|
||||
},
|
||||
logoContainer: {
|
||||
paddingLeft: 32,
|
||||
paddingRight: 8,
|
||||
paddingRight: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
@ -4,11 +4,11 @@ import * as Animatable from 'react-native-animatable';
|
||||
// Constants
|
||||
|
||||
// Components
|
||||
import { TextButton } from '../../buttons';
|
||||
import { LineBreak } from '../../basicUIElements';
|
||||
// Styles
|
||||
import styles from './loginHeaderStyles';
|
||||
import getWindowDimensions from '../../../utils/getWindowDimensions';
|
||||
import { IconButton } from '../..';
|
||||
|
||||
class LoginHeaderView extends PureComponent {
|
||||
/* Props
|
||||
@ -27,24 +27,18 @@ class LoginHeaderView extends PureComponent {
|
||||
// Component Functions
|
||||
|
||||
render() {
|
||||
const { description, isKeyboardOpen, onPress, rightButtonText, title } = this.props;
|
||||
const { description, isKeyboardOpen, title, onBackPress } = this.props;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<View styles={styles.container}>
|
||||
<View style={styles.headerRow}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Image
|
||||
resizeMode="contain"
|
||||
style={styles.logo}
|
||||
source={require('../../../assets/ecency_logo_transparent.png')}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.headerButton}>
|
||||
<TextButton
|
||||
onPress={onPress}
|
||||
text={rightButtonText}
|
||||
textStyle={{ color: '#357ce6' }}
|
||||
<View style={styles.backIconContainer}>
|
||||
<IconButton
|
||||
iconStyle={styles.backIcon}
|
||||
iconType="MaterialIcons"
|
||||
name="close"
|
||||
onPress={onBackPress}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@ -61,7 +55,7 @@ class LoginHeaderView extends PureComponent {
|
||||
<Image
|
||||
resizeMode="contain"
|
||||
style={styles.mascot}
|
||||
source={require('../../../assets/love_mascot.png')}
|
||||
source={require('../../../assets/ecency_logo_transparent.png')}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@ -76,7 +70,7 @@ class LoginHeaderView extends PureComponent {
|
||||
export default LoginHeaderView;
|
||||
|
||||
const { height } = getWindowDimensions();
|
||||
const bodyHeight = height / 3.9;
|
||||
const bodyHeight = 120;
|
||||
const showAnimation = {
|
||||
from: {
|
||||
opacity: 0,
|
||||
|
@ -47,13 +47,17 @@ class MainButton extends Component {
|
||||
iconType,
|
||||
textStyle,
|
||||
iconPosition,
|
||||
iconStyle,
|
||||
renderIcon,
|
||||
} = this.props;
|
||||
|
||||
if (isLoading) {
|
||||
this._getIndicator();
|
||||
}
|
||||
|
||||
const iconComponent = source ? (
|
||||
const iconComponent =
|
||||
renderIcon ||
|
||||
(source ? (
|
||||
<Image source={source} style={styles.image} resizeMode="contain" />
|
||||
) : (
|
||||
iconName && (
|
||||
@ -61,10 +65,10 @@ class MainButton extends Component {
|
||||
iconType={iconType || 'MaterialIcons'}
|
||||
color={iconColor}
|
||||
name={iconName}
|
||||
style={styles.icon}
|
||||
style={[styles.icon, iconStyle]}
|
||||
/>
|
||||
)
|
||||
);
|
||||
));
|
||||
|
||||
if (text) {
|
||||
return (
|
||||
@ -108,7 +112,7 @@ class MainButton extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { wrapperStyle, children, height, style, isLoading } = this.props;
|
||||
const { wrapperStyle, children, height, style, isLoading, bodyWrapperStyle } = this.props;
|
||||
const { isDisable } = this.state;
|
||||
|
||||
return (
|
||||
@ -123,7 +127,7 @@ class MainButton extends Component {
|
||||
style && style,
|
||||
]}
|
||||
>
|
||||
<View style={styles.body}>
|
||||
<View style={[styles.body, bodyWrapperStyle]}>
|
||||
{isLoading ? this._getIndicator() : children || this._getBody()}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
@ -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}
|
||||
|
25
src/components/orDivider/orDividerStyles.ts
Normal file
25
src/components/orDivider/orDividerStyles.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
export default EStyleSheet.create({
|
||||
dividerContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
divider: {
|
||||
borderWidth: 0.5,
|
||||
flex: 1,
|
||||
borderColor: '$primaryDarkGray',
|
||||
},
|
||||
leftDivider: {
|
||||
marginLeft: 20,
|
||||
},
|
||||
rightDivider: {
|
||||
marginRight: 20,
|
||||
},
|
||||
orText: {
|
||||
fontSize: 16,
|
||||
color: '$primaryDarkGray',
|
||||
marginHorizontal: 8,
|
||||
},
|
||||
});
|
24
src/components/orDivider/orDividerView.tsx
Normal file
24
src/components/orDivider/orDividerView.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { View, Text, ViewStyle } from 'react-native';
|
||||
import { useIntl } from 'react-intl';
|
||||
import styles from './orDividerStyles';
|
||||
|
||||
interface OrDividerProps {
|
||||
containerStyle?: ViewStyle;
|
||||
}
|
||||
const OrDivider = ({ containerStyle }: OrDividerProps) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<View style={[styles.dividerContainer, containerStyle]}>
|
||||
<View style={[styles.divider, styles.leftDivider]} />
|
||||
<Text style={styles.orText}>
|
||||
{intl.formatMessage({
|
||||
id: 'login.or',
|
||||
})}
|
||||
</Text>
|
||||
<View style={[styles.divider, styles.rightDivider]} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrDivider;
|
@ -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 {
|
||||
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}
|
||||
<MediaPreviewItem
|
||||
item={item}
|
||||
insertedMediaUrls={insertedMediaUrls}
|
||||
deleteIds={deleteIds}
|
||||
isDeleteMode={isDeleteMode}
|
||||
isDeleting={isDeleting}
|
||||
isExpandedMode={isExpandedMode}
|
||||
onPress={_onPress}
|
||||
/>
|
||||
{_renderCounter()}
|
||||
{_renderMinus()}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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({
|
||||
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'],
|
||||
})
|
||||
.then((images) => {
|
||||
if (images && !Array.isArray(images)) {
|
||||
images = [images];
|
||||
};
|
||||
|
||||
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({
|
||||
const _vidMode = mode === Modes.MODE_VIDEO;
|
||||
|
||||
if (_vidMode && isAddingToUploads) {
|
||||
speakUploaderRef.current.showUploader();
|
||||
return;
|
||||
}
|
||||
|
||||
const _options: Options = _vidMode
|
||||
? {
|
||||
mediaType: 'video',
|
||||
}
|
||||
: {
|
||||
includeBase64: true,
|
||||
mediaType: 'photo',
|
||||
})
|
||||
.then((image) => {
|
||||
_handleMediaOnSelected([image], true);
|
||||
};
|
||||
|
||||
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 && (
|
||||
<>
|
||||
{showModal && (
|
||||
<UploadsGalleryContent
|
||||
insertedMediaUrls={insertedMediaUrls}
|
||||
mediaUploads={mediaQuery.data.slice()}
|
||||
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',
|
||||
},
|
||||
});
|
@ -377,12 +377,13 @@
|
||||
"no_user": "User is not found."
|
||||
},
|
||||
"login": {
|
||||
"signin": "Sign in",
|
||||
"signin": "Login",
|
||||
"login_with_hs": "Login with hivesigner",
|
||||
"signup": "JOIN NOW",
|
||||
"signin_title": "To get all the benefits of using Ecency",
|
||||
"username": "Username",
|
||||
"password": "Password or WIF",
|
||||
"description": "By signing in, you agree to our Terms of Services and Privacy Policies.",
|
||||
"username": "username",
|
||||
"password": "password / private key",
|
||||
"description": "By logging in, you agree to our Terms of Services and Privacy Policies.",
|
||||
"cancel": "cancel",
|
||||
"login": "LOGIN",
|
||||
"steemconnect_description": "If you don't want to keep your password encrypted and saved on your device, you can use Hivesigner.",
|
||||
@ -394,7 +395,10 @@
|
||||
"deep_login_alert_title": "Easy Login @{username}",
|
||||
"deep_login_alert_body":"Verify direct login using access code",
|
||||
"deep_login_url_expired":"Login url expired, please use private key or password to login",
|
||||
"deep_login_malformed_url":"Malformed login url, please use private key or password to login"
|
||||
"deep_login_malformed_url":"Malformed login url, please use private key or password to login",
|
||||
"no_account_text": "Don't have an account?",
|
||||
"signup_now": "Sign up now!",
|
||||
"or": "OR"
|
||||
},
|
||||
"register": {
|
||||
"modal_title":"Get Hive Account",
|
||||
@ -515,6 +519,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 +562,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 +597,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>
|
||||
|
@ -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);
|
||||
|
@ -12,7 +12,7 @@ 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,
|
||||
@ -33,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,
|
||||
@ -52,6 +56,13 @@ import { PointActivityIds } from '../../../providers/ecency/ecency.types';
|
||||
import { usePostsCachePrimer } from '../../../providers/queries/postQueries/postQueries';
|
||||
import { PostTypes } from '../../../constants/postTypes';
|
||||
|
||||
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
|
||||
*@props --> props name here description here Value Type Here
|
||||
@ -96,7 +107,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 +203,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 +319,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 +413,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 +434,8 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
return;
|
||||
}
|
||||
|
||||
speakContentBuilder.build(fields.body);
|
||||
|
||||
const beneficiaries = this._extractBeneficiaries();
|
||||
const postBodySummaryContent = postBodySummary(
|
||||
get(fields, 'body', ''),
|
||||
@ -450,13 +463,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 || postBodySummaryContent,
|
||||
videos: Object.keys(videos).length > 0 && videos,
|
||||
});
|
||||
|
||||
const jsonMeta = makeJsonMetadata(meta, draftField.tags);
|
||||
@ -574,7 +602,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 +616,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 +705,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
if (fields.tags.length === 0) {
|
||||
fields.tags = ['hive-125125'];
|
||||
}
|
||||
|
||||
this._setScheduledPost({
|
||||
author,
|
||||
permlink,
|
||||
@ -651,6 +728,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
|
||||
)
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
|
||||
// track user activity for points
|
||||
userActivityMutation.mutate({
|
||||
pointsTy: PointActivityIds.POST,
|
||||
@ -673,6 +751,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 +801,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 +822,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 +888,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 +899,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 +919,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 +1323,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}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { View, Platform, Keyboard } from 'react-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Platform, Keyboard, Text } from 'react-native';
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||
import ScrollableTabView from 'react-native-scrollable-tab-view';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
// Actions
|
||||
@ -15,79 +14,94 @@ import {
|
||||
LoginHeader,
|
||||
MainButton,
|
||||
Modal,
|
||||
TabBar,
|
||||
TextButton,
|
||||
OrDivider,
|
||||
} from '../../../components';
|
||||
|
||||
// Constants
|
||||
import { default as ROUTES } from '../../../constants/routeNames';
|
||||
import { ECENCY_TERMS_URL } from '../../../config/ecencyApi';
|
||||
|
||||
// Styles
|
||||
import styles from './loginStyles';
|
||||
import globalStyles from '../../../globalStyles';
|
||||
import { HiveSignerIcon } from '../../../assets/svgs';
|
||||
|
||||
import STEEM_CONNECT_LOGO from '../../../assets/steem_connect.png';
|
||||
import { ECENCY_TERMS_URL } from '../../../config/ecencyApi';
|
||||
const LoginScreen = ({
|
||||
initialUsername,
|
||||
getAccountsWithUsername,
|
||||
navigation,
|
||||
handleOnPressLogin,
|
||||
handleSignUp,
|
||||
isLoading,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [username, setUsername] = useState(initialUsername || '');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isUsernameValid, setIsUsernameValid] = useState(true);
|
||||
const [keyboardIsOpen, setKeyboardIsOpen] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
class LoginScreen extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
useEffect(() => {
|
||||
if (initialUsername) {
|
||||
_handleUsernameChange(initialUsername);
|
||||
}
|
||||
}, []);
|
||||
|
||||
this.state = {
|
||||
username: props.initialUsername || '',
|
||||
password: '',
|
||||
isUsernameValid: true,
|
||||
keyboardIsOpen: false,
|
||||
isModalOpen: false,
|
||||
useEffect(() => {
|
||||
const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
|
||||
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
|
||||
|
||||
return () => {
|
||||
keyboardDidShowListener.remove();
|
||||
keyboardDidHideListener.remove();
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.initialUsername) {
|
||||
this._handleUsernameChange(this.props.initialUsername);
|
||||
}
|
||||
}
|
||||
const debouncedCheckValidity = debounce((uname) => {
|
||||
_checkUsernameIsValid(uname);
|
||||
}, 1000);
|
||||
|
||||
componentWillUnmount() {
|
||||
this.keyboardDidShowListener.remove();
|
||||
this.keyboardDidHideListener.remove();
|
||||
}
|
||||
useEffect(() => {
|
||||
if (username) {
|
||||
debouncedCheckValidity(username);
|
||||
|
||||
_handleOnPasswordChange = (value) => {
|
||||
this.setState({ password: value });
|
||||
return () => debouncedCheckValidity.cancel();
|
||||
}
|
||||
}, [username]);
|
||||
|
||||
const _keyboardDidShow = () => {
|
||||
setKeyboardIsOpen(true);
|
||||
};
|
||||
|
||||
_handleUsernameChange = (username) => {
|
||||
const { getAccountsWithUsername } = this.props;
|
||||
const _keyboardDidHide = () => {
|
||||
setKeyboardIsOpen(false);
|
||||
};
|
||||
|
||||
this.setState({ username });
|
||||
const _handleOnPasswordChange = (value) => {
|
||||
setPassword(value);
|
||||
};
|
||||
|
||||
getAccountsWithUsername(username).then((res) => {
|
||||
const isValid = res.includes(username);
|
||||
const _handleUsernameChange = (username) => {
|
||||
const formattedUsername = username.trim().toLowerCase();
|
||||
setUsername(formattedUsername);
|
||||
};
|
||||
|
||||
this.setState({ isUsernameValid: isValid });
|
||||
const _checkUsernameIsValid = (uname) => {
|
||||
getAccountsWithUsername(uname).then((res) => {
|
||||
const isValid = res.includes(uname);
|
||||
setIsUsernameValid(isValid);
|
||||
});
|
||||
};
|
||||
|
||||
_handleOnModalToggle = () => {
|
||||
const { isModalOpen } = this.state;
|
||||
this.setState({ isModalOpen: !isModalOpen });
|
||||
const _handleOnModalToggle = () => {
|
||||
setIsModalOpen(!isModalOpen);
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () =>
|
||||
this.setState({ keyboardIsOpen: true }),
|
||||
const _renderHiveicon = () => (
|
||||
<View style={styles.hsLoginBtnIconStyle}>
|
||||
<HiveSignerIcon />
|
||||
</View>
|
||||
);
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () =>
|
||||
this.setState({ keyboardIsOpen: false }),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navigation, intl, handleOnPressLogin, handleSignUp, isLoading } = this.props;
|
||||
const { username, isUsernameValid, keyboardIsOpen, password, isModalOpen } = this.state;
|
||||
|
||||
console.log('keyboardIsOpen : ', keyboardIsOpen);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LoginHeader
|
||||
@ -102,20 +116,13 @@ class LoginScreen extends PureComponent {
|
||||
rightButtonText={intl.formatMessage({
|
||||
id: 'login.signup',
|
||||
})}
|
||||
onBackPress={() => {
|
||||
navigation.navigate({
|
||||
name: ROUTES.DRAWER.MAIN,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ScrollableTabView
|
||||
locked={isLoading}
|
||||
style={globalStyles.tabView}
|
||||
renderTabBar={() => (
|
||||
<TabBar
|
||||
style={styles.tabbar}
|
||||
tabUnderlineDefaultWidth={100}
|
||||
tabUnderlineScaleX={2} // default 3
|
||||
activeColor="#357ce6"
|
||||
inactiveColor="#222"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
|
||||
<View
|
||||
tabLabel={intl.formatMessage({
|
||||
id: 'login.signin',
|
||||
@ -132,7 +139,7 @@ class LoginScreen extends PureComponent {
|
||||
leftIconName="close"
|
||||
iconType="MaterialCommunityIcons"
|
||||
isValid={isUsernameValid}
|
||||
onChange={debounce(this._handleUsernameChange, 1000)}
|
||||
onChange={_handleUsernameChange}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'login.username',
|
||||
})}
|
||||
@ -141,12 +148,13 @@ class LoginScreen extends PureComponent {
|
||||
isFirstImage
|
||||
value={username}
|
||||
inputStyle={styles.input}
|
||||
onBlur={() => _checkUsernameIsValid(username)}
|
||||
/>
|
||||
<FormInput
|
||||
rightIconName="lock"
|
||||
leftIconName="close"
|
||||
isValid={isUsernameValid}
|
||||
onChange={(value) => this._handleOnPasswordChange(value)}
|
||||
onChange={_handleOnPasswordChange}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'login.password',
|
||||
})}
|
||||
@ -164,20 +172,6 @@ class LoginScreen extends PureComponent {
|
||||
link={ECENCY_TERMS_URL}
|
||||
iconName="ios-information-circle-outline"
|
||||
/>
|
||||
</KeyboardAwareScrollView>
|
||||
|
||||
<View style={styles.footerButtons}>
|
||||
<TextButton
|
||||
style={styles.cancelButton}
|
||||
onPress={() =>
|
||||
navigation.navigate({
|
||||
name: ROUTES.DRAWER.MAIN,
|
||||
})
|
||||
}
|
||||
text={intl.formatMessage({
|
||||
id: 'login.cancel',
|
||||
})}
|
||||
/>
|
||||
<MainButton
|
||||
onPress={() => handleOnPressLogin(username, password)}
|
||||
iconName="person"
|
||||
@ -188,40 +182,51 @@ class LoginScreen extends PureComponent {
|
||||
textStyle={styles.mainBtnText}
|
||||
isDisable={!isUsernameValid || password.length < 2 || username.length < 2}
|
||||
isLoading={isLoading}
|
||||
wrapperStyle={styles.loginBtnWrapper}
|
||||
bodyWrapperStyle={styles.loginBtnBodyWrapper}
|
||||
height={50}
|
||||
iconStyle={styles.loginBtnIconStyle}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View tabLabel="Hivesigner" style={styles.tabbarItem}>
|
||||
<InformationArea
|
||||
description={intl.formatMessage({
|
||||
id: 'login.steemconnect_description',
|
||||
})}
|
||||
iconName="ios-information-circle-outline"
|
||||
link="https://hivesigner.com"
|
||||
/>
|
||||
<OrDivider />
|
||||
<MainButton
|
||||
wrapperStyle={styles.mainButtonWrapper}
|
||||
onPress={() => this._handleOnModalToggle()}
|
||||
source={STEEM_CONNECT_LOGO}
|
||||
text="hive"
|
||||
secondText="signer"
|
||||
onPress={() => _handleOnModalToggle()}
|
||||
renderIcon={_renderHiveicon()}
|
||||
text={intl.formatMessage({
|
||||
id: 'login.login_with_hs',
|
||||
})}
|
||||
textStyle={styles.hsLoginBtnText}
|
||||
wrapperStyle={styles.loginBtnWrapper}
|
||||
bodyWrapperStyle={styles.loginBtnBodyWrapper}
|
||||
height={48}
|
||||
style={styles.hsLoginBtnStyle}
|
||||
/>
|
||||
</KeyboardAwareScrollView>
|
||||
<View style={styles.footerButtons}>
|
||||
<Text style={styles.noAccountText}>
|
||||
{intl.formatMessage({
|
||||
id: 'login.no_account_text',
|
||||
})}
|
||||
</Text>
|
||||
<Text style={styles.signUpNowText} onPress={() => handleSignUp()}>
|
||||
{intl.formatMessage({
|
||||
id: 'login.signup_now',
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollableTabView>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
isFullScreen
|
||||
isCloseButton
|
||||
handleOnModalClose={this._handleOnModalToggle}
|
||||
handleOnModalClose={_handleOnModalToggle}
|
||||
title={intl.formatMessage({
|
||||
id: 'login.signin',
|
||||
})}
|
||||
>
|
||||
<HiveSigner handleOnModalClose={this._handleOnModalToggle} />
|
||||
<HiveSigner handleOnModalClose={_handleOnModalToggle} />
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(LoginScreen);
|
||||
export default LoginScreen;
|
||||
|
@ -3,7 +3,7 @@ import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
export default EStyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '$primaryLightBackground',
|
||||
backgroundColor: '$primaryBackgroundColor',
|
||||
},
|
||||
tabbar: {
|
||||
alignSelf: 'center',
|
||||
@ -11,10 +11,9 @@ export default EStyleSheet.create({
|
||||
backgroundColor: '$primaryBackgroundColor',
|
||||
},
|
||||
tabbarItem: {
|
||||
flex: 1,
|
||||
// flex: 1,
|
||||
backgroundColor: '$primaryBackgroundColor',
|
||||
minWidth: '$deviceWidth',
|
||||
height: '$deviceHeight / 1.95',
|
||||
},
|
||||
mainButtonWrapper: {
|
||||
position: 'absolute',
|
||||
@ -22,20 +21,13 @@ export default EStyleSheet.create({
|
||||
bottom: 24,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
footerButtons: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'flex-end',
|
||||
paddingRight: 24,
|
||||
paddingBottom: 24,
|
||||
backgroundColor: '$primaryBackgroundColor',
|
||||
},
|
||||
|
||||
cancelButton: {
|
||||
marginRight: 10,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
formWrapper: {
|
||||
flexGrow: 1,
|
||||
marginHorizontal: 30,
|
||||
marginVertical: 10,
|
||||
},
|
||||
@ -44,6 +36,44 @@ export default EStyleSheet.create({
|
||||
flexGrow: 1,
|
||||
},
|
||||
mainBtnText: {
|
||||
marginRight: 12,
|
||||
flexGrow: 1,
|
||||
},
|
||||
loginBtnWrapper: {
|
||||
marginVertical: 12,
|
||||
},
|
||||
loginBtnBodyWrapper: {
|
||||
flex: 1,
|
||||
},
|
||||
loginBtnIconStyle: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
},
|
||||
hsLoginBtnStyle: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: '$primaryBlue',
|
||||
},
|
||||
hsLoginBtnText: {
|
||||
flexGrow: 1,
|
||||
color: '$primaryBlue',
|
||||
},
|
||||
hsLoginBtnIconStyle: {
|
||||
marginLeft: 20,
|
||||
},
|
||||
footerButtons: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 24,
|
||||
backgroundColor: '$primaryBackgroundColor',
|
||||
},
|
||||
noAccountText: {
|
||||
color: '$primaryDarkGray',
|
||||
fontSize: 16,
|
||||
},
|
||||
signUpNowText: {
|
||||
color: '$primaryBlue',
|
||||
marginLeft: 4,
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
@ -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