Merge remote-tracking branch 'origin/development' into nt/ui-fixes

This commit is contained in:
noumantahir 2022-10-07 10:40:18 +05:00
commit d285675d6d
29 changed files with 1006 additions and 939 deletions

View File

@ -828,4 +828,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 0282022703ad578ab2d9afbf3147ba3b373b4311
COCOAPODS: 1.11.2
COCOAPODS: 1.11.3

View File

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

View File

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

View File

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

View File

@ -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}
/>
) : (
<ActivityIndicator color="white" style={styles.activityIndicator} />
<ActivityIndicator
color={color || EStyleSheet.value('$primaryBlack')}
style={styles.activityIndicator}
/>
)}
</TouchableOpacity>
</Fragment>

View File

@ -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 = () => (
<View style={styles.loading}>
<ActivityIndicator animating size="large" />
</View>
);
_renderSectionHeader = ({ section: { title, index } }) => (
<ContainerHeader hasSeperator={index !== 0} isBoldTitle title={title} key={title} />
)
);
_renderItem = ({ item }) => (
<>
{item.sectionTitle && <ContainerHeader hasSeperator={!item.firstSection} isBoldTitle title={item.sectionTitle} />}
{item.sectionTitle && (
<ContainerHeader hasSeperator={!item.firstSection} isBoldTitle title={item.sectionTitle} />
)}
<NotificationLine
notification={item}
handleOnPressNotification={this.props.navigateToNotificationRoute}
handleOnUserPress={() => { 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}
/>
<ThemeContainer>
{({ isDarkTheme }) =>
{({ isDarkTheme }) => (
<FlatList
ref={this.listRef}
data={_notifications}
keyExtractor={(item, index) => `${item.id}-${index}`}
onEndReached={() => getActivities(selectedFilter, true)}
onEndReached={() => getActivities(true)}
onEndReachedThreshold={0.3}
ListFooterComponent={this._renderFooterLoading}
ListEmptyComponent={
isLoading ? <ListPlaceHolder/> : (
<Text style={globalStyles.hintText}>
{intl.formatMessage({ id: 'notification.noactivity' })}
</Text>
isNotificationRefreshing ? (
<ListPlaceHolder />
) : (
<Text style={globalStyles.hintText}>
{intl.formatMessage({ id: 'notification.noactivity' })}
</Text>
)
}
}
contentContainerStyle={styles.listContentContainer}
refreshControl={
<RefreshControl
refreshing={isNotificationRefreshing}
onRefresh={() => getActivities(selectedFilter)}
onRefresh={() => getActivities()}
progressBackgroundColor="#357CE6"
tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'}
titleColor="#fff"
@ -251,8 +257,7 @@ class NotificationView extends PureComponent {
}
renderItem={this._renderItem}
/>
}
)}
</ThemeContainer>
</View>
);

View File

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

View File

@ -44,7 +44,7 @@ export default EStyleSheet.create({
borderColor: '$borderColor',
borderRadius: 8,
padding: 2,
color: '$primaryBlack',
// color: '$primaryBlack',
width: 172,
marginRight: 33,
},

View File

@ -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<TNode>) => {
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 <AutoHeightImage
contentWidth={maxImgWidth}
imgUrl={tnode.children[0].attributes.src}
isAnchored={false}
activeOpacity={0.8}
onPress={_onPress}
/>
return (
<AutoHeightImage
contentWidth={maxImgWidth}
imgUrl={tnode.children[0].attributes.src}
isAnchored={false}
activeOpacity={0.8}
onPress={_onPress}
/>
);
}
return <InternalRenderer tnode={tnode} onPress={_onPress} {...props} />;
};
const _imageRenderer = ({ tnode }: CustomRendererProps<TNode>) => {
const imgUrl = tnode.attributes.src;
const _onPress = () => {
@ -280,15 +268,15 @@ export const PostHtmlRenderer = memo(
return <TDefaultRenderer {...props} />;
};
//based on number of columns a table have, sets scroll enabled or disable, also adjust table full width
const _tableRenderer = ({ InternalRenderer, ...props }: CustomRendererProps<TNode>) => {
// 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(
<ScrollView horizontal={true} scrollEnabled={isScrollable}>
<InternalRenderer {...props} />
</ScrollView>
)
}
);
};
// iframe renderer for rendering iframes in body
@ -313,19 +301,10 @@ export const PostHtmlRenderer = memo(
handleVideoPress(iframeProps.source.uri);
}
};
return (
<VideoThumb contentWidth={contentWidth} onPress={_onPress} />
)
return <VideoThumb contentWidth={contentWidth} onPress={_onPress} />;
} else {
return (
<VideoPlayer
mode='uri'
uri={iframeProps.source.uri}
contentWidth={contentWidth}
/>
);
return <VideoPlayer mode="uri" uri={iframeProps.source.uri} contentWidth={contentWidth} />;
}
};
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,
},
}),
[],
);

