diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ebc88464b..6a6f6c417 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -828,4 +828,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 0282022703ad578ab2d9afbf3147ba3b373b4311 -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/package.json b/package.json index 41b31a112..f384fbf4e 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "react-native-iap": "^7.5.6", "react-native-image-crop-picker": "^0.35.2", "react-native-image-zoom-viewer": "^2.2.27", - "react-native-iphone-x-helper": "^1.3.1", + "react-native-iphone-x-helper": "Norcy/react-native-iphone-x-helper", "react-native-keyboard-aware-scroll-view": "^0.9.1", "react-native-level-fs": "^3.0.0", "react-native-linear-gradient": "^2.4.2", diff --git a/src/components/collapsibleCard/view/collapsibleCardView.js b/src/components/collapsibleCard/view/collapsibleCardView.js index b83ef42ab..b7ea00f5f 100644 --- a/src/components/collapsibleCard/view/collapsibleCardView.js +++ b/src/components/collapsibleCard/view/collapsibleCardView.js @@ -1,5 +1,6 @@ import React, { PureComponent } from 'react'; -import { View, TouchableHighlight, Animated } from 'react-native'; +import { View, TouchableHighlight } from 'react-native'; +import Animated, { Easing } from 'react-native-reanimated'; // Constants @@ -50,6 +51,7 @@ class CollapsibleCardView extends PureComponent { Animated.timing(this.anime.height, { toValue: this.anime.expanded ? this._getMinValue() : this._getMaxValue() + (moreHeight || 0), duration: 200, + easing: Easing.inOut(Easing.ease), }).start(); this.anime.expanded = !this.anime.expanded; diff --git a/src/components/draftListItem/view/draftListItemView.tsx b/src/components/draftListItem/view/draftListItemView.tsx index 60c5de868..f4a552d2d 100644 --- a/src/components/draftListItem/view/draftListItemView.tsx +++ b/src/components/draftListItem/view/draftListItemView.tsx @@ -3,6 +3,7 @@ import { View, Text, TouchableOpacity } from 'react-native'; import { injectIntl } from 'react-intl'; // Utils +import FastImage from 'react-native-fast-image'; import { getTimeFromNow } from '../../../utils/time'; // Components @@ -14,8 +15,6 @@ import { OptionsModal } from '../../atoms'; import styles from './draftListItemStyles'; import { ScheduledPostStatus } from '../../../providers/ecency/ecency.types'; import { PopoverWrapper } from '../../popoverWrapper/popoverWrapperView'; -import FastImage from 'react-native-fast-image'; - const DraftListItemView = ({ title, @@ -59,24 +58,24 @@ const DraftListItemView = ({ status === ScheduledPostStatus.PENDING ? intl.formatMessage({ id: 'schedules.pending' }) : status === ScheduledPostStatus.POSTPONED - ? intl.formatMessage({ id: 'schedules.postponed' }) - : status === ScheduledPostStatus.PUBLISHED - ? intl.formatMessage({ id: 'schedules.published' }) - : intl.formatMessage({ id: 'schedules.error' }); + ? intl.formatMessage({ id: 'schedules.postponed' }) + : status === ScheduledPostStatus.PUBLISHED + ? intl.formatMessage({ id: 'schedules.published' }) + : intl.formatMessage({ id: 'schedules.error' }); const statusIcon = status === ScheduledPostStatus.PENDING ? 'timer' : status === ScheduledPostStatus.POSTPONED - ? 'schedule' - : status === ScheduledPostStatus.PUBLISHED - ? 'check-circle' - : 'error'; + ? 'schedule' + : status === ScheduledPostStatus.PUBLISHED + ? 'check-circle' + : 'error'; const statusIconColor = status === ScheduledPostStatus.PUBLISHED ? '#4FD688' : status === ScheduledPostStatus.ERROR - ? '#e63535' - : '#c1c5c7'; + ? '#e63535' + : '#c1c5c7'; return ( diff --git a/src/components/iconButton/view/iconButtonView.js b/src/components/iconButton/view/iconButtonView.js index 359b927ab..f9df79f5d 100644 --- a/src/components/iconButton/view/iconButtonView.js +++ b/src/components/iconButton/view/iconButtonView.js @@ -1,5 +1,6 @@ import React, { Fragment } from 'react'; import { TouchableOpacity, ActivityIndicator } from 'react-native'; +import EStyleSheet from 'react-native-extended-stylesheet'; import { Icon } from '../../icon'; import styles from './iconButtonStyles'; @@ -47,7 +48,10 @@ const IconButton = ({ badgeCount={badgeCount} /> ) : ( - + )} diff --git a/src/components/notification/view/notificationView.tsx b/src/components/notification/view/notificationView.tsx index 2c2738710..a52c10857 100644 --- a/src/components/notification/view/notificationView.tsx +++ b/src/components/notification/view/notificationView.tsx @@ -47,14 +47,13 @@ class NotificationView extends PureComponent { // Component Functions _handleOnDropdownSelect = async (index) => { - const { getActivities, changeSelectedFilter, } = this.props; + const { changeSelectedFilter } = this.props; const { filters, contentOffset } = this.state; const _selectedFilter = filters[index].key; this.setState({ selectedFilter: _selectedFilter, selectedIndex: index, contentOffset }); await changeSelectedFilter(_selectedFilter, index); - getActivities(_selectedFilter, false); this.listRef.current?.scrollToOffset({ x: 0, y: 0, animated: false }); }; @@ -176,34 +175,40 @@ class NotificationView extends PureComponent { return 5; }; - _getActivityIndicator = () => ( ); - _renderSectionHeader = ({ section: { title, index } }) => ( - ) - + ); _renderItem = ({ item }) => ( <> - {item.sectionTitle && } + {item.sectionTitle && ( + + )} { this.props.handleOnUserPress(item.source) }} + handleOnUserPress={() => { + this.props.handleOnUserPress(item.source); + }} globalProps={this.props.globalProps} /> - ) - + ); render() { - const { readAllNotification, getActivities, isNotificationRefreshing, intl, isLoading } = this.props; + const { + readAllNotification, + getActivities, + isNotificationRefreshing, + intl, + isLoading, + } = this.props; const { filters, selectedFilter, selectedIndex } = this.state; const _notifications = this._getNotificationsArrays(); @@ -222,27 +227,28 @@ class NotificationView extends PureComponent { onRightIconPress={readAllNotification} /> - {({ isDarkTheme }) => - + {({ isDarkTheme }) => ( `${item.id}-${index}`} - onEndReached={() => getActivities(selectedFilter, true)} + onEndReached={() => getActivities(true)} onEndReachedThreshold={0.3} ListFooterComponent={this._renderFooterLoading} ListEmptyComponent={ - isLoading ? : ( - - {intl.formatMessage({ id: 'notification.noactivity' })} - + isNotificationRefreshing ? ( + + ) : ( + + {intl.formatMessage({ id: 'notification.noactivity' })} + ) - } + } contentContainerStyle={styles.listContentContainer} refreshControl={ getActivities(selectedFilter)} + onRefresh={() => getActivities()} progressBackgroundColor="#357CE6" tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'} titleColor="#fff" @@ -251,8 +257,7 @@ class NotificationView extends PureComponent { } renderItem={this._renderItem} /> - - } + )} ); diff --git a/src/components/pinAnimatedInput/views/pinAnimatedInputView.js b/src/components/pinAnimatedInput/views/pinAnimatedInputView.js index d24a0b160..92ffc3d99 100644 --- a/src/components/pinAnimatedInput/views/pinAnimatedInputView.js +++ b/src/components/pinAnimatedInput/views/pinAnimatedInputView.js @@ -33,6 +33,7 @@ class PinAnimatedInput extends Component { toValue: 1, duration: 250, easing: Easing.linear, + useNativeDriver: false, //setting it to false as animation is not being used }), ), ]).start((o) => { @@ -87,6 +88,5 @@ class PinAnimatedInput extends Component { ); } } - export default PinAnimatedInput; /* eslint-enable */ diff --git a/src/components/postBoost/postBoostStyles.js b/src/components/postBoost/postBoostStyles.js index aa62377d9..dd45dc09d 100644 --- a/src/components/postBoost/postBoostStyles.js +++ b/src/components/postBoost/postBoostStyles.js @@ -44,7 +44,7 @@ export default EStyleSheet.create({ borderColor: '$borderColor', borderRadius: 8, padding: 2, - color: '$primaryBlack', + // color: '$primaryBlack', width: 172, marginRight: 33, }, diff --git a/src/components/postHtmlRenderer/postHtmlRenderer.tsx b/src/components/postHtmlRenderer/postHtmlRenderer.tsx index 9ccef58f0..16b5f8fd4 100644 --- a/src/components/postHtmlRenderer/postHtmlRenderer.tsx +++ b/src/components/postHtmlRenderer/postHtmlRenderer.tsx @@ -1,10 +1,10 @@ import React, { memo, useMemo } from 'react'; import RenderHTML, { CustomRendererProps, Element, TNode } from 'react-native-render-html'; +import { useHtmlIframeProps, iframeModel } from '@native-html/iframe-plugin'; 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 { VideoPlayer } from '..'; import { useHtmlTableProps } from '@native-html/table-plugin'; @@ -46,7 +46,7 @@ export const PostHtmlRenderer = memo( console.log('Comment body:', body); - const _minTableColWidth = (contentWidth / 3) - 12; + const _minTableColWidth = contentWidth / 3 - 12; const _handleOnLinkPress = (data: LinkData) => { if (!data) { @@ -119,10 +119,9 @@ export const PostHtmlRenderer = memo( default: break; } - } catch (error) { } + } catch (error) {} }; - //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 @@ -142,8 +141,6 @@ export const PostHtmlRenderer = memo( return getMaxImageWidth(tnode.parent); }; - - //Does some needed dom modifications for proper rendering const _onElement = (element: Element) => { if (element.tagName === 'img' && element.attribs.src) { @@ -152,10 +149,9 @@ export const PostHtmlRenderer = memo( onElementIsImage(imgUrl); } - //this avoids invalid rendering of first element of table pushing rest of columsn to extreme right. if (element.tagName === 'table') { - console.log('table detected') + console.log('table detected'); element.children.forEach((child) => { if (child.name === 'tr') { @@ -168,15 +164,15 @@ export const PostHtmlRenderer = memo( if (gChild.name !== 'td' && headerIndex === -1) { headerIndex = index; } else if (colIndex === -1) { - colIndex = index + colIndex = index; } } - }) + }); //if row contans a header with column siblings //remove first child and place it as first separate row in table if (headerIndex !== -1 && colIndex !== -1 && headerIndex < colIndex) { - console.log("time to do some switching", headerIndex, colIndex); + console.log('time to do some switching', headerIndex, colIndex); const header = child.children[headerIndex]; const headerRow = new Element('tr', {}, [header]); @@ -184,13 +180,10 @@ export const PostHtmlRenderer = memo( prependChild(element, headerRow); } } - }) + }); } }; - - - const _anchorRenderer = ({ InternalRenderer, tnode, ...props }: CustomRendererProps) => { const parsedTnode = parseLinkData(tnode); const _onPress = () => { @@ -199,10 +192,8 @@ export const PostHtmlRenderer = memo( _handleOnLinkPress(data); }; - //process video link if (tnode.classes?.indexOf('markdown-video-link') >= 0) { - if (isComment) { const imgElement = tnode.children.find((child) => { return child.classes.indexOf('video-thumbnail') > 0 ? true : false; @@ -226,23 +217,20 @@ export const PostHtmlRenderer = memo( if (tnode.children.length === 1 && tnode.children[0].tagName === 'img') { const maxImgWidth = getMaxImageWidth(tnode); - return + return ( + + ); } - return ; }; - - - - const _imageRenderer = ({ tnode }: CustomRendererProps) => { const imgUrl = tnode.attributes.src; const _onPress = () => { @@ -280,15 +268,15 @@ export const PostHtmlRenderer = memo( return ; }; - //based on number of columns a table have, sets scroll enabled or disable, also adjust table full width const _tableRenderer = ({ InternalRenderer, ...props }: CustomRendererProps) => { // const tableProps = useHtmlTableProps(props); let maxColumns = 0; - props.tnode.children.forEach((child) => - maxColumns = child.children.length > maxColumns ? child.children.length : maxColumns - ) + props.tnode.children.forEach( + (child) => + (maxColumns = child.children.length > maxColumns ? child.children.length : maxColumns), + ); const isScrollable = maxColumns > 3; const _tableWidth = isScrollable ? maxColumns * _minTableColWidth : contentWidth; @@ -298,8 +286,8 @@ export const PostHtmlRenderer = memo( - ) - } + ); + }; // iframe renderer for rendering iframes in body @@ -313,19 +301,10 @@ export const PostHtmlRenderer = memo( handleVideoPress(iframeProps.source.uri); } }; - return ( - - ) + return ; } else { - return ( - - ); + return ; } - }; const tagsStyles = useMemo( @@ -341,68 +320,54 @@ export const PostHtmlRenderer = memo( code: styles.code, li: styles.li, p: styles.p, - h6: styles.h6 + h6: styles.h6, }), - [contentWidth] + [contentWidth], ); - const baseStyle = useMemo( - () => ( - { ...styles.baseStyle, width: contentWidth } - ), - [contentWidth] - ); + const baseStyle = useMemo(() => ({ ...styles.baseStyle, width: contentWidth }), [contentWidth]); const classesStyles = useMemo( - () => ( - { - phishy: styles.phishy, - 'text-justify': styles.textJustify, - 'text-center': styles.textCenter, - } - ), + () => ({ + phishy: styles.phishy, + 'text-justify': styles.textJustify, + 'text-center': styles.textCenter, + }), [], ); const renderers = useMemo( - () => ( - { + () => + ({ img: _imageRenderer, a: _anchorRenderer, p: _paraRenderer, iframe: _iframeRenderer, - table: _tableRenderer - } as any - ), + table: _tableRenderer, + } as any), [], ); const domVisitors = useMemo( - () => ( - { - onElement: _onElement, - } - ), + () => ({ + onElement: _onElement, + }), [], ); const customHTMLElementModels = useMemo( - () => ( - { - iframe: iframeModel, - } - ), + () => ({ + iframe: iframeModel, + }), [], ); const renderersProps = useMemo( - () => ( - { - iframe: { - scalesPageToFit: true - }, - } - ), + () => ({ + iframe: { + scalesPageToFit: true, + }, + }), [], ); diff --git a/src/components/progressiveImage/index.js b/src/components/progressiveImage/index.js index 9006df132..46c3a2c27 100644 --- a/src/components/progressiveImage/index.js +++ b/src/components/progressiveImage/index.js @@ -1,6 +1,7 @@ import React, { useState } from 'react'; -import { View, StyleSheet, Animated } from 'react-native'; +import { View, StyleSheet } from 'react-native'; import FastImage from 'react-native-fast-image'; +import Animated, { Easing } from 'react-native-reanimated'; const styles = StyleSheet.create({ imageOverlay: { @@ -31,12 +32,14 @@ const ProgressiveImage = ({ thumbnailSource, source, style, ...props }) => { }*/ Animated.timing(thumbnailAnimated, { toValue: 1, + easing: Easing.inOut(Easing.ease), }).start(); }; const onImageLoad = () => { Animated.timing(imageAnimated, { toValue: 1, + easing: Easing.inOut(Easing.ease), }).start(); }; diff --git a/src/components/snippetEditorModal/snippetEditorModal.tsx b/src/components/snippetEditorModal/snippetEditorModal.tsx index e5935443a..6d4bb7672 100644 --- a/src/components/snippetEditorModal/snippetEditorModal.tsx +++ b/src/components/snippetEditorModal/snippetEditorModal.tsx @@ -4,166 +4,143 @@ import { Alert, KeyboardAvoidingView, Platform, View } from 'react-native'; import { TextInput } from '..'; import { ThemeContainer } from '../../containers'; import { Snippet } from '../../models'; -import { addFragment, updateFragment} from '../../providers/ecency/ecency'; +import { useSnippetsMutation } from '../../providers/queries'; import { TextButton } from '../buttons'; import Modal from '../modal'; import styles from './snippetEditorModalStyles'; - export interface SnippetEditorModalRef { - showNewModal:()=>void; - showEditModal:(snippet:Snippet)=>void; + showNewModal: () => void; + showEditModal: (snippet: Snippet) => void; } -interface SnippetEditorModalProps { - onSnippetsUpdated:(snips:Array)=>void; -} +const SnippetEditorModal = ({}, ref) => { + const intl = useIntl(); + const titleInputRef = useRef(null); + const bodyInputRef = useRef(null); -const SnippetEditorModal = ({onSnippetsUpdated}: SnippetEditorModalProps, ref) => { - const intl = useIntl(); - const titleInputRef = useRef(null); - const bodyInputRef = useRef(null); + const snippetsMutation = useSnippetsMutation(); - const [title, setTitle] = useState(''); - const [body, setBody] = useState(''); - const [snippetId, setSnippetId] = useState(null); - const [isNewSnippet, setIsNewSnippet] = useState(true); - const [showModal, setShowModal] = useState(false); - const [titleHeight, setTitleHeight] = useState(0) + const [title, setTitle] = useState(''); + const [body, setBody] = useState(''); + const [snippetId, setSnippetId] = useState(null); + const [isNewSnippet, setIsNewSnippet] = useState(true); + const [showModal, setShowModal] = useState(false); + const [titleHeight, setTitleHeight] = useState(0); - useImperativeHandle(ref, () => ({ - showNewModal: () => { - setTitle(''); - setBody(''); - setIsNewSnippet(true); - setShowModal(true); - }, - showEditModal:(snippet:Snippet)=>{ - setSnippetId(snippet.id); - setTitle(snippet.title); - setBody(snippet.body); - setIsNewSnippet(false); - setShowModal(true); - } - })); + useImperativeHandle(ref, () => ({ + showNewModal: () => { + setTitle(''); + setBody(''); + setIsNewSnippet(true); + setShowModal(true); + }, + showEditModal: (snippet: Snippet) => { + setSnippetId(snippet.id); + setTitle(snippet.title); + setBody(snippet.body); + setIsNewSnippet(false); + setShowModal(true); + }, + })); - - //save snippet based on editor type - const _saveSnippet = async () => { - try{ - if(!title || !body){ - Alert.alert(intl.formatMessage({id:'snippets.message_incomplete'})); - return; - } - - let response = []; - if(!isNewSnippet){ - console.log("Updating snippet:", snippetId, title, body) - response = await updateFragment(snippetId, title, body); - console.log("Response from add snippet: ", response) - }else{ - console.log("Saving snippet:", title, body) - const res = await addFragment(title, body) - response = res && res.fragments - console.log("Response from add snippet: ", response) - } - setShowModal(false); - onSnippetsUpdated(response); - - }catch(err){ - Alert.alert(intl.formatMessage({id:'snippets.message_failed'})) - console.warn("Failed to save snippet", err) - } - + //save snippet based on editor type + const _saveSnippet = async () => { + if (!title || !body) { + Alert.alert(intl.formatMessage({ id: 'snippets.message_incomplete' })); + return; } + console.log('Saving snippet:', title, body); - const _renderContent = ( - - {({isDarkTheme})=>( - - + snippetsMutation.mutate({ + id: isNewSnippet ? null : snippetId, + title, + body, + }); - - - { - setTitleHeight(event.nativeEvent.contentSize.height); - }} - onChangeText={setTitle} - value={title} - /> - - - - + setShowModal(false); + }; - - - setShowModal(false)} - style={styles.closeButton} - /> - - - - - )} - - ) + const _renderContent = ( + + {({ isDarkTheme }) => ( + + + + { + setTitleHeight(event.nativeEvent.contentSize.height); + }} + onChangeText={setTitle} + value={title} + /> + + + + + + + setShowModal(false)} + style={styles.closeButton} + /> + + + + )} + + ); return ( - {setShowModal(false)}} - presentationStyle="formSheet" - title={intl.formatMessage({ - id:isNewSnippet - ? 'snippets.title_add_snippet' - : 'snippets.title_edit_snippet' - })} - animationType="slide" - style={styles.modalStyle} - > - {_renderContent} - - + { + setShowModal(false); + }} + presentationStyle="formSheet" + title={intl.formatMessage({ + id: isNewSnippet ? 'snippets.title_add_snippet' : 'snippets.title_edit_snippet', + })} + animationType="slide" + style={styles.modalStyle} + > + {_renderContent} + ); }; export default forwardRef(SnippetEditorModal); - - diff --git a/src/components/snippetsModal/snippetItem.tsx b/src/components/snippetsModal/snippetItem.tsx index 03fc755c0..95f81b444 100644 --- a/src/components/snippetsModal/snippetItem.tsx +++ b/src/components/snippetsModal/snippetItem.tsx @@ -1,43 +1,72 @@ import * as React from 'react'; -import { Text, View, Button } from 'react-native'; +import { useIntl } from 'react-intl'; +import { Alert, Text, View } from 'react-native'; +import { useSnippetDeleteMutation } from '../../providers/queries'; import IconButton from '../iconButton'; import styles from './snippetsModalStyles'; interface SnippetItemProps { - title:string; - body:string; - index:number; - onEditPress:()=>void; - onRemovePress:()=>void; + id: string | null; + title: string; + body: string; + index: number; + onEditPress: () => void; } -const SnippetItem = ({title, body, index, onEditPress, onRemovePress}: SnippetItemProps) => { +const SnippetItem = ({ id, title, body, index, onEditPress }: SnippetItemProps) => { + const intl = useIntl(); + const snippetsDeleteMutation = useSnippetDeleteMutation(); + + const _onRemovePress = () => { + //asks for remvoe confirmation and run remove routing upon confirming + if (id) { + Alert.alert( + intl.formatMessage({ id: 'snippets.title_remove_confirmation' }), + intl.formatMessage({ id: 'snippets.message_remove_confirmation' }), + [ + { + text: intl.formatMessage({ id: 'snippets.btn_cancel' }), + style: 'cancel', + }, + { + text: intl.formatMessage({ id: 'snippets.btn_confirm' }), + onPress: () => snippetsDeleteMutation.mutate(id), + }, + ], + ); + } + }; + return ( - {`${title}`} - - + {`${title}`} + {id && ( + <> + + + + )} - + {`${body}`} - - ) + ); }; -export default SnippetItem; \ No newline at end of file +export default SnippetItem; diff --git a/src/components/snippetsModal/snippetsModal.tsx b/src/components/snippetsModal/snippetsModal.tsx index b2b6ca8cd..c917ed38f 100644 --- a/src/components/snippetsModal/snippetsModal.tsx +++ b/src/components/snippetsModal/snippetsModal.tsx @@ -1,172 +1,106 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { View, FlatList, Text, TouchableOpacity, Alert } from 'react-native'; +import React, { useRef } from 'react'; +import { View, FlatList, Text, TouchableOpacity, Alert, RefreshControl } from 'react-native'; import { useIntl } from 'react-intl'; -import { getFragments, deleteFragment } from '../../providers/ecency/ecency'; +import { deleteFragment } from '../../providers/ecency/ecency'; import { MainButton } from '..'; import styles from './snippetsModalStyles'; -import { RefreshControl } from 'react-native'; -import SnippetEditorModal, { SnippetEditorModalRef } from '../snippetEditorModal/snippetEditorModal'; + +import SnippetEditorModal, { + SnippetEditorModalRef, +} from '../snippetEditorModal/snippetEditorModal'; import SnippetItem from './snippetItem'; import { Snippet } from '../../models'; import { useAppSelector } from '../../hooks'; +import { useSnippetDeleteMutation, useSnippetsQuery } from '../../providers/queries'; interface SnippetsModalProps { - handleOnSelect:(snippetText:string)=>void, + handleOnSelect: (snippetText: string) => void; } -const SnippetsModal = ({ handleOnSelect }:SnippetsModalProps) => { +const SnippetsModal = ({ handleOnSelect }: SnippetsModalProps) => { const editorRef = useRef(null); const intl = useIntl(); - const isLoggedIn = useAppSelector(state => state.application.isLoggedIn) - - const [snippets, setSnippets] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - _getSnippets(); - }, []); - - - - //fetch snippets from server - const _getSnippets = async () => { - try{ - - setIsLoading(true); - const snips = await getFragments() - console.log("snips received", snips) - setSnippets(snips); - setIsLoading(false); - - }catch(err){ - console.warn("Failed to get snippets") - setIsLoading(false); - } - } - - //removes snippet from users snippet collection on user confirmation - const _removeSnippet = async (id:string) => { - try{ - - setIsLoading(true); - const snips = await deleteFragment(id) - setSnippets(snips); - setIsLoading(false); - - }catch(err){ - console.warn("Failed to get snippets") - setIsLoading(false); - } - } - + const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn); + const snippetsQuery = useSnippetsQuery(); //render list item for snippet and handle actions; - const _renderItem = ({ item, index }:{item:Snippet, index:number}) => { - - const _onPress = () => handleOnSelect(item.body) - - //asks for remvoe confirmation and run remove routing upon confirming - const _onRemovePress = () => { - Alert.alert( - intl.formatMessage({id:'snippets.title_remove_confirmation'}), - intl.formatMessage({id:'snippets.message_remove_confirmation'}), - [ - { - text:intl.formatMessage({id:'snippets.btn_cancel'}), - style:'cancel' - }, - { - text:intl.formatMessage({id:'snippets.btn_confirm'}), - onPress:()=>_removeSnippet(item.id) - } - ] - ) - } + const _renderItem = ({ item, index }: { item: Snippet; index: number }) => { + const _onPress = () => handleOnSelect(item.body); const _onEditPress = () => { - if(editorRef.current){ + if (editorRef.current) { editorRef.current.showEditModal(item); } - } + }; return ( - + - ) + ); }; - - //render empty list placeholder const _renderEmptyContent = () => { return ( <> - {intl.formatMessage({id:'snippets.label_no_snippets'})} + {intl.formatMessage({ id: 'snippets.label_no_snippets' })} ); }; - - //renders footer with add snipept button and shows new snippet modal const _renderFloatingButton = () => { - if(!isLoggedIn){ + if (!isLoggedIn) { return null; } - + const _onPress = () => { - if(editorRef.current){ + if (editorRef.current) { editorRef.current.showNewModal(); } - } + }; return ( ); }; - - return ( index.toString()} renderItem={_renderItem} ListEmptyComponent={_renderEmptyContent} refreshControl={ - } /> {_renderFloatingButton()} - - + ); }; diff --git a/src/components/toastNotification/view/toastNotificaitonView.js b/src/components/toastNotification/view/toastNotificaitonView.js index 4606ed037..8dfd04273 100644 --- a/src/components/toastNotification/view/toastNotificaitonView.js +++ b/src/components/toastNotification/view/toastNotificaitonView.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; -import { Animated, TouchableOpacity, Text } from 'react-native'; +import { TouchableOpacity, Text } from 'react-native'; +import { View as AnimatedView } from 'react-native-animatable'; // Styles import styles from './toastNotificationStyles'; @@ -12,70 +13,60 @@ class ToastNotification extends Component { constructor(props) { super(props); - this.state = { - animatedValue: new Animated.Value(0), - }; - } - - // Component Functions - _showToast() { - const { duration } = this.props; - const animatedValue = new Animated.Value(0); - - this.setState({ animatedValue }); - - Animated.timing(animatedValue, { toValue: 1, duration: 350 }).start(); - - if (duration) { - this.closeTimer = setTimeout(() => { - this._hideToast(); - }, duration); - } - } - - _hideToast() { - const { animatedValue } = this.state; - const { onHide } = this.props; - - Animated.timing(animatedValue, { toValue: 0.0, duration: 350 }).start(() => { - if (onHide) { - onHide(); - } - }); - - if (this.closeTimer) { - clearTimeout(this.closeTimer); - } } // Component Life Cycles - UNSAFE_componentWillMount() { + componentDidMount() { this._showToast(); } + handleViewRef = (ref) => (this.view = ref); + + // Component Functions + _showToast = () => { + const { duration, isTop } = this.props; + const initialPosition = isTop ? { top: 0 } : { bottom: 0 }; + const finalPosition = isTop ? { top: 100 } : { bottom: 100 }; + this.view + .animate({ 0: { opacity: 0, ...initialPosition }, 1: { opacity: 1, ...finalPosition } }) + .then((endState) => { + if (duration) { + this.closeTimer = setTimeout(() => { + this._hideToast(); + }, duration); + } + }); + }; + _hideToast = () => { + const { isTop } = this.props; + const finalPosition = isTop ? { top: 0 } : { bottom: 0 }; + const initialPosition = isTop ? { top: 100 } : { bottom: 100 }; + this.view + .animate({ 0: { opacity: 1, ...initialPosition }, 1: { opacity: 0, ...finalPosition } }) + .then((endState) => { + const { onHide } = this.props; + if (onHide) { + onHide(); + } + }); + }; + render() { - const { text, textStyle, style, onPress, isTop } = this.props; - const { animatedValue } = this.state; - const outputRange = isTop ? [-50, 0] : [50, 0]; - const y = animatedValue.interpolate({ - inputRange: [0, 1], - outputRange, - }); - const position = isTop ? { top: 100 } : { bottom: 100 }; + const { text, textStyle, style, onPress } = this.props; return ( onPress && onPress()}> - {text} - + ); } diff --git a/src/components/toggleSwitch/view/toggleSwitchView.js b/src/components/toggleSwitch/view/toggleSwitchView.js index 99862e453..37f55947e 100644 --- a/src/components/toggleSwitch/view/toggleSwitchView.js +++ b/src/components/toggleSwitch/view/toggleSwitchView.js @@ -1,5 +1,6 @@ import React, { PureComponent } from 'react'; -import { View, TouchableOpacity, Animated, NativeModules } from 'react-native'; +import { View, TouchableOpacity, NativeModules } from 'react-native'; +import Animated, { Easing } from 'react-native-reanimated'; // Constants @@ -97,6 +98,7 @@ class ToggleSwitchView extends PureComponent { Animated.timing(this.offsetX, { toValue, duration, + easing: Easing.inOut(Easing.ease), }).start(); }; diff --git a/src/providers/ecency/ecency.ts b/src/providers/ecency/ecency.ts index eddb80bed..531903960 100644 --- a/src/providers/ecency/ecency.ts +++ b/src/providers/ecency/ecency.ts @@ -15,9 +15,11 @@ import { import { CommentHistoryItem, LatestMarketPrices, + NotificationFilters, ReceivedVestingShare, Referral, ReferralStat, + Snippet, } from './ecency.types'; /** @@ -326,7 +328,7 @@ export const deleteFavorite = async (targetUsername: string) => { export const getFragments = async () => { try { const response = await ecencyApi.post('/private-api/fragments'); - return response.data; + return response.data as Snippet[]; } catch (error) { console.warn('Failed to get fragments', error); bugsnagInstance.notify(error); @@ -416,16 +418,9 @@ export const getLeaderboard = async (duration: 'day' | 'week' | 'month') => { * @returns array of notifications */ export const getNotifications = async (data: { - filter?: - | 'rvotes' - | 'mentions' - | 'follows' - | 'replies' - | 'reblogs' - | 'transfers' - | 'delegations' - | 'nfavorites'; + filter?: NotificationFilters; since?: string; + limit?: number; }) => { try { const response = await ecencyApi.post('/private-api/notifications', data); diff --git a/src/providers/ecency/ecency.types.ts b/src/providers/ecency/ecency.types.ts index 571cfe098..79ccb9e7e 100644 --- a/src/providers/ecency/ecency.types.ts +++ b/src/providers/ecency/ecency.types.ts @@ -1,54 +1,61 @@ -import { QuoteItem } from "../../redux/reducers/walletReducer"; +import { QuoteItem } from '../../redux/reducers/walletReducer'; export interface ReceivedVestingShare { - delegator:string; - delegatee:string; - vesting_shares:string; - timestamp:string; + delegator: string; + delegatee: string; + vesting_shares: string; + timestamp: string; +} + +export interface Snippet { + id: string; + title: string; + body: string; + created: string; + modified: string; } export interface EcencyUser { - username:string; - points:string; - unclaimed_points:string; - points_by_type:{[key:string]:string}; - unclaimed_points_by_type:{[key:string]:string}; + username: string; + points: string; + unclaimed_points: string; + points_by_type: { [key: string]: string }; + unclaimed_points_by_type: { [key: string]: string }; } export interface Referral { - id:number; - referral:string; - rewarded:boolean; - username:string; - created:string + id: number; + referral: string; + rewarded: boolean; + username: string; + created: string; } export interface ReferralStat { - total: number; - rewarded: number; + total: number; + rewarded: number; } export interface UserPoint { - id: number; - type: number; - amount: string; - created:string; - memo?: string; - receiver?: string; - sender?: string; - + id: number; + type: number; + amount: string; + created: string; + memo?: string; + receiver?: string; + sender?: string; } export interface LatestQuotes { - [key:string]:QuoteItem + [key: string]: QuoteItem; } export interface CommentHistoryItem { - body: string; - tags: [string]; - title: string; - timestamp:string; - v: number; + body: string; + tags: [string]; + title: string; + timestamp: string; + v: number; } export enum ScheduledPostStatus { @@ -56,4 +63,16 @@ export enum ScheduledPostStatus { POSTPONED = 2, PUBLISHED = 3, ERROR = 4, -} \ No newline at end of file +} + +export enum NotificationFilters { + ACTIVITIES = "activities", + RVOTES = "rvotes", + MENTIONS = "mentions", + FOLLOWS = "follows", + REPLIES = "replies", + REBLOGS = "reblogs", + TRANFERS = "transfers", + DELEGATIONS = "delegations", + FAVOURITES = "nfavorites" +} diff --git a/src/providers/queries/editorQueries.ts b/src/providers/queries/editorQueries.ts new file mode 100644 index 000000000..591f44314 --- /dev/null +++ b/src/providers/queries/editorQueries.ts @@ -0,0 +1,90 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useIntl } from 'react-intl'; +import { useAppDispatch } from '../../hooks'; +import { toastNotification } from '../../redux/actions/uiAction'; +import { addFragment, deleteFragment, getFragments, updateFragment } from '../ecency/ecency'; +import { Snippet } from '../ecency/ecency.types'; +import QUERIES from './queryKeys'; + +interface SnippetMutationVars { + id: string | null; + title: string; + body: string; +} + +export const useSnippetsQuery = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + return useQuery([QUERIES.SNIPPETS.GET], getFragments, { + onError: () => { + dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); + }, + }); +}; + +export const useSnippetsMutation = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const queryClient = useQueryClient(); + + return useMutation( + async (vars) => { + console.log('going to add/update snippet', vars); + if (vars.id) { + const response = await updateFragment(vars.id, vars.title, vars.body); + return response; + } else { + const response = await addFragment(vars.title, vars.body); + return response; + } + }, + { + onMutate: (vars) => { + console.log('mutate snippets for add/update', vars); + + const _newItem = { + id: vars.id, + title: vars.title, + body: vars.body, + created: new Date().toDateString(), + modified: new Date().toDateString(), + } as Snippet; + + const data = queryClient.getQueryData([QUERIES.SNIPPETS.GET]); + + let _newData: Snippet[] = data ? [...data] : []; + if (vars.id) { + const snipIndex = _newData.findIndex((item) => vars.id === item.id); + _newData[snipIndex] = _newItem; + } else { + _newData = [_newItem, ..._newData]; + } + + queryClient.setQueryData([QUERIES.SNIPPETS.GET], _newData); + }, + onSuccess: (data) => { + console.log('added/updated snippet', data); + queryClient.invalidateQueries([QUERIES.SNIPPETS.GET]); + }, + onError: () => { + dispatch(toastNotification(intl.formatMessage({ id: 'snippets.message_failed' }))); + }, + }, + ); +}; + +export const useSnippetDeleteMutation = () => { + const queryClient = useQueryClient(); + const dispatch = useAppDispatch(); + const intl = useIntl(); + return useMutation(deleteFragment, { + retry: 3, + onSuccess: (data) => { + console.log('Success scheduled post delete', data); + queryClient.setQueryData([QUERIES.SNIPPETS.GET], data); + }, + onError: () => { + dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); + }, + }); +}; diff --git a/src/providers/queries/index.ts b/src/providers/queries/index.ts index 9a9705790..bf3b903af 100644 --- a/src/providers/queries/index.ts +++ b/src/providers/queries/index.ts @@ -22,3 +22,7 @@ export const initQueryClient = () => { persistOptions: { persister: asyncStoragePersister }, } as PersistQueryClientProviderProps; }; + +export * from './notificationQueries'; +export * from './draftQueries'; +export * from './editorQueries'; diff --git a/src/providers/queries/notificationQueries.ts b/src/providers/queries/notificationQueries.ts new file mode 100644 index 000000000..40ea235d6 --- /dev/null +++ b/src/providers/queries/notificationQueries.ts @@ -0,0 +1,132 @@ +import { + QueryKey, + useMutation, + UseMutationOptions, + useQueries, + useQueryClient, +} from '@tanstack/react-query'; +import { useState } from 'react'; +import { useIntl } from 'react-intl'; +import bugsnapInstance from '../../config/bugsnag'; +import { useAppDispatch, useAppSelector } from '../../hooks'; +import { updateUnreadActivityCount } from '../../redux/actions/accountAction'; +import { toastNotification } from '../../redux/actions/uiAction'; +import { getNotifications, markNotifications } from '../ecency/ecency'; +import { NotificationFilters } from '../ecency/ecency.types'; +import { markHiveNotifications } from '../hive/dhive'; +import QUERIES from './queryKeys'; + +const FETCH_LIMIT = 20; + +export const useNotificationsQuery = (filter: NotificationFilters) => { + const [isRefreshing, setIsRefreshing] = useState(false); + const [pageParams, setPageParams] = useState(['']); + + const _fetchNotifications = async (pageParam: string) => { + console.log('fetching page since:', pageParam); + const response = await getNotifications({ filter, since: pageParam, limit: FETCH_LIMIT }); + console.log('new page fetched', response); + return response || []; + }; + + const _getNextPageParam = (lastPage: any[]) => { + const lastId = lastPage && lastPage.length ? lastPage.lastItem.id : undefined; + console.log('extracting next page parameter', lastId); + return lastId; + }; + + //query initialization + const notificationQueries = useQueries({ + queries: pageParams.map((pageParam) => ({ + queryKey: [QUERIES.NOTIFICATIONS.GET, filter, pageParam], + queryFn: () => _fetchNotifications(pageParam), + initialData: [], + })), + }); + + const _refresh = async () => { + setIsRefreshing(true); + setPageParams(['']); + await notificationQueries[0].refetch(); + setIsRefreshing(false); + }; + + const _fetchNextPage = () => { + const lastId = _getNextPageParam(notificationQueries.lastItem.data); + if (!pageParams.includes(lastId)) { + pageParams.push(lastId); + setPageParams([...pageParams]); + } + }; + + return { + data: notificationQueries.flatMap((query) => query.data), + isRefreshing, + isLoading: notificationQueries.lastItem.isLoading || notificationQueries.lastItem.isFetching, + fetchNextPage: _fetchNextPage, + refresh: _refresh, + }; +}; + +export const useNotificationReadMutation = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const queryClient = useQueryClient(); + + const currentAccount = useAppSelector((state) => state.account.currentAccount); + const pinCode = useAppSelector((state) => state.application.pin); + + //id is options, if no id is provided program marks all notifications as read; + const _mutationFn = async (id?: string) => { + try { + const response = await markNotifications(id); + console.log('Ecency notifications marked as Read', response); + if (!id) { + await markHiveNotifications(currentAccount, pinCode); + console.log('Hive notifications marked as Read'); + } + + return response.unread || 0; + } catch (err) { + bugsnapInstance.notify(err); + } + }; + + const _options: UseMutationOptions = { + onMutate: async (notificationId) => { + //TODO: find a way to optimise mutations by avoiding too many loops + console.log('on mutate data', notificationId); + + //update query data + const queriesData: [QueryKey, any[] | undefined][] = queryClient.getQueriesData([ + QUERIES.NOTIFICATIONS.GET, + ]); + console.log('query data', queriesData); + + queriesData.forEach(([queryKey, data]) => { + if (data) { + console.log('mutating data', queryKey); + const _mutatedData = data.map((item) => ({ + ...item, + read: !notificationId || notificationId === item.id ? 1 : item.read, + })); + queryClient.setQueryData(queryKey, _mutatedData); + } + }); + }, + + onSuccess: async (unreadCount, notificationId) => { + console.log('on success data', unreadCount); + + dispatch(updateUnreadActivityCount(unreadCount)); + if (!notificationId) { + queryClient.invalidateQueries([QUERIES.NOTIFICATIONS.GET]); + } + }, + onError: () => { + dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); + }, + }; + + return useMutation(_mutationFn, _options); +}; diff --git a/src/providers/queries/queryKeys.ts b/src/providers/queries/queryKeys.ts index f0195924d..3241e2d49 100644 --- a/src/providers/queries/queryKeys.ts +++ b/src/providers/queries/queryKeys.ts @@ -5,6 +5,12 @@ const QUERIES = { SCHEDULES: { GET: 'QUERY_GET_SCHEDULES', }, + NOTIFICATIONS:{ + GET: 'QERUY_GET_NOTIFICATIONS' + }, + SNIPPETS: { + GET: 'QUERY_GET_SNIPPETS', + } }; export default QUERIES; diff --git a/src/redux/store/store.ts b/src/redux/store/store.ts index 8d15beac3..59e930dc3 100644 --- a/src/redux/store/store.ts +++ b/src/redux/store/store.ts @@ -2,38 +2,39 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; import { persistStore, persistReducer, createTransform } from 'redux-persist'; import AsyncStorage from '@react-native-community/async-storage'; +import createMigrate from 'redux-persist/es/createMigrate'; import Reactotron from '../../../reactotron-config'; import reducer from '../reducers'; -import createMigrate from 'redux-persist/es/createMigrate'; import MigrationHelpers from '../../utils/migrationHelpers'; const transformCacheVoteMap = createTransform( - (inboundState:any) => ({ - ...inboundState, - votes : Array.from(inboundState.votes), - comments : Array.from(inboundState.comments), - drafts : Array.from(inboundState.drafts), - subscribedCommunities: Array.from(inboundState.subscribedCommunities) + (inboundState: any) => ({ + ...inboundState, + votes: Array.from(inboundState.votes), + comments: Array.from(inboundState.comments), + drafts: Array.from(inboundState.drafts), + subscribedCommunities: Array.from(inboundState.subscribedCommunities), }), - (outboundState) => ({ - ...outboundState, - votes:new Map(outboundState.votes), - comments:new Map(outboundState.comments), + (outboundState) => ({ + ...outboundState, + votes: new Map(outboundState.votes), + comments: new Map(outboundState.comments), drafts: new Map(outboundState.drafts), - subscribedCommunities: new Map(outboundState.subscribedCommunities) + subscribedCommunities: new Map(outboundState.subscribedCommunities), }), - {whitelist:['cache']} + { whitelist: ['cache'] }, ); const transformWalkthroughMap = createTransform( - (inboundState:any) => ({ ...inboundState, walkthroughMap : Array.from(inboundState.walkthroughMap)}), - (outboundState) => ({ ...outboundState, walkthroughMap:new Map(outboundState.walkthroughMap)}), - {whitelist:['walkthrough']} + (inboundState: any) => ({ + ...inboundState, + walkthroughMap: Array.from(inboundState.walkthroughMap), + }), + (outboundState) => ({ ...outboundState, walkthroughMap: new Map(outboundState.walkthroughMap) }), + { whitelist: ['walkthrough'] }, ); - - // Middleware: Redux Persist Config const persistConfig = { // Root @@ -44,11 +45,8 @@ const persistConfig = { // Blacklist (Don't Save Specific Reducers) blacklist: ['communities', 'user', 'ui'], timeout: 0, - transforms:[ - transformCacheVoteMap, - transformWalkthroughMap - ], - migrate: createMigrate(MigrationHelpers.reduxMigrations, {debug:false}) + transforms: [transformCacheVoteMap, transformWalkthroughMap], + migrate: createMigrate(MigrationHelpers.reduxMigrations, { debug: false }), }; // Middleware: Redux Persist Persisted Reducer @@ -66,8 +64,7 @@ const persistor = persistStore(store); export { store, persistor }; - // Infer the `RootState` and `AppDispatch` types from the store itself -export type RootState = ReturnType +export type RootState = ReturnType; // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} -export type AppDispatch = typeof store.dispatch +export type AppDispatch = typeof store.dispatch; diff --git a/src/screens/application/container/applicationContainer.tsx b/src/screens/application/container/applicationContainer.tsx index 072d30f70..cf7f673cb 100644 --- a/src/screens/application/container/applicationContainer.tsx +++ b/src/screens/application/container/applicationContainer.tsx @@ -100,7 +100,7 @@ class ApplicationContainer extends Component { super(props); this.state = { isRenderRequire: true, - isIos: Platform.OS !== 'android', + // isIos: Platform.OS !== 'android', appState: AppState.currentState, foregroundNotificationData: null, }; @@ -145,7 +145,7 @@ class ApplicationContainer extends Component { ); }; - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps) { const { isGlobalRenderRequired, dispatch } = this.props; if (isGlobalRenderRequired !== prevProps.isGlobalRenderRequired && isGlobalRenderRequired) { @@ -218,7 +218,7 @@ class ApplicationContainer extends Component { }; _handleDeepLink = async (url = '') => { - const { currentAccount, intl } = this.props; + const { currentAccount } = this.props; if (!url) { return; @@ -308,6 +308,7 @@ class ApplicationContainer extends Component { if (appState.match(/inactive|background/) && nextAppState === 'active') { this._refreshGlobalProps(); + this._refreshUnreadActivityCount(); if (_isPinCodeOpen && this._pinCodeTimer) { clearTimeout(this._pinCodeTimer); } @@ -356,7 +357,7 @@ class ApplicationContainer extends Component { const type = get(push, 'type', ''); const fullPermlink = get(push, 'permlink1', '') + get(push, 'permlink2', '') + get(push, 'permlink3', ''); - const username = get(push, 'target', ''); + // const username = get(push, 'target', ''); const activity_id = get(push, 'id', ''); switch (type) { @@ -487,8 +488,14 @@ class ApplicationContainer extends Component { actions.fetchCoinQuotes(); }; + _refreshUnreadActivityCount = async () => { + const { dispatch } = this.props as any; + const unreadActivityCount = await getUnreadNotificationCount(); + dispatch(updateUnreadActivityCount(unreadActivityCount)); + }; + _getUserDataFromRealm = async () => { - const { dispatch, pinCode, isPinCodeOpen: _isPinCodeOpen, isConnected } = this.props; + const { dispatch, isPinCodeOpen: _isPinCodeOpen, isConnected } = this.props; let realmData = []; const res = await getAuthStatus(); @@ -643,8 +650,12 @@ class ApplicationContainer extends Component { //update notification settings and update push token for each signed accoutn useing access tokens _registerDeviceForNotifications = (settings?: any) => { - const { currentAccount, otherAccounts, notificationDetails, isNotificationsEnabled } = - this.props; + const { + currentAccount, + otherAccounts, + notificationDetails, + isNotificationsEnabled, + } = this.props; const isEnabled = settings ? !!settings.notification : isNotificationsEnabled; settings = settings || notificationDetails; diff --git a/src/screens/drafts/container/draftsContainer.tsx b/src/screens/drafts/container/draftsContainer.tsx index 044ef9270..e8b811d8d 100644 --- a/src/screens/drafts/container/draftsContainer.tsx +++ b/src/screens/drafts/container/draftsContainer.tsx @@ -9,7 +9,7 @@ import { useGetSchedulesQuery, useMoveScheduleToDraftsMutation, useScheduleDeleteMutation, -} from '../../../providers/queries/draftQueries'; +} from '../../../providers/queries'; // Middleware @@ -52,14 +52,11 @@ const DraftsContainer = ({ currentAccount, navigation, route }) => { }; const _editDraft = (id: string) => { - const selectedDraft = drafts.find((draft) => draft._id === id); - navigation.navigate({ name: ROUTES.SCREENS.EDITOR, key: `editor_draft_${id}`, params: { - draft: selectedDraft, - fetchPost: refetchDrafts, + draftId: id, }, }); }; diff --git a/src/screens/editor/container/editorContainer.tsx b/src/screens/editor/container/editorContainer.tsx index e389284fb..f9f0f1b69 100644 --- a/src/screens/editor/container/editorContainer.tsx +++ b/src/screens/editor/container/editorContainer.tsx @@ -8,8 +8,7 @@ import { isArray } from 'lodash'; // Services and Actions import { Buffer } from 'buffer'; - -import { useQueryClient } from '@tanstack/react-query'; +import { QueryClient, useQueryClient } from '@tanstack/react-query'; import { addDraft, updateDraft, getDrafts, addSchedule } from '../../../providers/ecency/ecency'; import { toastNotification, setRcOffer } from '../../../redux/actions/uiAction'; import { @@ -22,6 +21,7 @@ import { // Constants import { default as ROUTES } from '../../../constants/routeNames'; + // Utilities import { generatePermlink, @@ -34,6 +34,7 @@ import { extractImageUrls, } from '../../../utils/editor'; // import { generateSignature } from '../../../utils/image'; + // Component import EditorScreen from '../screen/editorScreen'; import { removeBeneficiaries, setBeneficiaries } from '../../../redux/actions/editorActions'; @@ -85,7 +86,7 @@ class EditorContainer extends Component { // Component Life Cycle Functions componentDidMount() { this._isMounted = true; - const { currentAccount, route } = this.props; + const { currentAccount, route, queryClient } = this.props; const username = currentAccount && currentAccount.name ? currentAccount.name : ''; let isReply; let draftId; @@ -98,16 +99,22 @@ class EditorContainer extends Component { const navigationParams = route.params; hasSharedIntent = navigationParams.hasSharedIntent; - if (navigationParams.draft) { - _draft = navigationParams.draft; + if (navigationParams.draftId) { + draftId = navigationParams.draftId; + const cachedDrafts: any = queryClient.getQueryData([QUERIES.DRAFTS.GET]); - // this._loadMeta(_draft); + if (cachedDrafts && cachedDrafts.length) { + //get draft from query cache + const _draft = cachedDrafts.find((draft) => draft._id === draftId); - this.setState({ - draftId: _draft._id, - }); - this._getStorageDraft(username, isReply, _draft); + this.setState({ + draftId, + }); + + this._getStorageDraft(username, isReply, _draft); + } } + if (navigationParams.community) { this.setState({ community: navigationParams.community, @@ -240,9 +247,9 @@ class EditorContainer extends Component { : paramDraft.tags.split(','); this.setState({ draftPost: { - title: paramDraft.title, - body: paramDraft.body, - tags: _tags, + title: paramDraft.title || '', + body: paramDraft.body || '', + tags: _tags || [], meta: paramDraft.meta ? paramDraft.meta : null, }, draftId: paramDraft._id, diff --git a/src/screens/notification/container/notificationContainer.js b/src/screens/notification/container/notificationContainer.js deleted file mode 100644 index 53adf7681..000000000 --- a/src/screens/notification/container/notificationContainer.js +++ /dev/null @@ -1,239 +0,0 @@ -/* eslint-disable react/no-unused-state */ -import React, { Component } from 'react'; -import { Alert } from 'react-native'; -import { connect } from 'react-redux'; -import get from 'lodash/get'; -import { injectIntl } from 'react-intl'; - -// Actions and Services -import { unionBy } from 'lodash'; -import { getNotifications, markNotifications } from '../../../providers/ecency/ecency'; -import { updateUnreadActivityCount } from '../../../redux/actions/accountAction'; - -// Constants -import ROUTES from '../../../constants/routeNames'; - -// Components -import NotificationScreen from '../screen/notificationScreen'; -import { showProfileModal } from '../../../redux/actions/uiAction'; -import { markHiveNotifications } from '../../../providers/hive/dhive'; -import bugsnapInstance from '../../../config/bugsnag'; - -class NotificationContainer extends Component { - constructor(props) { - super(props); - this.state = { - notificationsMap: new Map(), - lastNotificationId: null, - isRefreshing: true, - isLoading: false, - selectedFilter: 'activities', - endOfNotification: false, - selectedIndex: 0, - }; - } - - componentDidMount() { - const { isConnected } = this.props; - if (isConnected) { - this._getActivities(); - } - } - - _getActivities = (type = 'activities', loadMore = false, loadUnread = false) => { - const { lastNotificationId, endOfNotification, isLoading, notificationsMap } = this.state; - const since = loadMore ? lastNotificationId : null; - - if (isLoading) { - return; - } - - if (!endOfNotification || !loadMore || loadUnread) { - this.setState({ - isRefreshing: !loadMore, - isLoading: true, - }); - getNotifications({ filter: type, since: since, limit: 20 }) - .then((res) => { - const lastId = res.length > 0 ? [...res].pop().id : null; - - if (loadMore && (lastId === lastNotificationId || res.length === 0)) { - this.setState({ - endOfNotification: true, - isRefreshing: false, - isLoading: false, - }); - } else { - console.log(''); - const stateNotifications = notificationsMap.get(type) || []; - const _notifications = loadMore - ? unionBy(stateNotifications, res, 'id') - : loadUnread - ? unionBy(res, stateNotifications, 'id') - : res; - notificationsMap.set(type, _notifications); - this.setState({ - notificationsMap, - lastNotificationId: lastId, - isRefreshing: false, - isLoading: false, - }); - } - }) - .catch(() => this.setState({ isRefreshing: false, isLoading: false })); - } - }; - - _navigateToNotificationRoute = (data) => { - const { navigation, dispatch } = this.props; - const type = get(data, 'type'); - const permlink = get(data, 'permlink'); - const author = get(data, 'author'); - let routeName; - let params; - let key; - if (data && !data.read) { - markNotifications(data.id).then((result) => { - const { unread } = result; - dispatch(updateUnreadActivityCount(unread)); - }); - } - - if (permlink && author) { - routeName = ROUTES.SCREENS.POST; - key = permlink; - params = { - author, - permlink, - }; - } else if (type === 'follow') { - routeName = ROUTES.SCREENS.PROFILE; - key = get(data, 'follower'); - params = { - username: get(data, 'follower'), - }; - } else if (type === 'transfer') { - routeName = ROUTES.TABBAR.WALLET; - } else if (type === 'spin') { - routeName = ROUTES.SCREENS.BOOST; - } else if (type === 'inactive') { - routeName = ROUTES.SCREENS.EDITOR; - } - - if (routeName) { - navigation.navigate({ - name: routeName, - params, - key, - }); - } - }; - - _handleOnUserPress = (username) => { - const { dispatch } = this.props; - dispatch(showProfileModal(username)); - }; - - _readAllNotification = () => { - const { dispatch, intl, isConnected, currentAccount, pinCode } = this.props; - const { notificationsMap } = this.state; - - if (!isConnected) { - return; - } - - this.setState({ isRefreshing: true }); - - markNotifications() - .then(() => { - notificationsMap.forEach((notifications, key) => { - const updatedNotifications = notifications.map((item) => ({ ...item, read: 1 })); - notificationsMap.set(key, updatedNotifications); - }); - - dispatch(updateUnreadActivityCount(0)); - markHiveNotifications(currentAccount, pinCode) - .then(() => { - console.log('Hive notifications marked as Read'); - }) - .catch((err) => { - bugsnapInstance.notify(err); - }); - this.setState({ notificationsMap, isRefreshing: false }); - }) - .catch(() => { - Alert.alert( - intl.formatMessage({ id: 'alert.error' }), - intl.formatMessage({ d: 'alert.unknow_error' }), - ); - this.setState({ isRefreshing: false }); - }); - }; - - _handleOnPressLogin = () => { - const { navigation } = this.props; - - navigation.navigate(ROUTES.SCREENS.LOGIN); - }; - - _changeSelectedFilter = async (value, ind) => { - this.setState({ selectedFilter: value, endOfNotification: false, selectedIndex: ind }); - }; - - UNSAFE_componentWillReceiveProps(nextProps) { - const { selectedFilter, notificationsMap } = this.state; - const { currentAccount } = this.props; - if (currentAccount && nextProps.currentAccount) { - if (nextProps.currentAccount.name !== currentAccount.name) { - this.setState( - { - endOfNotification: false, - notificationsMap: new Map(), - }, - () => this._getActivities(selectedFilter), - ); - } else if ( - nextProps.currentAccount.unread_activity_count > currentAccount.unread_activity_count - ) { - notificationsMap.forEach((value, key) => { - console.log('fetching new activities for ', key); - this._getActivities(key, false, true); - }); - } - } - } - - render() { - const { isLoggedIn, globalProps } = this.props; - const { notificationsMap, selectedFilter, isRefreshing, isLoading } = this.state; - - const _notifications = notificationsMap.get(selectedFilter) || []; - return ( - - ); - } -} - -const mapStateToProps = (state) => ({ - isLoggedIn: state.application.isLoggedIn, - isConnected: state.application.isConnected, - pinCode: state.application.pin, - currentAccount: state.account.currentAccount, - globalProps: state.account.globalProps, - activeBottomTab: state.ui.activeBottomTab, -}); - -export default injectIntl(connect(mapStateToProps)(NotificationContainer)); -/* eslint-enable */ diff --git a/src/screens/notification/container/notificationContainer.tsx b/src/screens/notification/container/notificationContainer.tsx new file mode 100644 index 000000000..4608f4c59 --- /dev/null +++ b/src/screens/notification/container/notificationContainer.tsx @@ -0,0 +1,150 @@ +/* eslint-disable react/no-unused-state */ +import React, { useRef, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import get from 'lodash/get'; + +// Actions and Services +import { useEffect } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; + +// Constants +import ROUTES from '../../../constants/routeNames'; + +// Components +import NotificationScreen from '../screen/notificationScreen'; +import { showProfileModal } from '../../../redux/actions/uiAction'; +import { useAppSelector } from '../../../hooks'; +import { useNotificationReadMutation, useNotificationsQuery } from '../../../providers/queries'; +import { NotificationFilters } from '../../../providers/ecency/ecency.types'; +import QUERIES from '../../../providers/queries/queryKeys'; + +const NotificationContainer = ({ navigation }) => { + const dispatch = useDispatch(); + const queryClient = useQueryClient(); + + const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn); + const isConnected = useAppSelector((state) => state.application.isConnected); + const currentAccount = useAppSelector((state) => state.account.currentAccount); + const globalProps = useAppSelector((state) => state.account.globalProps); + + const unreadCountRef = useRef(currentAccount.unread_acitivity_count || 0); + const curUsername = useRef(currentAccount.username); + + const notificationReadMutation = useNotificationReadMutation(); + const allNotificationsQuery = useNotificationsQuery(NotificationFilters.ACTIVITIES); + const repliesNotificationsQuery = useNotificationsQuery(NotificationFilters.REPLIES); + const mentiosnNotificationsQuery = useNotificationsQuery(NotificationFilters.MENTIONS); + + const [selectedFilter, setSelectedFilter] = useState(NotificationFilters.ACTIVITIES); + + const selectedQuery = + selectedFilter === NotificationFilters.REPLIES + ? repliesNotificationsQuery + : selectedFilter === NotificationFilters.MENTIONS + ? mentiosnNotificationsQuery + : allNotificationsQuery; + + useEffect(() => { + if (curUsername.current !== currentAccount.username) { + queryClient.removeQueries([QUERIES.NOTIFICATIONS.GET]); + selectedQuery.refresh(); + curUsername.current = currentAccount.useranme; + } + }, [currentAccount.username]); + + useEffect(() => { + if (currentAccount.unread_activity_count > unreadCountRef.current) { + queryClient.invalidateQueries([QUERIES.NOTIFICATIONS.GET]); + //TODO: fetch new notifications instead + } + unreadCountRef.current = currentAccount.unread_activity_count; + }, [currentAccount.unread_activity_count]); + + const _getActivities = (loadMore = false) => { + if (loadMore) { + console.log('load more notifications'); + selectedQuery.fetchNextPage(); + } else { + console.log('refreshing'); + selectedQuery.refresh(); + } + }; + + const _navigateToNotificationRoute = (data) => { + const type = get(data, 'type'); + const permlink = get(data, 'permlink'); + const author = get(data, 'author'); + let routeName; + let params; + let key; + if (data && !data.read) { + notificationReadMutation.mutate(data.id); + } + + if (permlink && author) { + routeName = ROUTES.SCREENS.POST; + key = permlink; + params = { + author, + permlink, + }; + } else if (type === 'follow') { + routeName = ROUTES.SCREENS.PROFILE; + key = get(data, 'follower'); + params = { + username: get(data, 'follower'), + }; + } else if (type === 'transfer') { + routeName = ROUTES.TABBAR.WALLET; + } else if (type === 'spin') { + routeName = ROUTES.SCREENS.BOOST; + } else if (type === 'inactive') { + routeName = ROUTES.SCREENS.EDITOR; + } + + if (routeName) { + navigation.navigate({ + name: routeName, + params, + key, + }); + } + }; + + const _handleOnUserPress = (username) => { + dispatch(showProfileModal(username)); + }; + + //TODO: handle mark as read mutations + const _readAllNotification = () => { + if (!isConnected) { + return; + } + notificationReadMutation.mutate(); + }; + + const _handleOnPressLogin = () => { + navigation.navigate(ROUTES.SCREENS.LOGIN); + }; + + const _notifications = selectedQuery.data; + + return ( + + ); +}; + +export default NotificationContainer; +/* eslint-enable */ diff --git a/src/utils/migrationHelpers.ts b/src/utils/migrationHelpers.ts index cf9ba5d77..c5c9c4492 100644 --- a/src/utils/migrationHelpers.ts +++ b/src/utils/migrationHelpers.ts @@ -5,195 +5,178 @@ import Config from 'react-native-config'; import THEME_OPTIONS from '../constants/options/theme'; import { getUnreadNotificationCount } from '../providers/ecency/ecency'; import { getPointsSummary } from '../providers/ecency/ePoint'; -import { migrateToMasterKeyWithAccessToken, refreshSCToken, updatePinCode } from '../providers/hive/auth'; +import { + migrateToMasterKeyWithAccessToken, + refreshSCToken, + updatePinCode, +} from '../providers/hive/auth'; import { getMutes } from '../providers/hive/dhive'; import AUTH_TYPE from '../constants/authType'; // Services -import { - getSettings, getUserDataWithUsername, -} from '../realm/realm'; +import { getSettings, getUserDataWithUsername } from '../realm/realm'; import { updateCurrentAccount } from '../redux/actions/accountAction'; import { - isDarkTheme, - changeNotificationSettings, - changeAllNotificationSettings, - setApi, - setCurrency, - setLanguage, - setNsfw, - isDefaultFooter, - isPinCodeOpen, - setColorTheme, - setSettingsMigrated, - setPinCode, - setEncryptedUnlockPin, - setPostUpvotePercent, - setCommentUpvotePercent, + isDarkTheme, + changeNotificationSettings, + changeAllNotificationSettings, + setApi, + setCurrency, + setLanguage, + setNsfw, + isDefaultFooter, + isPinCodeOpen, + setColorTheme, + setSettingsMigrated, + setPinCode, + setEncryptedUnlockPin, + setPostUpvotePercent, + setCommentUpvotePercent, } from '../redux/actions/applicationActions'; import { fetchSubscribedCommunities } from '../redux/actions/communitiesAction'; import { - hideActionModal, - hideProfileModal, - setRcOffer, - toastNotification, + hideActionModal, + hideProfileModal, + setRcOffer, + toastNotification, } from '../redux/actions/uiAction'; import { decryptKey, encryptKey } from './crypto'; - //migrates settings from realm to redux once and do no user realm for settings again; export const migrateSettings = async (dispatch: any, settingsMigratedV2: boolean) => { + if (settingsMigratedV2) { + return; + } - if (settingsMigratedV2) { - return; + //reset certain properties + dispatch(hideActionModal()); + dispatch(hideProfileModal()); + dispatch(toastNotification('')); + dispatch(setRcOffer(false)); + + const settings = await getSettings(); + + if (settings) { + const isDarkMode = Appearance.getColorScheme() === 'dark'; + dispatch(isDarkTheme(settings.isDarkTheme !== null ? settings.isDarkTheme : isDarkMode)); + dispatch(setColorTheme(THEME_OPTIONS.findIndex((item) => item.value === settings.isDarkTheme))); + await dispatch(isPinCodeOpen(!!settings.isPinCodeOpen)); + if (settings.language !== '') dispatch(setLanguage(settings.language)); + if (settings.server !== '') dispatch(setApi(settings.server)); + if (settings.upvotePercent !== '') { + const percent = Number(settings.upvotePercent); + dispatch(setPostUpvotePercent(percent)); + dispatch(setCommentUpvotePercent(percent)); + } + if (settings.isDefaultFooter !== '') dispatch(isDefaultFooter(settings.isDefaultFooter)); //TODO: remove as not being used + + if (settings.nsfw !== '') dispatch(setNsfw(settings.nsfw)); + + dispatch(setCurrency(settings.currency !== '' ? settings.currency : 'usd')); + + if (settings.notification !== '') { + dispatch( + changeNotificationSettings({ + type: 'notification', + action: settings.notification, + }), + ); + + dispatch(changeAllNotificationSettings(settings)); } - //reset certain properties - dispatch(hideActionModal()); - dispatch(hideProfileModal()); - dispatch(toastNotification('')); - dispatch(setRcOffer(false)); - - - const settings = await getSettings(); - - if (settings) { - const isDarkMode = Appearance.getColorScheme() === 'dark'; - dispatch(isDarkTheme(settings.isDarkTheme !== null ? settings.isDarkTheme : isDarkMode)); - dispatch(setColorTheme(THEME_OPTIONS.findIndex(item => item.value === settings.isDarkTheme))); - await dispatch(isPinCodeOpen(!!settings.isPinCodeOpen)); - if (settings.language !== '') dispatch(setLanguage(settings.language)); - if (settings.server !== '') dispatch(setApi(settings.server)); - if (settings.upvotePercent !== '') { - const percent = Number(settings.upvotePercent); - dispatch(setPostUpvotePercent(percent)); - dispatch(setCommentUpvotePercent(percent)); - } - if (settings.isDefaultFooter !== '') dispatch(isDefaultFooter(settings.isDefaultFooter)); //TODO: remove as not being used - - - if (settings.nsfw !== '') dispatch(setNsfw(settings.nsfw)); - - dispatch(setCurrency(settings.currency !== '' ? settings.currency : 'usd')); - - if (settings.notification !== '') { - dispatch( - changeNotificationSettings({ - type: 'notification', - action: settings.notification, - }), - ); - - dispatch(changeAllNotificationSettings(settings)); - } - - await dispatch(setSettingsMigrated(true)) - } -} - - + await dispatch(setSettingsMigrated(true)); + } +}; //migrates local user data to use default pin encruption instead of user pin encryption export const migrateUserEncryption = async (dispatch, currentAccount, encUserPin, onFailure) => { + const oldPinCode = decryptKey(encUserPin, Config.PIN_KEY); - const oldPinCode = decryptKey(encUserPin, Config.PIN_KEY); + if (oldPinCode === undefined || oldPinCode === Config.DEFAULT_PIN) { + return; + } - if (oldPinCode === undefined || oldPinCode === Config.DEFAULT_PIN) { - return; + try { + const pinData = { + pinCode: Config.DEFAULT_PIN, + username: currentAccount.username, + oldPinCode, + }; + + const response = updatePinCode(pinData); + + const _currentAccount = currentAccount; + _currentAccount.local = response; + + dispatch( + updateCurrentAccount({ + ..._currentAccount, + }), + ); + + const encryptedPin = encryptKey(Config.DEFAULT_PIN, Config.PIN_KEY); + dispatch(setPinCode(encryptedPin)); + } catch (err) { + console.warn('pin update failure: ', err); + } + + dispatch(setEncryptedUnlockPin(encUserPin)); + + const realmData = await getUserDataWithUsername(currentAccount.name); + + let _currentAccount = currentAccount; + _currentAccount.username = _currentAccount.name; + _currentAccount.local = realmData[0]; + + try { + const pinHash = encryptKey(Config.DEFAULT_PIN, Config.PIN_KEY); + //migration script for previously mast key based logged in user not having access token + if (realmData[0].authType !== AUTH_TYPE.STEEM_CONNECT && realmData[0].accessToken === '') { + _currentAccount = await migrateToMasterKeyWithAccessToken( + _currentAccount, + realmData[0], + pinHash, + ); } + //refresh access token + const encryptedAccessToken = await refreshSCToken(_currentAccount.local, Config.DEFAULT_PIN); + _currentAccount.local.accessToken = encryptedAccessToken; + } catch (error) { + onFailure(error); + } - try { - const pinData = { - pinCode: Config.DEFAULT_PIN, - username: currentAccount.username, - oldPinCode, - }; - - const response = updatePinCode(pinData) - - const _currentAccount = currentAccount; - _currentAccount.local = response; - - dispatch( - updateCurrentAccount({ - ..._currentAccount, - }), - ); - - const encryptedPin = encryptKey(Config.DEFAULT_PIN, Config.PIN_KEY); - dispatch(setPinCode(encryptedPin)); - - } catch (err) { - console.warn('pin update failure: ', err); - } - - - dispatch(setEncryptedUnlockPin(encUserPin)) - - const realmData = await getUserDataWithUsername(currentAccount.name) - - let _currentAccount = currentAccount; - _currentAccount.username = _currentAccount.name; - _currentAccount.local = realmData[0]; - - try { - const pinHash = encryptKey(Config.DEFAULT_PIN, Config.PIN_KEY); - //migration script for previously mast key based logged in user not having access token - if ( - realmData[0].authType !== AUTH_TYPE.STEEM_CONNECT && - realmData[0].accessToken === '' - ) { - _currentAccount = await migrateToMasterKeyWithAccessToken( - _currentAccount, - realmData[0], - pinHash, - ); - } - - //refresh access token - const encryptedAccessToken = await refreshSCToken(_currentAccount.local, Config.DEFAULT_PIN); - _currentAccount.local.accessToken = encryptedAccessToken; - } catch (error) { - onFailure(error) - } - - //get unread notifications - try { - _currentAccount.unread_activity_count = await getUnreadNotificationCount(); - _currentAccount.pointsSummary = await getPointsSummary(_currentAccount.username); - _currentAccount.mutes = await getMutes(_currentAccount.username); - } catch (err) { - console.warn( - 'Optional user data fetch failed, account can still function without them', - err, - ); - } - - dispatch(updateCurrentAccount({ ..._currentAccount })); - dispatch(fetchSubscribedCommunities(_currentAccount.username)); - -} - + //get unread notifications + try { + _currentAccount.unread_activity_count = await getUnreadNotificationCount(); + _currentAccount.pointsSummary = await getPointsSummary(_currentAccount.username); + _currentAccount.mutes = await getMutes(_currentAccount.username); + } catch (err) { + console.warn('Optional user data fetch failed, account can still function without them', err); + } + dispatch(updateCurrentAccount({ ..._currentAccount })); + dispatch(fetchSubscribedCommunities(_currentAccount.username)); +}; const reduxMigrations = { - 0: (state) => { - const upvotePercent = state.application.upvotePercent; - state.application.postUpvotePercent = upvotePercent; - state.application.commentUpvotePercent = upvotePercent - state.application.upvotePercent = undefined; - return state - }, - 1: (state) => { - state.application.notificationDetails.favoriteNotification = true - return state; - } -} + 0: (state) => { + const upvotePercent = state.application.upvotePercent; + state.application.postUpvotePercent = upvotePercent; + state.application.commentUpvotePercent = upvotePercent; + state.application.upvotePercent = undefined; + return state; + }, + 1: (state) => { + state.application.notificationDetails.favoriteNotification = true; + return state; + }, +}; export default { - migrateSettings, - migrateUserEncryption, - reduxMigrations, -} + migrateSettings, + migrateUserEncryption, + reduxMigrations, +}; diff --git a/yarn.lock b/yarn.lock index abdaedc85..268d38062 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8933,7 +8933,11 @@ react-native-image-zoom-viewer@^2.2.27: dependencies: react-native-image-pan-zoom "^2.1.9" -react-native-iphone-x-helper@^1.0.3, react-native-iphone-x-helper@^1.3.1: +react-native-iphone-x-helper@Norcy/react-native-iphone-x-helper: + version "2.0.0" + resolved "https://codeload.github.com/Norcy/react-native-iphone-x-helper/tar.gz/aff3730b2614947a72bcdd86e35ba0217ca1054c" + +react-native-iphone-x-helper@^1.0.3: version "1.3.1" resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==