Merge remote-tracking branch 'origin/development' into nt/comment-modal

# Conflicts:
#	src/screens/editor/container/editorContainer.tsx
This commit is contained in:
noumantahir 2023-12-12 12:02:38 +05:00
commit c8f9b7c0f6
53 changed files with 9640 additions and 489 deletions

View File

@ -12,5 +12,6 @@
},
"workbench.colorCustomizations": {
"editorUnnecessaryCode.border": "#dd7aab"
}
},
"java.compile.nullAnalysis.mode": "automatic"
}

View File

@ -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

View File

@ -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>

View File

@ -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.** { *; }

View File

@ -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

View File

@ -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",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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
View File

@ -0,0 +1 @@
export { default as HiveSignerIcon } from './hive-signer-icon';

View File

@ -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"

View File

@ -201,7 +201,7 @@ const FormInputView = ({
) : value && value.length > 0 ? (
<Icon
iconType={iconType || 'MaterialIcons'}
onPress={() => setValue('')}
onPress={() => _handleOnChange('')}
name={leftIconName}
style={styles.icon}
/>

View File

@ -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,
};

View File

@ -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',
},

View File

@ -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,

View File

@ -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>

View File

@ -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={() => {

View File

@ -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;

View File

@ -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}

View 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,
},
});

View 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;

View File

@ -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('');

View File

@ -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

View File

@ -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 }) => {

View File

@ -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>
);
};

View File

@ -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>
);
});

View File

@ -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,

View File

@ -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,
});

View File

@ -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}
/>
</>
);
},
);

View File

@ -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',
},
});

View File

@ -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",

View File

@ -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}>

View File

@ -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} />

View File

@ -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 {

View File

@ -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);

View File

@ -0,0 +1,4 @@
import * as editorQueries from './editorQueries';
import * as speakQueries from './speakQueries';
export { editorQueries, speakQueries };

View 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,
};
};

View File

@ -13,6 +13,7 @@ const QUERIES = {
},
MEDIA: {
GET: 'QUERY_GET_UPLOADS',
GET_VIDEOS: 'QUERY_GET_VIDEO_UPLOADS',
},
WALLET: {
GET: 'QUERY_GET_ASSETS',

View 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';

View 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;
};

View 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);
}
};

View 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;
}

View File

@ -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,
});

View File

@ -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';

View File

@ -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;
}

View File

@ -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>

View File

@ -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);

View File

@ -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(),
});

View File

@ -495,6 +495,7 @@ class EditorScreen extends Component {
/>
)}
<MarkdownEditor
draftId={draftId}
paramFiles={paramFiles}
componentID="body"
draftBody={fields && fields.body}

View File

@ -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;

View File

@ -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,
},
});

View File

@ -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;

View File

@ -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"