View File

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

View File

@ -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<Snippet>)=>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<string|null>(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<string | null>(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 = (
<ThemeContainer>
{({isDarkTheme})=>(
<KeyboardAvoidingView
style={styles.container}
keyboardVerticalOffset={Platform.OS == 'ios' ? 64 : null}
behavior={Platform.OS === 'ios' ? 'padding' : null}
>
<View style={styles.inputContainer}>
snippetsMutation.mutate({
id: isNewSnippet ? null : snippetId,
title,
body,
});
<View style={{height:Math.max(35, titleHeight)}}>
<TextInput
autoFocus={true}
innerRef={titleInputRef}
style={styles.titleInput}
height={Math.max(35, titleHeight)}
placeholderTextColor={isDarkTheme ? '#526d91' : '#c1c5c7'}
maxLength={250}
placeholder={intl.formatMessage({id:'snippets.placeholder_title'})}
multiline
numberOfLines={2}
onContentSizeChange={(event) => {
setTitleHeight(event.nativeEvent.contentSize.height);
}}
onChangeText={setTitle}
value={title}
/>
</View>
<TextInput
multiline
autoCorrect={true}
value={body}
onChangeText={setBody}
placeholder={intl.formatMessage({id:'snippets.placeholder_body'})}
placeholderTextColor={isDarkTheme ? '#526d91' : '#c1c5c7'}
selectionColor="#357ce6"
style={styles.bodyWrapper}
underlineColorAndroid="transparent"
innerRef={bodyInputRef}
autoGrow={false}
scrollEnabled={false}
height={100}
/>
</View>
setShowModal(false);
};
<View style={styles.actionPanel}>
<TextButton
text={intl.formatMessage({id:'snippets.btn_close'})}
onPress={()=>setShowModal(false)}
style={styles.closeButton}
/>
<TextButton
text={intl.formatMessage({id:'snippets.btn_save'})}
onPress={_saveSnippet}
textStyle={styles.btnText}
style={styles.saveButton}
/>
</View>
</KeyboardAvoidingView>
)}
</ThemeContainer>
)
const _renderContent = (
<ThemeContainer>
{({ isDarkTheme }) => (
<KeyboardAvoidingView
style={styles.container}
keyboardVerticalOffset={Platform.OS == 'ios' ? 64 : null}
behavior={Platform.OS === 'ios' ? 'padding' : null}
>
<View style={styles.inputContainer}>
<View style={{ height: Math.max(35, titleHeight) }}>
<TextInput
autoFocus={true}
innerRef={titleInputRef}
style={styles.titleInput}
height={Math.max(35, titleHeight)}
placeholderTextColor={isDarkTheme ? '#526d91' : '#c1c5c7'}
maxLength={250}
placeholder={intl.formatMessage({ id: 'snippets.placeholder_title' })}
multiline
numberOfLines={2}
onContentSizeChange={(event) => {
setTitleHeight(event.nativeEvent.contentSize.height);
}}
onChangeText={setTitle}
value={title}
/>
</View>
<TextInput
multiline
autoCorrect={true}
value={body}
onChangeText={setBody}
placeholder={intl.formatMessage({ id: 'snippets.placeholder_body' })}
placeholderTextColor={isDarkTheme ? '#526d91' : '#c1c5c7'}
selectionColor="#357ce6"
style={styles.bodyWrapper}
underlineColorAndroid="transparent"
innerRef={bodyInputRef}
autoGrow={false}
scrollEnabled={false}
height={100}
/>
</View>
<View style={styles.actionPanel}>
<TextButton
text={intl.formatMessage({ id: 'snippets.btn_close' })}
onPress={() => setShowModal(false)}
style={styles.closeButton}
/>
<TextButton
text={intl.formatMessage({ id: 'snippets.btn_save' })}
onPress={_saveSnippet}
textStyle={styles.btnText}
style={styles.saveButton}
/>
</View>
</KeyboardAvoidingView>
)}
</ThemeContainer>
);
return (
<Modal
isOpen={showModal}
handleOnModalClose={()=>{setShowModal(false)}}
presentationStyle="formSheet"
title={intl.formatMessage({
id:isNewSnippet
? 'snippets.title_add_snippet'
: 'snippets.title_edit_snippet'
})}
animationType="slide"
style={styles.modalStyle}
>
{_renderContent}
</Modal>
<Modal
isOpen={showModal}
handleOnModalClose={() => {
setShowModal(false);
}}
presentationStyle="formSheet"
title={intl.formatMessage({
id: isNewSnippet ? 'snippets.title_add_snippet' : 'snippets.title_edit_snippet',
})}
animationType="slide"
style={styles.modalStyle}
>
{_renderContent}
</Modal>
);
};
export default forwardRef(SnippetEditorModal);

