Merge pull request #2494 from ecency/nt/use-query-snippets

Nt/use query snippets
This commit is contained in:
Feruz M 2022-10-05 19:16:08 +03:00 committed by GitHub
commit cd939241e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 356 additions and 309 deletions

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

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

@ -18,6 +18,7 @@ import {
ReceivedVestingShare,
Referral,
ReferralStat,
Snippet,
} from './ecency.types';
/**
@ -326,7 +327,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);

View File

@ -1,59 +1,66 @@
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 {
PENDING = 1,
POSTPONED = 2,
PUBLISHED = 3,
ERROR = 4,
}
PENDING = 1,
POSTPONED = 2,
PUBLISHED = 3,
ERROR = 4,
}

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,5 @@ export const initQueryClient = () => {
persistOptions: { persister: asyncStoragePersister },
} as PersistQueryClientProviderProps;
};
export * from './editorQueries';

View File

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