mirror of
https://github.com/ecency/ecency-mobile.git
synced 2024-12-22 12:51:42 +03:00
Merge branch 'sa/post-video-render' of https://github.com/ecency/ecency-mobile into sa/post-video-render
This commit is contained in:
commit
e8d0f67619
@ -93,6 +93,7 @@ import { ForegroundNotification } from './foregroundNotification';
|
||||
import { PostHtmlRenderer } from './postHtmlRenderer';
|
||||
import { QuickProfileModal } from './organisms';
|
||||
import QuickReplyModal from './quickReplyModal/quickReplyModalView';
|
||||
import VideoPlayer from './videoPlayer/videoPlayerView';
|
||||
|
||||
// Basic UI Elements
|
||||
import {
|
||||
@ -234,4 +235,5 @@ export {
|
||||
PostHtmlRenderer,
|
||||
QuickProfileModal,
|
||||
QuickReplyModal,
|
||||
VideoPlayer,
|
||||
};
|
||||
|
@ -1,27 +1,31 @@
|
||||
import React, { memo, } from "react";
|
||||
import RenderHTML, { CustomRendererProps, Element, TNode } from "react-native-render-html";
|
||||
import styles from "./postHtmlRendererStyles";
|
||||
import { LinkData, parseLinkData } from "./linkDataParser";
|
||||
import VideoThumb from "./videoThumb";
|
||||
import { AutoHeightImage } from "../autoHeightImage/autoHeightImage";
|
||||
import { useHtmlIframeProps,iframeModel } from '@native-html/iframe-plugin';
|
||||
import WebView from "react-native-webview";
|
||||
import React, { memo } from 'react';
|
||||
import RenderHTML, { CustomRendererProps, Element, TNode } from 'react-native-render-html';
|
||||
import styles from './postHtmlRendererStyles';
|
||||
import { LinkData, parseLinkData } from './linkDataParser';
|
||||
import VideoThumb from './videoThumb';
|
||||
import { AutoHeightImage } from '../autoHeightImage/autoHeightImage';
|
||||
import { useHtmlIframeProps, iframeModel } from '@native-html/iframe-plugin';
|
||||
import WebView from 'react-native-webview';
|
||||
import { View } from 'react-native';
|
||||
import YoutubeIframe from 'react-native-youtube-iframe';
|
||||
import { VideoPlayer } from '..';
|
||||
|
||||
interface PostHtmlRendererProps {
|
||||
contentWidth:number;
|
||||
body:string;
|
||||
onLoaded?:()=>void;
|
||||
setSelectedImage:(imgUrl:string)=>void;
|
||||
setSelectedLink:(url:string)=>void;
|
||||
onElementIsImage:(imgUrl:string)=>void;
|
||||
handleOnPostPress:(permlink:string, authro:string)=>void;
|
||||
handleOnUserPress:(username:string)=>void;
|
||||
handleTagPress:(tag:string, filter?:string)=>void;
|
||||
handleVideoPress:(videoUrl:string)=>void;
|
||||
handleYoutubePress:(videoId:string, startTime:number)=>void;
|
||||
contentWidth: number;
|
||||
body: string;
|
||||
onLoaded?: () => void;
|
||||
setSelectedImage: (imgUrl: string) => void;
|
||||
setSelectedLink: (url: string) => void;
|
||||
onElementIsImage: (imgUrl: string) => void;
|
||||
handleOnPostPress: (permlink: string, authro: string) => void;
|
||||
handleOnUserPress: (username: string) => void;
|
||||
handleTagPress: (tag: string, filter?: string) => void;
|
||||
handleVideoPress: (videoUrl: string) => void;
|
||||
handleYoutubePress: (videoId: string, startTime: number) => void;
|
||||
}
|
||||
|
||||
export const PostHtmlRenderer = memo(({
|
||||
export const PostHtmlRenderer = memo(
|
||||
({
|
||||
contentWidth,
|
||||
body,
|
||||
onLoaded,
|
||||
@ -33,209 +37,174 @@ export const PostHtmlRenderer = memo(({
|
||||
handleTagPress,
|
||||
handleVideoPress,
|
||||
handleYoutubePress,
|
||||
}:PostHtmlRendererProps) => {
|
||||
}: PostHtmlRendererProps) => {
|
||||
//new renderer functions
|
||||
body = body.replace(/<center>/g, '<div class="text-center">').replace(/<\/center>/g, '</div>');
|
||||
|
||||
//new renderer functions
|
||||
body = body.replace(/<center>/g, '<div class="text-center">').replace(/<\/center>/g,'</div>');
|
||||
console.log('Comment body:', body);
|
||||
|
||||
console.log("Comment body:", body);
|
||||
|
||||
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;
|
||||
const _handleOnLinkPress = (data: LinkData) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
|
||||
const _onElement = (element:Element) => {
|
||||
if(element.tagName === 'img' && element.attribs.src){
|
||||
|
||||
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 _onElement = (element: Element) => {
|
||||
if (element.tagName === 'img' && element.attribs.src) {
|
||||
const imgUrl = element.attribs.src;
|
||||
console.log("img element detected", imgUrl);
|
||||
onElementIsImage(imgUrl)
|
||||
console.log('img element detected', imgUrl);
|
||||
onElementIsImage(imgUrl);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const _anchorRenderer = ({
|
||||
InternalRenderer,
|
||||
tnode,
|
||||
...props
|
||||
}:CustomRendererProps<TNode>) => {
|
||||
|
||||
const _anchorRenderer = ({ InternalRenderer, tnode, ...props }: CustomRendererProps<TNode>) => {
|
||||
const parsedTnode = parseLinkData(tnode);
|
||||
const _onPress = () => {
|
||||
console.log("Link Pressed:", tnode)
|
||||
console.log('Link Pressed:', tnode);
|
||||
const data = parseLinkData(tnode);
|
||||
_handleOnLinkPress(data);
|
||||
};
|
||||
|
||||
if (tnode.classes?.indexOf('markdown-video-link') >= 0) {
|
||||
// get video src
|
||||
let videoHref = tnode.attributes['data-embed-src'] || tnode.attributes['data-video-href'] || tnode.children[0].attributes['src'];
|
||||
|
||||
if (tnode.classes?.indexOf('markdown-video-link-youtube') >= 0) {
|
||||
return (
|
||||
<WebView
|
||||
scalesPageToFit={true}
|
||||
bounces={false}
|
||||
javaScriptEnabled={true}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
onLoadEnd={() => {
|
||||
console.log('load end');
|
||||
}}
|
||||
onLoadStart={() => {
|
||||
console.log('load start');
|
||||
}}
|
||||
source={{ uri: videoHref }}
|
||||
style={{ width: contentWidth, height: (contentWidth * 9) / 16 }}
|
||||
startInLoadingState={true}
|
||||
onShouldStartLoadWithRequest={() => true}
|
||||
mediaPlaybackRequiresUserAction={true}
|
||||
allowsInlineMediaPlayback={true}
|
||||
<VideoPlayer
|
||||
contentWidth={contentWidth}
|
||||
youtubeVideoId={parsedTnode.youtubeId}
|
||||
startTime={parsedTnode.startTime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if(tnode.classes?.indexOf('markdown-video-link') >= 0){
|
||||
const imgElement = tnode.children.find((child)=>{
|
||||
return child.classes.indexOf('video-thumbnail') > 0 ? true:false
|
||||
})
|
||||
if(!imgElement){
|
||||
return (
|
||||
<VideoThumb contentWidth={contentWidth} onPress={_onPress} />
|
||||
)
|
||||
if (tnode.classes?.indexOf('markdown-video-link') >= 0) {
|
||||
return (
|
||||
<VideoPlayer
|
||||
contentWidth={contentWidth}
|
||||
videoUrl={parsedTnode.videoHref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (tnode.classes?.indexOf('markdown-video-link') >= 0) {
|
||||
const imgElement = tnode.children.find((child) => {
|
||||
return child.classes.indexOf('video-thumbnail') > 0 ? true : false;
|
||||
});
|
||||
if (!imgElement) {
|
||||
return <VideoThumb contentWidth={contentWidth} onPress={_onPress} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<InternalRenderer
|
||||
tnode={tnode}
|
||||
onPress={_onPress}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <InternalRenderer tnode={tnode} onPress={_onPress} {...props} />;
|
||||
};
|
||||
|
||||
//this method checks if image is a child of table column
|
||||
//and calculates img width accordingly,
|
||||
//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
|
||||
if(!tnode.parent || tnode.parent.tagName === 'body'){
|
||||
if (!tnode.parent || tnode.parent.tagName === 'body') {
|
||||
return contentWidth;
|
||||
}
|
||||
|
||||
//return divided width based on number td tags
|
||||
if(tnode.parent.tagName === 'td'){
|
||||
const cols = tnode.parent.parent.children.length
|
||||
return contentWidth/cols;
|
||||
if (tnode.parent.tagName === 'td') {
|
||||
const cols = tnode.parent.parent.children.length;
|
||||
return contentWidth / cols;
|
||||
}
|
||||
|
||||
//check next parent
|
||||
return getMaxImageWidth(tnode.parent);
|
||||
}
|
||||
|
||||
|
||||
const _imageRenderer = ({
|
||||
tnode,
|
||||
}:CustomRendererProps<TNode>) => {
|
||||
|
||||
};
|
||||
|
||||
const _imageRenderer = ({ tnode }: CustomRendererProps<TNode>) => {
|
||||
const imgUrl = tnode.attributes.src;
|
||||
const _onPress = () => {
|
||||
console.log("Image Pressed:", imgUrl)
|
||||
console.log('Image Pressed:', imgUrl);
|
||||
setSelectedImage(imgUrl);
|
||||
};
|
||||
|
||||
|
||||
const isVideoThumb = tnode.classes?.indexOf('video-thumbnail') >= 0;
|
||||
const isAnchored = tnode.parent?.tagName === 'a';
|
||||
|
||||
|
||||
if(isVideoThumb){
|
||||
return <VideoThumb contentWidth={contentWidth} uri={imgUrl}/>;
|
||||
}
|
||||
else {
|
||||
if (isVideoThumb) {
|
||||
return <VideoThumb contentWidth={contentWidth} uri={imgUrl} />;
|
||||
} else {
|
||||
const maxImgWidth = getMaxImageWidth(tnode);
|
||||
return (
|
||||
<AutoHeightImage
|
||||
contentWidth={maxImgWidth}
|
||||
<AutoHeightImage
|
||||
contentWidth={maxImgWidth}
|
||||
imgUrl={imgUrl}
|
||||
isAnchored={isAnchored}
|
||||
onPress={_onPress}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* the para renderer is designd to remove margins from para
|
||||
@ -243,31 +212,18 @@ export const PostHtmlRenderer = memo(({
|
||||
* a weired misalignment of bullet and content
|
||||
* @returns Default Renderer
|
||||
*/
|
||||
const _paraRenderer = ({
|
||||
TDefaultRenderer,
|
||||
...props
|
||||
}:CustomRendererProps<TNode>) => {
|
||||
const _paraRenderer = ({ TDefaultRenderer, ...props }: CustomRendererProps<TNode>) => {
|
||||
props.style = props.tnode.parent.tagName === 'li' ? styles.pLi : styles.p;
|
||||
|
||||
props.style = props.tnode.parent.tagName === 'li'
|
||||
? styles.pLi
|
||||
: styles.p
|
||||
return <TDefaultRenderer {...props} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<TDefaultRenderer
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// iframe renderer for rendering iframes in body
|
||||
const _iframeRenderer = function IframeRenderer(props) {
|
||||
const iframeProps = useHtmlIframeProps(props);
|
||||
// console.log('iframeProps : ', iframeProps);
|
||||
const checkSrcRegex = /(.*?)\.(mp4|webm|ogg)$/gi;
|
||||
const isVideoType = iframeProps.source.uri.match(checkSrcRegex);
|
||||
|
||||
// check if source contain video source then wrap it with video tag
|
||||
// else pass the source directly to webview
|
||||
const src = isVideoType
|
||||
? {
|
||||
html: `
|
||||
@ -300,55 +256,57 @@ export const PostHtmlRenderer = memo(({
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<RenderHTML
|
||||
source={{ html:body }}
|
||||
contentWidth={contentWidth}
|
||||
baseStyle={{...styles.baseStyle, width:contentWidth}}
|
||||
classesStyles={{
|
||||
phishy:styles.phishy,
|
||||
'text-justify':styles.textJustify,
|
||||
'text-center':styles.textCenter
|
||||
}}
|
||||
tagsStyles={{
|
||||
body:styles.body,
|
||||
a:styles.a,
|
||||
img:styles.img,
|
||||
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,
|
||||
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)
|
||||
|
||||
return (
|
||||
<RenderHTML
|
||||
source={{ html: body }}
|
||||
contentWidth={contentWidth}
|
||||
baseStyle={{ ...styles.baseStyle, width: contentWidth }}
|
||||
classesStyles={{
|
||||
phishy: styles.phishy,
|
||||
'text-justify': styles.textJustify,
|
||||
'text-center': styles.textCenter,
|
||||
}}
|
||||
tagsStyles={{
|
||||
body: styles.body,
|
||||
a: styles.a,
|
||||
img: styles.img,
|
||||
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,
|
||||
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({
|
||||
|
||||
});
|
98
src/components/videoPlayer/videoPlayerView.tsx
Normal file
98
src/components/videoPlayer/videoPlayerView.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import React, { useState } from 'react';
|
||||
import style from './videoPlayerStyles';
|
||||
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 VideoPlayerProps {
|
||||
youtubeVideoId?: string;
|
||||
videoUrl?: string;
|
||||
startTime?: number;
|
||||
contentWidth?: number;
|
||||
}
|
||||
|
||||
const VideoPlayer = ({ youtubeVideoId, videoUrl, startTime, contentWidth }: VideoPlayerProps) => {
|
||||
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);
|
||||
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}>
|
||||
{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>
|
||||
)}
|
||||
{videoUrl && (
|
||||
<View style={{ height: PLAYER_HEIGHT }}>
|
||||
<WebView
|
||||
scalesPageToFit={true}
|
||||
bounces={false}
|
||||
javaScriptEnabled={true}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
onLoadEnd={() => {
|
||||
setLoading(false);
|
||||
}}
|
||||
onLoadStart={() => {
|
||||
setLoading(true);
|
||||
}}
|
||||
source={{ uri: videoUrl }}
|
||||
style={{ width: contentWidth, height: (contentWidth * 9) / 16 }}
|
||||
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,
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue
Block a user