View File

@ -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 (
<View style={[styles.itemWrapper, index % 2 !== 0 && styles.itemWrapperGray]}>
<View style={styles.itemHeader}>
<Text style={styles.title} numberOfLines={1} >{`${title}`}</Text>
<IconButton
iconStyle={styles.itemIcon}
style={styles.itemIconWrapper}
iconType="MaterialCommunityIcons"
name="pencil"
onPress={onEditPress}
size={20}
/>
<IconButton
iconStyle={styles.itemIcon}
style={styles.itemIconWrapper}
iconType="MaterialCommunityIcons"
name="delete"
onPress={onRemovePress}
size={20}
/>
<Text style={styles.title} numberOfLines={1}>{`${title}`}</Text>
{id && (
<>
<IconButton
iconStyle={styles.itemIcon}
style={styles.itemIconWrapper}
iconType="MaterialCommunityIcons"
name="pencil"
onPress={onEditPress}
size={20}
/>
<IconButton
iconStyle={styles.itemIcon}
style={styles.itemIconWrapper}
isLoading={snippetsDeleteMutation.isLoading}
iconType="MaterialCommunityIcons"
name="delete"
onPress={_onRemovePress}
size={20}
/>
</>
)}
</View>
<Text style={styles.body} numberOfLines={2} ellipsizeMode="tail">{`${body}`}</Text>
</View>
)
);
};
export default SnippetItem;
export default SnippetItem;

View File

@ -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<SnippetEditorModalRef>(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 (
<TouchableOpacity onPress={_onPress}>
<SnippetItem
title={item.title}
body={item.body}
index={index}
onEditPress={_onEditPress}
onRemovePress={_onRemovePress}
/>
<SnippetItem
id={item.id}
title={item.title}
body={item.body}
index={index}
onEditPress={_onEditPress}
/>
</TouchableOpacity>
)
);
};
//render empty list placeholder
const _renderEmptyContent = () => {
return (
<>
<Text style={styles.title}>{intl.formatMessage({id:'snippets.label_no_snippets'})}</Text>
<Text style={styles.title}>{intl.formatMessage({ id: 'snippets.label_no_snippets' })}</Text>
</>
);
};
//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 (
<View style={styles.floatingContainer}>
<MainButton
style={{ width: 150}}
style={{ width: 150 }}
onPress={_onPress}
iconName="plus"
iconType="MaterialCommunityIcons"
iconColor="white"
text={intl.formatMessage({id:'snippets.btn_add'})}
text={intl.formatMessage({ id: 'snippets.btn_add' })}
/>
</View>
);
};
return (
<View style={styles.container}>
<View style={styles.bodyWrapper}>
<FlatList
data={snippets}
data={snippetsQuery.data}
keyExtractor={(item, index) => index.toString()}
renderItem={_renderItem}
ListEmptyComponent={_renderEmptyContent}
refreshControl={
<RefreshControl
refreshing={isLoading}
onRefresh={_getSnippets}
<RefreshControl
refreshing={snippetsQuery.isFetching}
onRefresh={snippetsQuery.refetch}
/>
}
/>
{_renderFloatingButton()}
</View>
<SnippetEditorModal
ref={editorRef}
onSnippetsUpdated={setSnippets}
/>
<SnippetEditorModal ref={editorRef} />
</View>
);
};

