Merge pull request #2795 from ecency/nt/video-n-background-upload

Nt/video n background upload
This commit is contained in:
Feruz M 2023-12-11 20:02:47 +05:30 committed by GitHub
commit f26d792ea5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 9256 additions and 249 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

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

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

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

@ -515,6 +515,7 @@
"scheduled_for":"Scheduled For",
"scheduled_immediate":"Immediate",
"scheduled_later":"Later",
"schedule_video_unsupported":"Scheduling video posts not available",
"settings_title":"Post Options",
"done":"DONE",
"draft_save_title":"Saving Draft",
@ -557,10 +558,20 @@
"btn_add":"Image",
"btn_insert":"INSERT",
"btn_delete":"DELETE",
"confirm_delete":"Are you sure you want to delete images from your uploads",
"confirm_delete":"Are you sure you want to delete selected items from your uploads",
"message_failed":"Failed to upload image",
"delete_failed":"Failed to delete image",
"failed_count":"Failed to upload {failedCount} of {totalCount} selected image(s)"
"failed_count":"Failed to upload {failedCount} of {totalCount} selected image(s)",
"publish_manual":"Ready",
"published":"Published",
"encoding_ipfs":"Encoding",
"encoding_preparing":"Preparing",
"deleted":"Deleted",
"start_upload":"START UPLOAD",
"uploading":"UPLOADING",
"select_thumb":"Select Thumbnail",
"warn_vid_already_added":"",
"warn_vid_in_process":""
},
"pincode": {
"enter_text": "Enter PIN to unlock",
@ -582,6 +593,8 @@
"continue": "Continue",
"okay":"Okay",
"done":"Done",
"notice":"Notice!",
"close":"CLOSE",
"move_question": "Are you sure to move to drafts?",
"success_shared": "Success! Content submitted!",
"success_moved": "Moved to draft",

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

@ -1,5 +1,4 @@
import EStyleSheet from 'react-native-extended-stylesheet';
import { getBottomSpace } from 'react-native-iphone-x-helper';
export default EStyleSheet.create({
sheetContent: {

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

@ -10,8 +10,9 @@ import { isArray } from 'lodash';
import { Buffer } from 'buffer';
import { useQueryClient } from '@tanstack/react-query';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import { postBodySummary } from '@ecency/render-helper';
import { addDraft, updateDraft, getDrafts, addSchedule } from '../../../providers/ecency/ecency';
import { toastNotification, setRcOffer } from '../../../redux/actions/uiAction';
import { toastNotification, setRcOffer, showActionModal } from '../../../redux/actions/uiAction';
import {
postContent,
getPurePost,
@ -32,12 +33,16 @@ import {
extractMetadata,
makeJsonMetadataForUpdate,
createPatch,
extract3SpeakIds,
} from '../../../utils/editor';
// import { generateSignature } from '../../../utils/image';
// Component
import EditorScreen from '../screen/editorScreen';
import { removeBeneficiaries, setBeneficiaries } from '../../../redux/actions/editorActions';
import {
removeBeneficiaries,
setAllowSpkPublishing,
setBeneficiaries,
} from '../../../redux/actions/editorActions';
import { DEFAULT_USER_DRAFT_ID, TEMP_BENEFICIARIES_ID } from '../../../redux/constants/constants';
import {
deleteDraftCacheEntry,
@ -50,7 +55,12 @@ import { useUserActivityMutation } from '../../../providers/queries/pointQueries
import { PointActivityIds } from '../../../providers/ecency/ecency.types';
import { usePostsCachePrimer } from '../../../providers/queries/postQueries/postQueries';
import { PostTypes } from '../../../constants/postTypes';
import { postBodySummary } from '@ecency/render-helper';
import { speakQueries } from '../../../providers/queries';
import {
BENEFICIARY_SRC_ENCODER,
DEFAULT_SPEAK_BENEFICIARIES,
} from '../../../providers/speak/constants';
import { ThreeSpeakVideo } from '../../../providers/speak/speak.types';
/*
* Props Name Description Value
@ -96,7 +106,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
// Component Life Cycle Functions
componentDidMount() {
this._isMounted = true;
const { currentAccount, route, queryClient } = this.props;
const { currentAccount, route, queryClient, dispatch } = this.props;
const username = currentAccount && currentAccount.name ? currentAccount.name : '';
let isReply;
let draftId;
@ -192,6 +202,9 @@ class EditorContainer extends Component<EditorContainerProps, any> {
this._requestKeyboardFocus();
this._appStateSub = AppState.addEventListener('change', this._handleAppStateChange);
// dispatch spk publishing status
dispatch(setAllowSpkPublishing(!isReply && !isEdit));
}
componentDidUpdate(prevProps: Readonly<any>, prevState: Readonly<any>): void {
@ -305,6 +318,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
const filteredBeneficiaries = draft.meta.beneficiaries.filter(
(item) => item.account !== currentAccount.username,
); // remove default beneficiary from array while saving
dispatch(setBeneficiaries(draft._id || TEMP_BENEFICIARIES_ID, filteredBeneficiaries));
}
}
@ -398,18 +412,14 @@ class EditorContainer extends Component<EditorContainerProps, any> {
_extractBeneficiaries = () => {
const { draftId } = this.state;
const { beneficiariesMap, currentAccount } = this.props;
const { beneficiariesMap } = this.props;
return (
beneficiariesMap[draftId || TEMP_BENEFICIARIES_ID] || [
{ account: currentAccount.name, weight: 10000 },
]
);
return beneficiariesMap[draftId || TEMP_BENEFICIARIES_ID] || [];
};
_saveDraftToDB = async (fields, saveAsNew = false) => {
const { isDraftSaved, draftId, thumbUrl, isReply, rewardType, postDescription } = this.state;
const { currentAccount, dispatch, intl, queryClient } = this.props;
const { currentAccount, dispatch, intl, queryClient, speakContentBuilder } = this.props;
try {
// saves draft locallly
@ -423,6 +433,8 @@ class EditorContainer extends Component<EditorContainerProps, any> {
return;
}
speakContentBuilder.build(fields.body);
const beneficiaries = this._extractBeneficiaries();
const postBodySummaryContent = postBodySummary(
get(fields, 'body', ''),
@ -450,13 +462,28 @@ class EditorContainer extends Component<EditorContainerProps, any> {
const _extractedMeta = await extractMetadata({
body: draftField.body,
thumbUrl,
videoThumbUrls: speakContentBuilder.thumbUrlsRef.current,
fetchRatios: false,
});
// inject video meta for draft
const speakIds = extract3SpeakIds({ body: draftField.body });
const videos: any = {};
const videosCache: any = queryClient.getQueryData([QUERIES.MEDIA.GET_VIDEOS]);
speakIds.forEach((_id) => {
const videoItem = videosCache.find((item) => item._id === _id);
if (videoItem?.speakData) {
videos[_id] = videoItem.speakData;
}
});
const meta = Object.assign({}, _extractedMeta, {
tags: draftField.tags,
beneficiaries,
rewardType,
description: postDescription ? postDescription : postBodySummaryContent,
description: postDescription || postBodySummaryContent,
videos: Object.keys(videos).length > 0 && videos,
});
const jsonMeta = makeJsonMetadata(meta, draftField.tags);
@ -574,7 +601,13 @@ class EditorContainer extends Component<EditorContainerProps, any> {
}
};
_submitPost = async ({ fields, scheduleDate }: { fields: any; scheduleDate?: string }) => {
_submitPost = async ({
fields: _fieldsBase,
scheduleDate,
}: {
fields: any;
scheduleDate?: string;
}) => {
const {
currentAccount,
dispatch,
@ -582,27 +615,69 @@ class EditorContainer extends Component<EditorContainerProps, any> {
navigation,
pinCode,
userActivityMutation,
// isDefaultFooter,
speakContentBuilder,
speakMutations,
} = this.props;
const { rewardType, isPostSending, thumbUrl, draftId, shouldReblog } = this.state;
const beneficiaries = this._extractBeneficiaries();
const fields = Object.assign({}, _fieldsBase);
let beneficiaries = this._extractBeneficiaries();
let videoPublishMeta: ThreeSpeakVideo | undefined = undefined;
if (isPostSending) {
return;
}
if (currentAccount) {
// build speak video body
try {
fields.body = speakContentBuilder.build(fields.body);
videoPublishMeta = speakContentBuilder.videoPublishMetaRef.current;
// verify and make video beneficiaries redundent
beneficiaries = beneficiaries.filter((item) => item.src !== BENEFICIARY_SRC_ENCODER);
if (videoPublishMeta) {
const encoderBene = [
...JSON.parse(videoPublishMeta.beneficiaries || '[]'),
...DEFAULT_SPEAK_BENEFICIARIES,
];
beneficiaries = [...encoderBene, ...beneficiaries];
}
} catch (err) {
console.warn('fail', err);
return;
}
if (scheduleDate && videoPublishMeta) {
dispatch(
showActionModal({
title: intl.formatMessage({ id: 'alert.notice' }),
body: intl.formatMessage({ id: 'editor.schedule_video_unsupported' }),
}),
);
return;
}
this.setState({
isPostSending: true,
});
const meta = await extractMetadata({ body: fields.body, thumbUrl, fetchRatios: true });
// only require video meta for unpublished video, it will always be one
const meta = await extractMetadata({
body: fields.body,
thumbUrl,
videoThumbUrls: speakContentBuilder.thumbUrlsRef.current,
fetchRatios: true,
videoPublishMeta,
});
const _tags = fields.tags.filter((tag) => tag && tag !== ' ');
const jsonMeta = makeJsonMetadata(meta, _tags);
// TODO: check if permlink is available github: #314 https://github.com/ecency/ecency-mobile/pull/314
let permlink = generatePermlink(fields.title || '');
let permlink = videoPublishMeta
? videoPublishMeta.permlink
: generatePermlink(fields.title || '');
let dublicatePost;
try {
@ -629,6 +704,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
if (fields.tags.length === 0) {
fields.tags = ['hive-125125'];
}
this._setScheduledPost({
author,
permlink,
@ -651,6 +727,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
)
.then((response) => {
console.log(response);
// track user activity for points
userActivityMutation.mutate({
pointsTy: PointActivityIds.POST,
@ -673,6 +750,18 @@ class EditorContainer extends Component<EditorContainerProps, any> {
});
}
// mark unpublished video as published on 3speak if that is the case
if (videoPublishMeta) {
console.log('marking inserted video as published');
speakMutations.updateInfoMutation.mutate({
id: videoPublishMeta._id,
title: fields.title,
body: fields.body,
tags: fields.tags,
});
speakMutations.markAsPublishedMutation.mutate(videoPublishMeta._id);
}
// post publish updates
dispatch(deleteDraftCacheEntry(DEFAULT_USER_DRAFT_ID + currentAccount.name));
@ -711,8 +800,14 @@ class EditorContainer extends Component<EditorContainerProps, any> {
};
_submitReply = async (fields) => {
const { currentAccount, pinCode, dispatch, userActivityMutation, draftsCollection } =
this.props;
const {
currentAccount,
pinCode,
dispatch,
userActivityMutation,
draftsCollection,
speakContentBuilder,
} = this.props;
const { isPostSending } = this.state;
if (isPostSending) {
@ -726,6 +821,8 @@ class EditorContainer extends Component<EditorContainerProps, any> {
const { post } = this.state;
fields.body = speakContentBuilder.build(fields.body);
const _prefix = `re-${post.author.replace(/\./g, '')}`;
const permlink = generateUniquePermlink(_prefix);
@ -790,7 +887,7 @@ class EditorContainer extends Component<EditorContainerProps, any> {
};
_submitEdit = async (fields) => {
const { currentAccount, pinCode, dispatch, postCachePrimer } = this.props;
const { currentAccount, pinCode, dispatch, postCachePrimer, speakContentBuilder } = this.props;
const { post, isEdit, isPostSending, thumbUrl, isReply } = this.state;
if (isPostSending) {
@ -801,6 +898,10 @@ class EditorContainer extends Component<EditorContainerProps, any> {
this.setState({
isPostSending: true,
});
// build speak video body
fields.body = speakContentBuilder.build(fields.body);
const { tags, body, title } = fields;
const {
markdownBody: oldBody,
@ -817,7 +918,12 @@ class EditorContainer extends Component<EditorContainerProps, any> {
newBody = patch;
}
const meta = await extractMetadata({ body: fields.body, thumbUrl, fetchRatios: true });
const meta = await extractMetadata({
body: fields.body,
videoThumbUrls: speakContentBuilder.thumbUrlsRef.current,
thumbUrl,
fetchRatios: true,
});
let jsonMeta = {};
@ -1216,6 +1322,8 @@ const mapStateToProps = (state) => ({
const mapQueriesToProps = () => ({
queryClient: useQueryClient(),
speakContentBuilder: speakQueries.useSpeakContentBuilder(),
speakMutations: speakQueries.useSpeakMutations(),
userActivityMutation: useUserActivityMutation(),
postCachePrimer: usePostsCachePrimer(),
});

View File

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

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"