mirror of
https://github.com/ecency/ecency-mobile.git
synced 2024-12-04 17:33:55 +03:00
Merge branch 'development' into sa/options-modal
This commit is contained in:
commit
863b83c5f3
@ -144,7 +144,7 @@ android {
|
|||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode versionMajor * 10000 + versionMinor * 100 + versionPatch
|
versionCode versionMajor * 10000 + versionMinor * 100 + versionPatch
|
||||||
versionName "3.0.25"
|
versionName "3.0.26"
|
||||||
resValue "string", "build_config_package", "app.esteem.mobile.android"
|
resValue "string", "build_config_package", "app.esteem.mobile.android"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
// react-native-image-crop-picker
|
// react-native-image-crop-picker
|
||||||
|
@ -15,11 +15,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>3.0.25</string>
|
<string>3.0.26</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2804</string>
|
<string>2805</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true />
|
<true />
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
|
@ -15,10 +15,10 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>BNDL</string>
|
<string>BNDL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>3.0.25</string>
|
<string>3.0.26</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2804</string>
|
<string>2805</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -1129,7 +1129,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 2804;
|
CURRENT_PROJECT_VERSION = 2805;
|
||||||
DEAD_CODE_STRIPPING = NO;
|
DEAD_CODE_STRIPPING = NO;
|
||||||
DEVELOPMENT_TEAM = 75B6RXTKGT;
|
DEVELOPMENT_TEAM = 75B6RXTKGT;
|
||||||
EXCLUDED_ARCHS = "";
|
EXCLUDED_ARCHS = "";
|
||||||
@ -1208,7 +1208,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Ecency/Ecency.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Ecency/Ecency.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 2804;
|
CURRENT_PROJECT_VERSION = 2805;
|
||||||
DEAD_CODE_STRIPPING = NO;
|
DEAD_CODE_STRIPPING = NO;
|
||||||
DEVELOPMENT_TEAM = 75B6RXTKGT;
|
DEVELOPMENT_TEAM = 75B6RXTKGT;
|
||||||
EXCLUDED_ARCHS = "";
|
EXCLUDED_ARCHS = "";
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>3.0.25</string>
|
<string>3.0.26</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
@ -15,10 +15,10 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>BNDL</string>
|
<string>BNDL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>3.0.25</string>
|
<string>3.0.26</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2804</string>
|
<string>2805</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>3.0.25</string>
|
<string>3.0.26</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2804</string>
|
<string>2805</string>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionAttributes</key>
|
<key>NSExtensionAttributes</key>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ecency",
|
"name": "ecency",
|
||||||
"version": "3.0.25",
|
"version": "3.0.26",
|
||||||
"displayName": "Ecency",
|
"displayName": "Ecency",
|
||||||
"private": true,
|
"private": true,
|
||||||
"rnpm": {
|
"rnpm": {
|
||||||
@ -18,7 +18,7 @@
|
|||||||
"test": "node node_modules/jest/bin/jest.js --watch",
|
"test": "node node_modules/jest/bin/jest.js --watch",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"lint:fix": "eslint src/ --fix",
|
"lint:fix": "eslint src/ --fix",
|
||||||
"format": "prettier --write 'src/**/*.{js,jsx}' && yarn lint:fix --fix",
|
"format": "prettier --write 'src/**/*.{js,jsx,ts,tsx}' && yarn lint:fix --fix",
|
||||||
"lint-staged": "lint-staged",
|
"lint-staged": "lint-staged",
|
||||||
"clear": "watchman watch-del-all && rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-* && rm -rf node_modules/ && yarn && yarn start -- --reset-cache",
|
"clear": "watchman watch-del-all && rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-* && rm -rf node_modules/ && yarn && yarn start -- --reset-cache",
|
||||||
"bump-patch": "npm version patch --no-git-tag-version",
|
"bump-patch": "npm version patch --no-git-tag-version",
|
||||||
@ -35,6 +35,7 @@
|
|||||||
"@esteemapp/react-native-multi-slider": "^1.1.0",
|
"@esteemapp/react-native-multi-slider": "^1.1.0",
|
||||||
"@esteemapp/react-native-slider": "^0.12.0",
|
"@esteemapp/react-native-slider": "^0.12.0",
|
||||||
"@hiveio/dhive": "^1.0.1",
|
"@hiveio/dhive": "^1.0.1",
|
||||||
|
"@native-html/iframe-plugin": "^2.6.1",
|
||||||
"@react-native-community/async-storage": "^1.11.0",
|
"@react-native-community/async-storage": "^1.11.0",
|
||||||
"@react-native-community/cameraroll": "^1.3.0",
|
"@react-native-community/cameraroll": "^1.3.0",
|
||||||
"@react-native-community/cli-platform-ios": "^4.10.1",
|
"@react-native-community/cli-platform-ios": "^4.10.1",
|
||||||
|
@ -93,6 +93,7 @@ import { ForegroundNotification } from './foregroundNotification';
|
|||||||
import { PostHtmlRenderer } from './postHtmlRenderer';
|
import { PostHtmlRenderer } from './postHtmlRenderer';
|
||||||
import { QuickProfileModal } from './organisms';
|
import { QuickProfileModal } from './organisms';
|
||||||
import QuickReplyModal from './quickReplyModal/quickReplyModalView';
|
import QuickReplyModal from './quickReplyModal/quickReplyModalView';
|
||||||
|
import VideoPlayer from './videoPlayer/videoPlayerView';
|
||||||
|
|
||||||
// Basic UI Elements
|
// Basic UI Elements
|
||||||
import {
|
import {
|
||||||
@ -234,4 +235,5 @@ export {
|
|||||||
PostHtmlRenderer,
|
PostHtmlRenderer,
|
||||||
QuickProfileModal,
|
QuickProfileModal,
|
||||||
QuickReplyModal,
|
QuickReplyModal,
|
||||||
|
VideoPlayer,
|
||||||
};
|
};
|
||||||
|
11
src/components/markdownEditor/view/formats/applyUsername.ts
Normal file
11
src/components/markdownEditor/view/formats/applyUsername.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { extractWordAtIndex } from '../../../../utils/editor';
|
||||||
|
import {replaceBetween } from './utils';
|
||||||
|
|
||||||
|
export default async ({ text, selection, setTextAndSelection, username}) => {
|
||||||
|
const _word = extractWordAtIndex(text, selection.start);
|
||||||
|
const _insertAt = text.indexOf(_word, selection.start - _word.length);
|
||||||
|
const _text = replaceBetween(text, {start:_insertAt, end:_insertAt + _word.length}, `@${username} `)
|
||||||
|
const _newPos = _insertAt + username.length + 2;
|
||||||
|
const _selection = { start: _newPos, end: _newPos };
|
||||||
|
setTextAndSelection({ selection: _selection, text: _text });
|
||||||
|
};
|
@ -110,4 +110,31 @@ export default EStyleSheet.create({
|
|||||||
bottom: 56,
|
bottom: 56,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
searchAccountsContainer: Platform.select({
|
||||||
|
//absolute positioning makes button hide behind keyboard on ios
|
||||||
|
ios: {
|
||||||
|
marginBottom: 12,
|
||||||
|
paddingTop: 8,
|
||||||
|
},
|
||||||
|
//on android the appearing of button was causing momentary glitch with ios variant style
|
||||||
|
android: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 56,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
userBubble: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginHorizontal: 4,
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
paddingVertical: 4,
|
||||||
|
backgroundColor: '$primaryBlue',
|
||||||
|
borderRadius: 24,
|
||||||
|
},
|
||||||
|
userBubbleText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '$white',
|
||||||
|
marginLeft: 6,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@ -46,6 +46,8 @@ import applySnippet from './formats/applySnippet';
|
|||||||
import { MainButton } from '../../mainButton';
|
import { MainButton } from '../../mainButton';
|
||||||
import isAndroidOreo from '../../../utils/isAndroidOreo';
|
import isAndroidOreo from '../../../utils/isAndroidOreo';
|
||||||
import { OptionsModal } from '../../atoms';
|
import { OptionsModal } from '../../atoms';
|
||||||
|
import { UsernameAutofillBar } from './usernameAutofillBar';
|
||||||
|
import applyUsername from './formats/applyUsername';
|
||||||
|
|
||||||
const MIN_BODY_INPUT_HEIGHT = 300;
|
const MIN_BODY_INPUT_HEIGHT = 300;
|
||||||
|
|
||||||
@ -184,6 +186,15 @@ const MarkdownEditorView = ({
|
|||||||
dispatch(toggleAccountsBottomSheet(!isVisibleAccountsBottomSheet));
|
dispatch(toggleAccountsBottomSheet(!isVisibleAccountsBottomSheet));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _onApplyUsername = (username) => {
|
||||||
|
applyUsername({
|
||||||
|
text,
|
||||||
|
selection,
|
||||||
|
setTextAndSelection: _setTextAndSelection,
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const _changeText = useCallback((input) => {
|
const _changeText = useCallback((input) => {
|
||||||
setText(input);
|
setText(input);
|
||||||
@ -449,8 +460,9 @@ const MarkdownEditorView = ({
|
|||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
>
|
>
|
||||||
{isAndroidOreo() ? _renderEditorWithoutScroll() : _renderEditorWithScroll()}
|
{isAndroidOreo() ? _renderEditorWithoutScroll() : _renderEditorWithScroll()}
|
||||||
|
<UsernameAutofillBar text={text} selection={selection} onApplyUsername={_onApplyUsername} />
|
||||||
{_renderFloatingDraftButton()}
|
{_renderFloatingDraftButton()}
|
||||||
|
|
||||||
{!isPreviewActive && _renderEditorButtons()}
|
{!isPreviewActive && _renderEditorButtons()}
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
88
src/components/markdownEditor/view/usernameAutofillBar.tsx
Normal file
88
src/components/markdownEditor/view/usernameAutofillBar.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import React, {useState, useEffect} from 'react';
|
||||||
|
import { View, FlatList, Text, TouchableOpacity } from "react-native"
|
||||||
|
import { UserAvatar } from '../..';
|
||||||
|
import { lookupAccounts } from '../../../providers/hive/dhive';
|
||||||
|
import { extractWordAtIndex } from '../../../utils/editor';
|
||||||
|
import styles from './markdownEditorStyles';
|
||||||
|
import {debounce} from 'lodash';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text:string,
|
||||||
|
selection:{
|
||||||
|
start:number,
|
||||||
|
end:number
|
||||||
|
}
|
||||||
|
onApplyUsername:(username:string)=>void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UsernameAutofillBar = ({text, selection, onApplyUsername}:Props) => {
|
||||||
|
|
||||||
|
const [searchedUsers, setSearchedUsers] = useState([])
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selection.start === selection.end && text) {
|
||||||
|
const word = extractWordAtIndex(text, selection.start);
|
||||||
|
console.log('selection word is: ', word);
|
||||||
|
if (word.startsWith('@') && word.length > 3) {
|
||||||
|
_handleUserSearch(word.substring(1));
|
||||||
|
} else {
|
||||||
|
setSearchedUsers([]);
|
||||||
|
setQuery('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [text, selection])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const _handleUserSearch = debounce(async (username) => {
|
||||||
|
if(query !== username){
|
||||||
|
let users = [];
|
||||||
|
if (username) {
|
||||||
|
setQuery(username)
|
||||||
|
users = await lookupAccounts(username);
|
||||||
|
console.log('result users for', username, users);
|
||||||
|
}
|
||||||
|
setSearchedUsers(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const _onUserSelect = (username) => {
|
||||||
|
onApplyUsername(username)
|
||||||
|
setSearchedUsers([]);
|
||||||
|
setQuery('')
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!searchedUsers || searchedUsers.length === 0 || query === ''){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _renderItem = ({item}:{item:string}) => {
|
||||||
|
|
||||||
|
const username = item;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onPress={()=>{_onUserSelect(username)}}>
|
||||||
|
<View style={styles.userBubble}>
|
||||||
|
<UserAvatar username={username}/>
|
||||||
|
<Text style={styles.userBubbleText}>{username}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.searchAccountsContainer}>
|
||||||
|
<FlatList
|
||||||
|
horizontal={true}
|
||||||
|
data={searchedUsers}
|
||||||
|
keyboardShouldPersistTaps="always"
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
renderItem={_renderItem}
|
||||||
|
keyExtractor={(item)=>`searched-user-${item}`}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
@ -13,7 +13,7 @@ import { navigate } from '../../../../navigation/service';
|
|||||||
// Constants
|
// Constants
|
||||||
import { default as ROUTES } from '../../../../constants/routeNames';
|
import { default as ROUTES } from '../../../../constants/routeNames';
|
||||||
|
|
||||||
import { PostHtmlRenderer, TextButton } from '../../..';
|
import { PostHtmlRenderer, TextButton, VideoPlayer } from '../../..';
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
import styles from './commentBodyStyles';
|
import styles from './commentBodyStyles';
|
||||||
@ -21,14 +21,12 @@ import styles from './commentBodyStyles';
|
|||||||
// Services and Actions
|
// Services and Actions
|
||||||
import { writeToClipboard } from '../../../../utils/clipboard';
|
import { writeToClipboard } from '../../../../utils/clipboard';
|
||||||
import { toastNotification } from '../../../../redux/actions/uiAction';
|
import { toastNotification } from '../../../../redux/actions/uiAction';
|
||||||
import VideoPlayerSheet from './videoPlayerSheet';
|
|
||||||
import { LongPressGestureHandler, State } from 'react-native-gesture-handler';
|
import { LongPressGestureHandler, State } from 'react-native-gesture-handler';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { OptionsModal } from '../../../atoms';
|
import { OptionsModal } from '../../../atoms';
|
||||||
import { useAppDispatch } from '../../../../hooks';
|
import { useAppDispatch } from '../../../../hooks';
|
||||||
import { isCommunity } from '../../../../utils/communityValidation';
|
import { isCommunity } from '../../../../utils/communityValidation';
|
||||||
import { GLOBAL_POST_FILTERS_VALUE } from '../../../../constants/options/filters';
|
import { GLOBAL_POST_FILTERS_VALUE } from '../../../../constants/options/filters';
|
||||||
import { startsWith } from 'core-js/core/string';
|
|
||||||
|
|
||||||
const WIDTH = Dimensions.get('window').width;
|
const WIDTH = Dimensions.get('window').width;
|
||||||
|
|
||||||
@ -337,6 +335,7 @@ const CommentBody = ({
|
|||||||
<PostHtmlRenderer
|
<PostHtmlRenderer
|
||||||
contentWidth={_contentWidth}
|
contentWidth={_contentWidth}
|
||||||
body={body}
|
body={body}
|
||||||
|
isComment={true}
|
||||||
onElementIsImage={_onElementIsImage}
|
onElementIsImage={_onElementIsImage}
|
||||||
setSelectedImage={_handleSetSelectedImage}
|
setSelectedImage={_handleSetSelectedImage}
|
||||||
setSelectedLink={_handleSetSelectedLink}
|
setSelectedLink={_handleSetSelectedLink}
|
||||||
@ -368,7 +367,12 @@ const CommentBody = ({
|
|||||||
setVideoUrl(null);
|
setVideoUrl(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<VideoPlayerSheet youtubeVideoId={youtubeVideoId} videoUrl={videoUrl} startTime={videoStartTime} />
|
<VideoPlayer
|
||||||
|
mode={youtubeVideoId ? 'youtube' : 'url'}
|
||||||
|
youtubeVideoId={youtubeVideoId}
|
||||||
|
videoUrl={videoUrl}
|
||||||
|
startTime={videoStartTime}
|
||||||
|
/>
|
||||||
</ActionsSheetView>
|
</ActionsSheetView>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
@ -15,11 +15,10 @@ import { toastNotification } from '../../../../redux/actions/uiAction';
|
|||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import { default as ROUTES } from '../../../../constants/routeNames';
|
import { default as ROUTES } from '../../../../constants/routeNames';
|
||||||
import VideoPlayerSheet from './videoPlayerSheet';
|
|
||||||
import { OptionsModal } from '../../../atoms';
|
import { OptionsModal } from '../../../atoms';
|
||||||
import { isCommunity } from '../../../../utils/communityValidation';
|
import { isCommunity } from '../../../../utils/communityValidation';
|
||||||
import { GLOBAL_POST_FILTERS_VALUE } from '../../../../constants/options/filters';
|
import { GLOBAL_POST_FILTERS_VALUE } from '../../../../constants/options/filters';
|
||||||
import { PostHtmlRenderer } from '../../..';
|
import { PostHtmlRenderer, VideoPlayer } from '../../..';
|
||||||
|
|
||||||
const WIDTH = Dimensions.get('window').width;
|
const WIDTH = Dimensions.get('window').width;
|
||||||
|
|
||||||
@ -294,7 +293,8 @@ const PostBody = ({ navigation, body, dispatch, onLoadEnd }) => {
|
|||||||
setVideoUrl(null);
|
setVideoUrl(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<VideoPlayerSheet
|
<VideoPlayer
|
||||||
|
mode={youtubeVideoId ? 'youtube' : 'url'}
|
||||||
youtubeVideoId={youtubeVideoId}
|
youtubeVideoId={youtubeVideoId}
|
||||||
videoUrl={videoUrl}
|
videoUrl={videoUrl}
|
||||||
startTime={videoStartTime}
|
startTime={videoStartTime}
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
import React, {useState} from 'react';
|
|
||||||
import { Dimensions } from 'react-native';
|
|
||||||
import { View, StyleSheet, ActivityIndicator } from 'react-native';
|
|
||||||
import WebView from 'react-native-webview';
|
|
||||||
import YoutubeIframe, { InitialPlayerParams } from 'react-native-youtube-iframe';
|
|
||||||
|
|
||||||
interface VideoPlayerSheetProps {
|
|
||||||
youtubeVideoId?:string;
|
|
||||||
videoUrl?:string;
|
|
||||||
startTime?:number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VideoPlayerSheet = ({youtubeVideoId, videoUrl, startTime}: VideoPlayerSheetProps) => {
|
|
||||||
|
|
||||||
const PLAYER_HEIGHT = Dimensions.get('screen').width * (9/16);
|
|
||||||
|
|
||||||
const [shouldPlay, setShouldPlay] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
const _onReady = () => {
|
|
||||||
setLoading(false)
|
|
||||||
setShouldPlay(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const _onChangeState = (event:string) => {
|
|
||||||
setShouldPlay(!(event == 'paused' || event == 'ended'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const _onError = () => {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialParams:InitialPlayerParams = {
|
|
||||||
start:startTime
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
|
|
||||||
{youtubeVideoId &&
|
|
||||||
<YoutubeIframe
|
|
||||||
height={PLAYER_HEIGHT}
|
|
||||||
videoId={youtubeVideoId}
|
|
||||||
initialPlayerParams={initialParams}
|
|
||||||
onReady={_onReady}
|
|
||||||
play={shouldPlay}
|
|
||||||
onChangeState={_onChangeState}
|
|
||||||
onError={_onError}
|
|
||||||
/>
|
|
||||||
|
|
||||||
}{
|
|
||||||
videoUrl &&
|
|
||||||
<View style={{height:PLAYER_HEIGHT}}>
|
|
||||||
<WebView
|
|
||||||
scalesPageToFit={true}
|
|
||||||
bounces={false}
|
|
||||||
javaScriptEnabled={true}
|
|
||||||
automaticallyAdjustContentInsets={false}
|
|
||||||
onLoadEnd={()=>{
|
|
||||||
setLoading(false);
|
|
||||||
}}
|
|
||||||
onLoadStart={()=>{
|
|
||||||
setLoading(true);
|
|
||||||
}}
|
|
||||||
source={{uri:videoUrl}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
}
|
|
||||||
{loading && <ActivityIndicator style={styles.activityIndicator}/>}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VideoPlayerSheet;
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
paddingVertical:16,
|
|
||||||
},
|
|
||||||
activityIndicator: {
|
|
||||||
position:'absolute', alignItems:'center', justifyContent:'center', top:0, bottom:0, left:0, right:0}
|
|
||||||
});
|
|
@ -1,28 +1,33 @@
|
|||||||
import React, { memo } from "react";
|
import React, { memo } from 'react';
|
||||||
import RenderHTML, { CustomRendererProps, Element, TNode } from "react-native-render-html";
|
import RenderHTML, { CustomRendererProps, Element, TNode } from 'react-native-render-html';
|
||||||
import styles from "./postHtmlRendererStyles";
|
import styles from './postHtmlRendererStyles';
|
||||||
import { LinkData, parseLinkData } from "./linkDataParser";
|
import { LinkData, parseLinkData } from './linkDataParser';
|
||||||
import VideoThumb from "./videoThumb";
|
import VideoThumb from './videoThumb';
|
||||||
import { AutoHeightImage } from "../autoHeightImage/autoHeightImage";
|
import { AutoHeightImage } from '../autoHeightImage/autoHeightImage';
|
||||||
|
import { useHtmlIframeProps, iframeModel } from '@native-html/iframe-plugin';
|
||||||
|
import WebView from 'react-native-webview';
|
||||||
|
import { VideoPlayer } from '..';
|
||||||
|
|
||||||
interface PostHtmlRendererProps {
|
interface PostHtmlRendererProps {
|
||||||
contentWidth:number;
|
contentWidth: number;
|
||||||
body:string;
|
body: string;
|
||||||
onLoaded?:()=>void;
|
isComment?: boolean;
|
||||||
setSelectedImage:(imgUrl:string)=>void;
|
onLoaded?: () => void;
|
||||||
setSelectedLink:(url:string)=>void;
|
setSelectedImage: (imgUrl: string) => void;
|
||||||
onElementIsImage:(imgUrl:string)=>void;
|
setSelectedLink: (url: string) => void;
|
||||||
handleOnPostPress:(permlink:string, authro:string)=>void;
|
onElementIsImage: (imgUrl: string) => void;
|
||||||
handleOnUserPress:(username:string)=>void;
|
handleOnPostPress: (permlink: string, authro: string) => void;
|
||||||
handleTagPress:(tag:string, filter?:string)=>void;
|
handleOnUserPress: (username: string) => void;
|
||||||
handleVideoPress:(videoUrl:string)=>void;
|
handleTagPress: (tag: string, filter?: string) => void;
|
||||||
handleYoutubePress:(videoId:string, startTime:number)=>void;
|
handleVideoPress: (videoUrl: string) => void;
|
||||||
|
handleYoutubePress: (videoId: string, startTime: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PostHtmlRenderer = memo(({
|
export const PostHtmlRenderer = memo(
|
||||||
|
({
|
||||||
contentWidth,
|
contentWidth,
|
||||||
body,
|
body,
|
||||||
|
isComment,
|
||||||
onLoaded,
|
onLoaded,
|
||||||
setSelectedImage,
|
setSelectedImage,
|
||||||
setSelectedLink,
|
setSelectedLink,
|
||||||
@ -32,185 +37,174 @@ export const PostHtmlRenderer = memo(({
|
|||||||
handleTagPress,
|
handleTagPress,
|
||||||
handleVideoPress,
|
handleVideoPress,
|
||||||
handleYoutubePress,
|
handleYoutubePress,
|
||||||
}:PostHtmlRendererProps) => {
|
}: PostHtmlRendererProps) => {
|
||||||
|
//new renderer functions
|
||||||
|
body = body.replace(/<center>/g, '<div class="text-center">').replace(/<\/center>/g, '</div>');
|
||||||
|
|
||||||
//new renderer functions
|
console.log('Comment body:', body);
|
||||||
body = body.replace(/<center>/g, '<div class="text-center">').replace(/<\/center>/g,'</div>');
|
|
||||||
|
|
||||||
|
const _handleOnLinkPress = (data: LinkData) => {
|
||||||
console.log("Comment body:", body);
|
if (!data) {
|
||||||
|
return;
|
||||||
const _handleOnLinkPress = (data:LinkData) => {
|
|
||||||
|
|
||||||
if(!data){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
href,
|
|
||||||
author,
|
|
||||||
permlink,
|
|
||||||
tag,
|
|
||||||
youtubeId,
|
|
||||||
startTime,
|
|
||||||
filter,
|
|
||||||
videoHref,
|
|
||||||
community
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case '_external':
|
|
||||||
case 'markdown-external-link':
|
|
||||||
setSelectedLink(href);
|
|
||||||
break;
|
|
||||||
case 'markdown-author-link':
|
|
||||||
if (handleOnUserPress) {
|
|
||||||
handleOnUserPress(author);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'markdown-post-link':
|
|
||||||
if (handleOnPostPress) {
|
|
||||||
handleOnPostPress(permlink, author);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'markdown-tag-link':
|
|
||||||
if(handleTagPress){
|
|
||||||
handleTagPress(tag, filter);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'markdown-video-link':
|
|
||||||
if(handleVideoPress){
|
|
||||||
handleVideoPress(videoHref)
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'markdown-video-link-youtube':
|
|
||||||
if(handleYoutubePress){
|
|
||||||
handleYoutubePress(youtubeId, startTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
//unused cases
|
|
||||||
case 'markdown-witnesses-link':
|
|
||||||
setSelectedLink(href);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'markdown-proposal-link':
|
|
||||||
setSelectedLink(href);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'markdown-community-link':
|
|
||||||
//tag press also handles community by default
|
|
||||||
if(handleTagPress){
|
|
||||||
handleTagPress(community, filter)
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
|
||||||
};
|
const {
|
||||||
|
type,
|
||||||
|
href,
|
||||||
const _onElement = (element:Element) => {
|
author,
|
||||||
if(element.tagName === 'img' && element.attribs.src){
|
permlink,
|
||||||
|
tag,
|
||||||
|
youtubeId,
|
||||||
|
startTime,
|
||||||
|
filter,
|
||||||
|
videoHref,
|
||||||
|
community,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case '_external':
|
||||||
|
case 'markdown-external-link':
|
||||||
|
setSelectedLink(href);
|
||||||
|
break;
|
||||||
|
case 'markdown-author-link':
|
||||||
|
if (handleOnUserPress) {
|
||||||
|
handleOnUserPress(author);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'markdown-post-link':
|
||||||
|
if (handleOnPostPress) {
|
||||||
|
handleOnPostPress(permlink, author);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'markdown-tag-link':
|
||||||
|
if (handleTagPress) {
|
||||||
|
handleTagPress(tag, filter);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'markdown-video-link':
|
||||||
|
if (handleVideoPress) {
|
||||||
|
handleVideoPress(videoHref);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'markdown-video-link-youtube':
|
||||||
|
if (handleYoutubePress) {
|
||||||
|
handleYoutubePress(youtubeId, startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
//unused cases
|
||||||
|
case 'markdown-witnesses-link':
|
||||||
|
setSelectedLink(href);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'markdown-proposal-link':
|
||||||
|
setSelectedLink(href);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'markdown-community-link':
|
||||||
|
//tag press also handles community by default
|
||||||
|
if (handleTagPress) {
|
||||||
|
handleTagPress(community, filter);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _onElement = (element: Element) => {
|
||||||
|
if (element.tagName === 'img' && element.attribs.src) {
|
||||||
const imgUrl = element.attribs.src;
|
const imgUrl = element.attribs.src;
|
||||||
console.log("img element detected", imgUrl);
|
console.log('img element detected', imgUrl);
|
||||||
onElementIsImage(imgUrl)
|
onElementIsImage(imgUrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _anchorRenderer = ({ InternalRenderer, tnode, ...props }: CustomRendererProps<TNode>) => {
|
||||||
const _anchorRenderer = ({
|
const parsedTnode = parseLinkData(tnode);
|
||||||
InternalRenderer,
|
|
||||||
tnode,
|
|
||||||
...props
|
|
||||||
}:CustomRendererProps<TNode>) => {
|
|
||||||
|
|
||||||
const _onPress = () => {
|
const _onPress = () => {
|
||||||
console.log("Link Pressed:", tnode)
|
console.log('Link Pressed:', tnode);
|
||||||
const data = parseLinkData(tnode);
|
const data = parseLinkData(tnode);
|
||||||
_handleOnLinkPress(data);
|
_handleOnLinkPress(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//process video link
|
||||||
if(tnode.classes?.indexOf('markdown-video-link') >= 0){
|
if(tnode.classes?.indexOf('markdown-video-link') >= 0){
|
||||||
const imgElement = tnode.children.find((child)=>{
|
if(isComment){
|
||||||
return child.classes.indexOf('video-thumbnail') > 0 ? true:false
|
const imgElement = tnode.children.find((child) => {
|
||||||
})
|
return child.classes.indexOf('video-thumbnail') > 0 ? true : false;
|
||||||
if(!imgElement){
|
});
|
||||||
|
if (!imgElement) {
|
||||||
|
return <VideoThumb contentWidth={contentWidth} onPress={_onPress} />;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return (
|
return (
|
||||||
<VideoThumb contentWidth={contentWidth} onPress={_onPress} />
|
<VideoPlayer
|
||||||
)
|
mode={parsedTnode.youtubeId ? 'youtube' : 'url'}
|
||||||
|
contentWidth={contentWidth}
|
||||||
|
videoUrl={parsedTnode.videoHref}
|
||||||
|
youtubeVideoId={parsedTnode.youtubeId}
|
||||||
|
startTime={parsedTnode.startTime}
|
||||||
|
disableAutoplay={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return <InternalRenderer tnode={tnode} onPress={_onPress} {...props} />;
|
||||||
<InternalRenderer
|
};
|
||||||
tnode={tnode}
|
|
||||||
onPress={_onPress}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//this method checks if image is a child of table column
|
//this method checks if image is a child of table column
|
||||||
//and calculates img width accordingly,
|
//and calculates img width accordingly,
|
||||||
//returns full width if img is not part of table
|
//returns full width if img is not part of table
|
||||||
const getMaxImageWidth = (tnode:TNode)=>{
|
const getMaxImageWidth = (tnode: TNode) => {
|
||||||
|
|
||||||
//return full width if not parent exist
|
//return full width if not parent exist
|
||||||
if(!tnode.parent || tnode.parent.tagName === 'body'){
|
if (!tnode.parent || tnode.parent.tagName === 'body') {
|
||||||
return contentWidth;
|
return contentWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
//return divided width based on number td tags
|
//return divided width based on number td tags
|
||||||
if(tnode.parent.tagName === 'td'){
|
if (tnode.parent.tagName === 'td') {
|
||||||
const cols = tnode.parent.parent.children.length
|
const cols = tnode.parent.parent.children.length;
|
||||||
return contentWidth/cols;
|
return contentWidth / cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
//check next parent
|
//check next parent
|
||||||
return getMaxImageWidth(tnode.parent);
|
return getMaxImageWidth(tnode.parent);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const _imageRenderer = ({ tnode }: CustomRendererProps<TNode>) => {
|
||||||
const _imageRenderer = ({
|
|
||||||
tnode,
|
|
||||||
}:CustomRendererProps<TNode>) => {
|
|
||||||
|
|
||||||
const imgUrl = tnode.attributes.src;
|
const imgUrl = tnode.attributes.src;
|
||||||
const _onPress = () => {
|
const _onPress = () => {
|
||||||
console.log("Image Pressed:", imgUrl)
|
console.log('Image Pressed:', imgUrl);
|
||||||
setSelectedImage(imgUrl);
|
setSelectedImage(imgUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isVideoThumb = tnode.classes?.indexOf('video-thumbnail') >= 0;
|
const isVideoThumb = tnode.classes?.indexOf('video-thumbnail') >= 0;
|
||||||
const isAnchored = tnode.parent?.tagName === 'a';
|
const isAnchored = tnode.parent?.tagName === 'a';
|
||||||
|
|
||||||
|
|
||||||
if(isVideoThumb){
|
if (isVideoThumb) {
|
||||||
return <VideoThumb contentWidth={contentWidth} uri={imgUrl}/>;
|
return <VideoThumb contentWidth={contentWidth} uri={imgUrl} />;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
const maxImgWidth = getMaxImageWidth(tnode);
|
const maxImgWidth = getMaxImageWidth(tnode);
|
||||||
return (
|
return (
|
||||||
<AutoHeightImage
|
<AutoHeightImage
|
||||||
contentWidth={maxImgWidth}
|
contentWidth={maxImgWidth}
|
||||||
imgUrl={imgUrl}
|
imgUrl={imgUrl}
|
||||||
isAnchored={isAnchored}
|
isAnchored={isAnchored}
|
||||||
onPress={_onPress}
|
onPress={_onPress}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the para renderer is designd to remove margins from para
|
* the para renderer is designd to remove margins from para
|
||||||
@ -218,58 +212,89 @@ export const PostHtmlRenderer = memo(({
|
|||||||
* a weired misalignment of bullet and content
|
* a weired misalignment of bullet and content
|
||||||
* @returns Default Renderer
|
* @returns Default Renderer
|
||||||
*/
|
*/
|
||||||
const _paraRenderer = ({
|
const _paraRenderer = ({ TDefaultRenderer, ...props }: CustomRendererProps<TNode>) => {
|
||||||
TDefaultRenderer,
|
props.style = props.tnode.parent.tagName === 'li' ? styles.pLi : styles.p;
|
||||||
...props
|
|
||||||
}:CustomRendererProps<TNode>) => {
|
|
||||||
|
|
||||||
props.style = props.tnode.parent.tagName === 'li'
|
return <TDefaultRenderer {...props} />;
|
||||||
? styles.pLi
|
};
|
||||||
: styles.p
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TDefaultRenderer
|
// iframe renderer for rendering iframes in body
|
||||||
{...props}
|
const _iframeRenderer = function IframeRenderer(props) {
|
||||||
/>
|
const iframeProps = useHtmlIframeProps(props);
|
||||||
)
|
const checkSrcRegex = /(.*?)\.(mp4|webm|ogg)$/gi;
|
||||||
}
|
const isVideoFormat = iframeProps.source.uri.match(checkSrcRegex);
|
||||||
|
|
||||||
|
//this hack help avoid autoplaying fullscreened iframe videos;
|
||||||
return (
|
const src = isVideoFormat ?
|
||||||
<RenderHTML
|
{
|
||||||
source={{ html:body }}
|
html: `
|
||||||
contentWidth={contentWidth}
|
<video width="100%" height="auto" controls>
|
||||||
baseStyle={{...styles.baseStyle, width:contentWidth}}
|
<source src="${iframeProps.source.uri}" type="video/mp4">
|
||||||
classesStyles={{
|
</video>`,
|
||||||
phishy:styles.phishy,
|
}:{
|
||||||
'text-justify':styles.textJustify,
|
uri: iframeProps.source.uri,
|
||||||
'text-center':styles.textCenter
|
};
|
||||||
}}
|
|
||||||
tagsStyles={{
|
return (
|
||||||
body:styles.body,
|
<VideoPlayer
|
||||||
a:styles.a,
|
mode='source'
|
||||||
img:styles.img,
|
source={src}
|
||||||
th:styles.th,
|
contentWidth={contentWidth}
|
||||||
tr:{...styles.tr, width:contentWidth}, //center tag causes tr to have 0 width if not exclusivly set, contentWidth help avoid that
|
/>
|
||||||
td:styles.td,
|
);
|
||||||
blockquote:styles.blockquote,
|
};
|
||||||
code:styles.code,
|
|
||||||
li:styles.li,
|
return (
|
||||||
p:styles.p,
|
<RenderHTML
|
||||||
table:styles.table,
|
source={{ html: body }}
|
||||||
}}
|
contentWidth={contentWidth}
|
||||||
domVisitors={{
|
baseStyle={{ ...styles.baseStyle, width: contentWidth }}
|
||||||
onElement:_onElement
|
classesStyles={{
|
||||||
}}
|
phishy: styles.phishy,
|
||||||
renderers={{
|
'text-justify': styles.textJustify,
|
||||||
img:_imageRenderer,
|
'text-center': styles.textCenter,
|
||||||
a:_anchorRenderer,
|
}}
|
||||||
p:_paraRenderer
|
tagsStyles={{
|
||||||
}}
|
body: styles.body,
|
||||||
onHTMLLoaded={onLoaded && onLoaded}
|
a: styles.a,
|
||||||
defaultTextProps={{
|
img: styles.img,
|
||||||
selectable:true
|
th: styles.th,
|
||||||
}}
|
tr: { ...styles.tr, width: contentWidth }, //center tag causes tr to have 0 width if not exclusivly set, contentWidth help avoid that
|
||||||
/>
|
td: styles.td,
|
||||||
)
|
blockquote: styles.blockquote,
|
||||||
}, (next, prev)=>next.body === prev.body)
|
code: styles.code,
|
||||||
|
li: styles.li,
|
||||||
|
p: styles.p,
|
||||||
|
table: styles.table,
|
||||||
|
}}
|
||||||
|
domVisitors={{
|
||||||
|
onElement: _onElement,
|
||||||
|
}}
|
||||||
|
renderers={{
|
||||||
|
img: _imageRenderer,
|
||||||
|
a: _anchorRenderer,
|
||||||
|
p: _paraRenderer,
|
||||||
|
iframe: _iframeRenderer,
|
||||||
|
}}
|
||||||
|
onHTMLLoaded={onLoaded && onLoaded}
|
||||||
|
defaultTextProps={{
|
||||||
|
selectable: true,
|
||||||
|
}}
|
||||||
|
customHTMLElementModels={{
|
||||||
|
iframe: iframeModel,
|
||||||
|
}}
|
||||||
|
renderersProps={{
|
||||||
|
iframe: {
|
||||||
|
scalesPageToFit: true,
|
||||||
|
webViewProps: {
|
||||||
|
/* Any prop you want to pass to iframe WebViews */
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
WebView={WebView}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(next, prev) => next.body === prev.body,
|
||||||
|
);
|
||||||
|
5
src/components/videoPlayer/videoPlayerStyles.ts
Normal file
5
src/components/videoPlayer/videoPlayerStyles.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||||
|
|
||||||
|
export default EStyleSheet.create({
|
||||||
|
|
||||||
|
});
|
112
src/components/videoPlayer/videoPlayerView.tsx
Normal file
112
src/components/videoPlayer/videoPlayerView.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Dimensions } from 'react-native';
|
||||||
|
import { View, StyleSheet, ActivityIndicator } from 'react-native';
|
||||||
|
import WebView from 'react-native-webview';
|
||||||
|
import YoutubeIframe, { InitialPlayerParams } from 'react-native-youtube-iframe';
|
||||||
|
import { WebViewSource } from 'react-native-webview/lib/WebViewTypes';
|
||||||
|
|
||||||
|
interface VideoPlayerProps {
|
||||||
|
mode: 'source'|'youtube'|'url';
|
||||||
|
contentWidth?: number;
|
||||||
|
youtubeVideoId?: string;
|
||||||
|
videoUrl?: string;
|
||||||
|
startTime?: number;
|
||||||
|
source?: WebViewSource;
|
||||||
|
|
||||||
|
//prop for youtube player
|
||||||
|
disableAutoplay?:boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoPlayer = ({
|
||||||
|
youtubeVideoId,
|
||||||
|
videoUrl,
|
||||||
|
startTime,
|
||||||
|
source,
|
||||||
|
contentWidth = Dimensions.get('screen').width,
|
||||||
|
mode,
|
||||||
|
disableAutoplay
|
||||||
|
}: VideoPlayerProps) => {
|
||||||
|
|
||||||
|
const PLAYER_HEIGHT = contentWidth * (9 / 16);
|
||||||
|
|
||||||
|
const [shouldPlay, setShouldPlay] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const _onReady = () => {
|
||||||
|
setLoading(false);
|
||||||
|
setShouldPlay(disableAutoplay ? false : true);
|
||||||
|
console.log('ready');
|
||||||
|
};
|
||||||
|
|
||||||
|
const _onChangeState = (event: string) => {
|
||||||
|
console.log(event);
|
||||||
|
setShouldPlay(!(event == 'paused' || event == 'ended'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const _onError = () => {
|
||||||
|
console.log('error!');
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialParams: InitialPlayerParams = {
|
||||||
|
start: startTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{mode === 'youtube' && youtubeVideoId && (
|
||||||
|
<View style={{ width: contentWidth, height: PLAYER_HEIGHT }}>
|
||||||
|
<YoutubeIframe
|
||||||
|
height={PLAYER_HEIGHT}
|
||||||
|
videoId={youtubeVideoId}
|
||||||
|
initialPlayerParams={initialParams}
|
||||||
|
onReady={_onReady}
|
||||||
|
play={shouldPlay}
|
||||||
|
onChangeState={_onChangeState}
|
||||||
|
onError={_onError}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{((mode === 'source' && source) || (mode === 'url' && videoUrl)) && (
|
||||||
|
<View style={{ height: PLAYER_HEIGHT }}>
|
||||||
|
<WebView
|
||||||
|
scalesPageToFit={true}
|
||||||
|
bounces={false}
|
||||||
|
javaScriptEnabled={true}
|
||||||
|
automaticallyAdjustContentInsets={false}
|
||||||
|
onLoadEnd={() => {
|
||||||
|
setLoading(false);
|
||||||
|
}}
|
||||||
|
onLoadStart={() => {
|
||||||
|
setLoading(true);
|
||||||
|
}}
|
||||||
|
source={source || { uri: videoUrl }}
|
||||||
|
style={{ width: contentWidth, height: PLAYER_HEIGHT}}
|
||||||
|
startInLoadingState={true}
|
||||||
|
onShouldStartLoadWithRequest={() => true}
|
||||||
|
mediaPlaybackRequiresUserAction={true}
|
||||||
|
allowsInlineMediaPlayback={true}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{loading && <ActivityIndicator style={styles.activityIndicator} />}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VideoPlayer;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
paddingVertical: 16,
|
||||||
|
},
|
||||||
|
activityIndicator: {
|
||||||
|
position: 'absolute',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
});
|
@ -44,6 +44,28 @@ export const generatePermlink = (title, random = false) => {
|
|||||||
return perm;
|
return perm;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const extractWordAtIndex = (text:string, index:number) => {
|
||||||
|
const END_REGEX = /[\s,]/
|
||||||
|
let word = '';
|
||||||
|
for(let i = index; i >= 0 && (!END_REGEX.test(text[i]) || i === index); i--){
|
||||||
|
if(text[i]){
|
||||||
|
word += text[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
word = word.split('').reverse().join('');
|
||||||
|
|
||||||
|
if(!END_REGEX.test(text[index])){
|
||||||
|
for(let i = index + 1; i < text.length && !END_REGEX.test(text[i]); i++){
|
||||||
|
if(text[i]){
|
||||||
|
word += text[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return word;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export const generateReplyPermlink = (toAuthor) => {
|
export const generateReplyPermlink = (toAuthor) => {
|
||||||
if (!toAuthor) {
|
if (!toAuthor) {
|
||||||
return '';
|
return '';
|
||||||
|
25
yarn.lock
25
yarn.lock
@ -1118,6 +1118,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz#2dc8c57044de0340eb53a7ba602e59abf80dc799"
|
resolved "https://registry.yarnpkg.com/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz#2dc8c57044de0340eb53a7ba602e59abf80dc799"
|
||||||
integrity sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==
|
integrity sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==
|
||||||
|
|
||||||
|
"@formidable-webview/webshell@^2.6.0":
|
||||||
|
version "2.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@formidable-webview/webshell/-/webshell-2.6.0.tgz#64704c0b513206e71b23118b3c9d096f0d545005"
|
||||||
|
integrity sha512-FwQQDajg1xs7W3CUiUNJMvdjgLjKLDGzs0XPzoVg0Dunhold1Jg7w5pihUdvVugFlNtkSpXMA+du9QDHE8lmpg==
|
||||||
|
|
||||||
"@hapi/address@2.x.x":
|
"@hapi/address@2.x.x":
|
||||||
version "2.1.4"
|
version "2.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
||||||
@ -1412,6 +1417,21 @@
|
|||||||
css-to-react-native "^3.0.0"
|
css-to-react-native "^3.0.0"
|
||||||
csstype "^3.0.8"
|
csstype "^3.0.8"
|
||||||
|
|
||||||
|
"@native-html/iframe-plugin@^2.6.1":
|
||||||
|
version "2.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@native-html/iframe-plugin/-/iframe-plugin-2.6.1.tgz#5b9c36d9d500f82f0bcf654bc005922df4211158"
|
||||||
|
integrity sha512-PM2vFNT44n/UkCm9+OUn+cNSKgiMjaw7c7/2JnnztHDLVMtPIf52K/86miWNpQpxFoy1ouoLVOvfjFRhoPXjag==
|
||||||
|
dependencies:
|
||||||
|
"@formidable-webview/webshell" "^2.6.0"
|
||||||
|
"@native-html/plugins-core" "1.3.0"
|
||||||
|
"@types/prop-types" "^15.7.4"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
|
"@native-html/plugins-core@1.3.0":
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@native-html/plugins-core/-/plugins-core-1.3.0.tgz#f1f24622097551930d9dab0214c4929d00f7446e"
|
||||||
|
integrity sha512-vce35gqGJKa2oPDZVa2sKjucFFVK+3g8quLayeXiJtj5LzuS8TWGrBFTS5O4ToUtE02AJkCDOLwAguCzK2HWdQ==
|
||||||
|
|
||||||
"@native-html/transient-render-engine@^9.2.2":
|
"@native-html/transient-render-engine@^9.2.2":
|
||||||
version "9.2.2"
|
version "9.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@native-html/transient-render-engine/-/transient-render-engine-9.2.2.tgz#00691518926ea47709185c3a25a786472c99a1f0"
|
resolved "https://registry.yarnpkg.com/@native-html/transient-render-engine/-/transient-render-engine-9.2.2.tgz#00691518926ea47709185c3a25a786472c99a1f0"
|
||||||
@ -1784,6 +1804,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
|
||||||
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
|
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
|
||||||
|
|
||||||
|
"@types/prop-types@^15.7.4":
|
||||||
|
version "15.7.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
|
||||||
|
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
|
||||||
|
|
||||||
"@types/ramda@^0.27.40":
|
"@types/ramda@^0.27.40":
|
||||||
version "0.27.44"
|
version "0.27.44"
|
||||||
resolved "https://registry.yarnpkg.com/@types/ramda/-/ramda-0.27.44.tgz#ba2283d67fcff366f7e68bd5124a0466e467967f"
|
resolved "https://registry.yarnpkg.com/@types/ramda/-/ramda-0.27.44.tgz#ba2283d67fcff366f7e68bd5124a0466e467967f"
|
||||||
|
Loading…
Reference in New Issue
Block a user