View File

@ -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 (
<TouchableOpacity disabled={!onPress} onPress={() => onPress && onPress()}>
<Animated.View
<AnimatedView
style={{
...styles.container,
...style,
...position,
opacity: animatedValue,
transform: [{ translateY: y }],
}}
easing="ease-in-out"
duration={500}
ref={this.handleViewRef}
>
<Text style={[styles.text, textStyle]}>{text}</Text>
</Animated.View>
</AnimatedView>
</TouchableOpacity>
);
}

View File

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

View File

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

View File

@ -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,
}
}
export enum NotificationFilters {
ACTIVITIES = "activities",
RVOTES = "rvotes",
MENTIONS = "mentions",
FOLLOWS = "follows",
REPLIES = "replies",
REBLOGS = "reblogs",
TRANFERS = "transfers",
DELEGATIONS = "delegations",
FAVOURITES = "nfavorites"
}

View File

@ -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<Snippet[]>([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<Snippet[], undefined, SnippetMutationVars>(
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<Snippet[]>([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<Snippet[], undefined, string>(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' })));
},
});
};

View File

@ -22,3 +22,7 @@ export const initQueryClient = () => {
persistOptions: { persister: asyncStoragePersister },
} as PersistQueryClientProviderProps;
};
export * from './notificationQueries';
export * from './draftQueries';
export * from './editorQueries';

View File

@ -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<number, unknown, string | undefined, void> = {
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);
};

View File

@ -5,6 +5,12 @@ const QUERIES = {
SCHEDULES: {
GET: 'QUERY_GET_SCHEDULES',
},
NOTIFICATIONS:{
GET: 'QERUY_GET_NOTIFICATIONS'
},
SNIPPETS: {
GET: 'QUERY_GET_SNIPPETS',
}
};
export default QUERIES;

View File

@ -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<typeof store.getState>
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
export type AppDispatch = typeof store.dispatch;

View File

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

View File

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

View File

@ -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<any, any> {
// 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<any, any> {
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<any, any> {
: 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,

View File

@ -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 (
<NotificationScreen
getActivities={this._getActivities}
notifications={_notifications}
navigateToNotificationRoute={this._navigateToNotificationRoute}
handleOnUserPress={this._handleOnUserPress}
readAllNotification={this._readAllNotification}
handleLoginPress={this._handleOnPressLogin}
isNotificationRefreshing={isRefreshing}
isLoggedIn={isLoggedIn}
changeSelectedFilter={this._changeSelectedFilter}
globalProps={globalProps}
isLoading={isLoading}
/>
);
}
}
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 */

View File

@ -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 (
<NotificationScreen
getActivities={_getActivities}
notifications={_notifications}
navigateToNotificationRoute={_navigateToNotificationRoute}
handleOnUserPress={_handleOnUserPress}
readAllNotification={_readAllNotification}
handleLoginPress={_handleOnPressLogin}
isNotificationRefreshing={selectedQuery.isRefreshing}
isLoggedIn={isLoggedIn}
changeSelectedFilter={setSelectedFilter}
globalProps={globalProps}
isLoading={selectedQuery.isLoading}
/>
);
};
export default NotificationContainer;
/* eslint-enable */

View File

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

View File

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