I have a scrolling view of posts. Each post has a corresponding user and I have a header that shows the user info of the current visible post. With Flutter this was simple, I just wrapped the post widget with a visibility detector. With React Native this is not very easy. I've tried onViewableItemsChanged but, since I am using a fuction not a class, that causes an error. I also tried some solutions that used onScroll and onMomentumScrollEnd but those all just stayed at index 0. How can I get the current index that is fully visible? If needed, I am fine with splitting up the pagination functions so I can just have a class with the UI and use onViewableItemsChanged but I don't know how to do that because the handleLoadMore function is used in the UI.
export default function PostsListView() {
const [users, setUsers] = useState<User[]>([]);
const [posts, setPosts] = useState<Post[]>([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [hasMore, setHasMore] = useState(true);
const onScroll = useCallback((event: any) => {
const slideSize = event.nativeEvent.layoutMeasurement.width;
const index = event.nativeEvent.contentOffset.x / slideSize;
const roundIndex = Math.round(index);
console.log("roundIndex:", roundIndex);
currentItem = roundIndex;
}, []);
useEffect(() => {
async function fetchPosts() {
setLoading(true);
const { data, error } = await supabase
.from("posts")
.select("*")
.order("date", { ascending: false })
.range((page - 1) * PAGE_SIZE, page * PAGE_SIZE - 1);
if (error) {
console.error(error);
showAlert();
setLoading(false);
return;
}
const newPosts = data.map((post: any) => new Post(post));
setPosts((prevPosts) => [...prevPosts, ...newPosts]);
setLoading(false);
setHasMore(data.length === PAGE_SIZE);
}
async function fetchUsers() {
const { data, error } = await supabase.from("posts").select("*");
if (error) {
showAlert();
console.error(error);
return;
}
const newUsers = data.map((user: any) => new User(user));
newUsers.forEach((user) => {
const userPosts = posts.filter((post) => post.uid === user.uid);
user.posts = [...user.posts, ...userPosts];
});
setUsers((prevUsers) => [...prevUsers, ...newUsers]);
}
fetchPosts();
fetchUsers();
}, [page]);
const handleLoadMore = () => {
if (!loading && hasMore) {
setPage((prevPage) => prevPage + 1);
}
};
const handleScroll = (event: any) => {
const index = Math.floor(
Math.floor(event.nativeEvent.contentOffset.x) /
Math.floor(event.nativeEvent.layoutMeasurement.width)
);
currentItem = index;
};
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
{loading ? (
<Text>Loading...</Text>
) : (
<FlatList
data={posts}
horizontal={false}
directionalLockEnabled={true}
renderItem={({ item }) => (
<View>
<HomePost
post={item}
user={
users.filter(function (u) {
return u.uid == item.uid;
})[0]
}
index={posts.indexOf(item)}
loading={loading}
/>
<SizedBox vertical={5} />
</View>
)}
keyExtractor={(item) => item.postId}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.1}
onScroll={onScroll}
onMomentumScrollEnd={onScroll}
/>
)}
</View>
);
}
If you provide a viewabilityConfig to the FlatList, you can use the onViewableItemsChanged event to learn which items are on screen. You just have to make sure that both the viewabilityConfig and onViewableItemsChanged values never change:
import { useState, useEffect, useRef, useCallback } from 'react';
import { Text, View, StyleSheet, FlatList, Image } from 'react-native';
import Constants from 'expo-constants';
// You can import from local files
import AssetExample from './components/AssetExample';
// or any pure javascript modules available in npm
import { Card } from 'react-native-paper';
const API_URL = 'https://random-data-api.com/api/v2/users?size=25';
export default function App() {
const [posts, setPosts] = useState([]);
const [visibleItems, setVisibleItems] = useState([]);
// wrapped in ref so that re-renders doesnt recreate it
const viewabilityConfig = useRef({
minimumViewTime: 100,
itemVisiblePercentThreshold: '90%',
}).current;
// wrapped in useCallback so that re-renders doesnt recreate it
const onViewableItemsChanged = useCallback(({ viewableItems }) => {
setVisibleItems(viewableItems.map(({ item }) => item));
}, []);
useEffect(() => {
fetch(API_URL)
.then((data) => data.json())
.then(setPosts);
}, []);
return (
<View style={styles.container}>
{visibleItems.length > 0 && (
<Text>
Currently visible:{' '}
{visibleItems
.map((item) => item.first_name + ' ' + item.last_name)
.join(', ')}
</Text>
)}
<View style={styles.flatlistContainer}>
<FlatList
data={posts}
renderItem={(props) => <Item {...props} />}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={onViewableItemsChanged}
/>
</View>
</View>
);
}
const Item = ({ item }) => {
return (
<View style={styles.itemContainer}>
<Text>
{item.first_name} {item.last_name}
</Text>
<Image
source={{ uri: item.avatar }}
style={{ width: 100, height: 100 }}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
flatlistContainer: {
width: '100%',
height: 500,
backgroundColor: 'lightblue',
},
itemContainer: {
justifyContent: 'center',
alignItems: 'center',
margin: 10,
},
});
Demo
You could get the currently viewable items with onViewableItemsChanged where you should get your information.
<FlatList
data={posts}
horizontal={false}
directionalLockEnabled={true}
// this should return an array with following infos
// [{
// item: {key: "key-12"},
// key: "key-12",
// index: 11,
// isViewable: true
// }]
onViewableItemsChanged={({changed, viewableItems}) => console.log(changed, viewableItems)}
....
I've gone through and created a supabase app that resembles your use case and the onViewableItemsChanged approach should work:
import { useContext, useEffect, useState, useRef, useCallback } from 'react';
import {
View,
StyleSheet,
Dimensions,
FlatList,
ActivityIndicator,
} from 'react-native';
import { Text } from 'react-native-paper';
import { SessionContext } from '../../Context';
import { supabase } from '../../initSupabase';
import PostItem from '../../components/PostItem';
const { height } = Dimensions.get('screen');
const totalPostsToGet = 2;
type Post = {
post: {
text: string;
media: {
type: string;
source: string;
};
};
id: number;
created_at: string;
uid: string;
};
export default function PostScreen(props) {
const { session } = useContext(SessionContext);
const [posts, setPosts] = useState<Post[]>([]);
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [visibleItems, setVisibleItems] = useState([]);
const viewabilityConfig = useRef({
// minimumViewTime: 100,
itemVisiblePercentThreshold: 50,
}).current;
// wrapped in useCallback so that re-renders doesnt recreate it
const onViewableItemsChanged = useCallback(({ viewableItems }) => {
setVisibleItems(viewableItems.map(({ item }) => item));
}, []);
const handleLoadMore = () => {
if (!isLoading && hasMore) {
setPage((prevPage) => prevPage + 1);
}
};
useEffect(() => {
if (!session) return;
const fetchLastPost = async () => {
const { data, error } = await supabase
.from('posts')
.select('*')
.order('created_at')
.range(0, 1);
return data[0];
};
const fetchPosts = async () => {
setIsLoading(true);
const lastPost = await fetchLastPost();
console.log('last post', lastPost);
const rangeStart = (page - 1) * totalPostsToGet;
const { data, error } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
.range(rangeStart, rangeStart + totalPostsToGet - 1);
if (error) {
console.log('error retrieving profile data', error);
}
if (data) {
setPosts((prev) => prev.concat(data));
// I couldnt figure out how PAGE_SIZE could be used to know that the last post was reached
// so I just grab the last post and look to see if its id is in current data
const hasLastPost = Boolean(
data.find((post) => post.id == lastPost.id)
);
setHasMore(!hasLastPost);
}
setIsLoading(false);
};
fetchPosts();
// subscribe to database changes
const subscription = supabase
.channel(`Posts`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
// filter: `id=eq.${session.user.id}`,
},
(payload) => {
setIsLoading(true);
console.log('Post update');
setPosts((prev) => {
// either push new post or update existing one
const postIndex = prev.findIndex(
(post) => post.id == payload.new.id
);
if (postIndex < 0) return [...prev, payload.new];
else {
const newPosts = [...prev];
newPosts[postIndex] = payload.new;
return newPosts;
}
});
setIsLoading(false);
}
)
.subscribe();
return () => {
supabase.removeChannel(subscription);
};
}, [session, page]);
return (
<View style={styles.container}>
<Text>Currently visible:</Text>
<View style={{ padding: 5, margin: 5 }}>
{visibleItems.map((item) => (
<Text>{item.post.text.substring(0, 30)}</Text>
))}
</View>
<View style={styles.flatlistContainer}>
{isLoading && <ActivityIndicator />}
<FlatList
data={posts}
keyExtractor={(item: Post) => item.id}
horizontal={false}
directionalLockEnabled
onEndReached={handleLoadMore}
renderItem={(props) => <PostItem {...props} />}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={onViewableItemsChanged}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
flatlistContainer: {
height: height * 0.45,
},
});
Demo
Take a look at the Intersection Observer API documentation which is an implementation you can use to detect when an element is visible on the screen or not.
Here's a very simple example where the green div is "observed". Whether it is visible or not is marked in state.
Here's a working sandbox
package.json
{
"name": "react18-intersection-observer",
"version": "1.0.0",
"description": "Detect visible screen elemenet using intersection observer",
"keywords": [
"react",
"starter"
],
"main": "src/index.js",
"dependencies": {
"react": "18.2.0",
"react-bootstrap": "^2.7.1",
"react-dom": "18.2.0",
"react-scripts": "^5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"author": "Wesley LeMahieu"
}
index.js
import { createRoot } from "react-dom/client";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(<App />);
app.js
import { useEffect, useRef, useState } from "react";
const App = () => {
const observer = useRef(null);
const observerRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
const observerCallback = async (e) => {
if (e.length) {
setIsVisible(e[0].isIntersecting);
} else {
setIsVisible(false);
}
};
useEffect(() => {
if (observerRef.current) {
if (observer.current) {
observer.current.disconnect();
observer.current.observe(observerRef.current);
} else {
observer.current = new IntersectionObserver(observerCallback);
observer.current.observe(observerRef.current);
}
}
return () => observer.current.disconnect();
}, []);
useEffect(() => {
if (isVisible) {
alert("Green box visible");
} else {
alert("Grey box visible");
}
console.log(isVisible ? "GREEN BOX VISIBLE" : "GREEN BOX NOT VISIBLE");
}, [isVisible]);
return (
<div style={{ display: "flex" }}>
<div
style={{
backgroundColor: isVisible ? "green" : "red",
width: "10%",
height: "3000px",
}}
>
Visible
</div>
<div style={{ width: "90%" }}>
<div
style={{ backgroundColor: "grey", opacity: 0.6, height: "1000px" }}
>
Other div
</div>
<div
ref={observerRef}
style={{ backgroundColor: "green", opacity: 0.6, height: "1000px" }}
>
Observed Div
</div>
<div
style={{ backgroundColor: "grey", opacity: 0.6, height: "1000px" }}
>
Other div
</div>
</div>
</div>
);
};
export default App;
Related
I am using redux persist with asyncstorage save items in a bookmarks list. The items are in a flatlist and when I click on one item, it navigates me to another screen. I would like to implement the functional bookmark in the header of that screen.
When I tried doing this, and clicked the bookmark in the header, and go back to the bookmarks, it just shows a blank card. It looks like it is not updating the state properly. How can I fix this?
StackNavigator.tsx
const MainStackNavigator = () => {
const { books, bookmarks } = useAppSelector((state) => state.booksReducer);
const dispatch = useDispatch();
const fetchBooks = () => dispatch(getBooks());
const addToBookmarkList = (book) => dispatch(addBookmark(book));
const removeFromBookmarkList = (book) => dispatch(removeBookmark(book));
useEffect(() => {
fetchBooks();
}, []);
const handleAddBookmark = (book) => {
addToBookmarkList(book);
};
const handleRemoveBookmark = (book) => {
removeFromBookmarkList(book);
};
const handleSwapBookmark = (book) => {
removeFromBookmarkList(book);
};
const RenderItem = () => {
const ifExists = (book) => {
if (bookmarks.filter((item) => item.id === book.id).length > 0) {
return true;
}
return false;
};
return (
<TouchableOpacity
onPress={() =>
ifExists(i) ? handleRemoveBookmark(i) : handleAddBookmark(i)
}
activeOpacity={0.7}
style={{
flexDirection: "row",
padding: 2,
backgroundColor: ifExists(i) ? "#F96D41" : "#2D3038",
borderRadius: 20,
alignItems: "center",
justifyContent: "center",
height: 40,
width: 40,
}}
>
<MaterialCommunityIcons
color={ifExists(i) ? "white" : "#64676D"}
size={24}
name={ifExists(i) ? "bookmark-outline" : "bookmark"}
/>
</TouchableOpacity>
);
};
return (
<AppStack.Navigator>
<AppStack.Screen
name="BookmarksScreen"
component={BookmarksScreen}
options={{
title: "Search",
statusBarColor: isDarkMode ? "white" : "black",
headerLargeTitle: true,
headerTranslucent: true,
headerLargeTitleHideShadow: true,
}}
/>
<AppStack.Screen
name="Screen2"
component={Screen2}
options={({ route }) => ({
headerLargeTitle: false,
title: route.params.name,
headerTranslucent: true,
headerRight: () => <RenderItem item={route.params.name} />,
})}
/>
</AppStack.Navigator>
);
};
actions.js
import axios from "axios";
import { BASE_URL } from "../config";
// Define action types
export const GET_BOOKS = "GET_BOOKS";
export const ADD_TO_BOOKMARK_LIST = "ADD_TO_BOOKMARK_LIST";
export const REMOVE_FROM_BOOKMARK_LIST = "REMOVE_FROM_BOOKMARK_LIST";
export const SWAP_IN_BOOKMARK_LIST = "SWAP_IN_BOOKMARK_LIST";
export const getBooks = () => {
try {
return async (dispatch) => {
const response = await axios.get(`${BASE_URL}`);
if (response.data) {
dispatch({
type: GET_BOOKS,
payload: response.data,
});
} else {
console.log("Unable to fetch data from the API BASE URL!");
}
};
} catch (error) {
console.log(error);
}
};
export const addBookmark = (book) => (dispatch) => {
dispatch({
type: ADD_TO_BOOKMARK_LIST,
payload: book,
});
};
export const removeBookmark = (book) => (dispatch) => {
dispatch({
type: REMOVE_FROM_BOOKMARK_LIST,
payload: book,
});
};
hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
reducers.ts
import {
GET_BOOKS,
ADD_TO_BOOKMARK_LIST,
REMOVE_FROM_BOOKMARK_LIST,
} from "./actions";
const initialState = {
books: [],
bookmarks: [],
};
function booksReducer(state = initialState, action) {
switch (action.type) {
case GET_BOOKS:
return { ...state, books: action.payload };
case ADD_TO_BOOKMARK_LIST:
return { ...state, bookmarks: [...state.bookmarks, action.payload] };
case REMOVE_FROM_BOOKMARK_LIST:
return {
...state,
bookmarks: state.bookmarks.filter(
(book) => book.id !== action.payload.id
),
};
default:
return state;
}
}
export default booksReducer;
store.ts
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import AsyncStorage from "#react-native-async-storage/async-storage";
import { persistStore, persistReducer } from "redux-persist";
import booksReducer from "./reducers";
const persistConfig = {
key: "root",
storage: AsyncStorage,
whitelist: ["bookmarks"],
};
const rootReducer = combineReducers({
booksReducer: persistReducer(persistConfig, booksReducer),
});
export const store = createStore(rootReducer, applyMiddleware(thunk));
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
BookmarksScreen.tsx
const BookmarksScreen = () => {
return (
<View>
<FlatList
data={bookmarks}
keyExtractor={(item) => item.id}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
/>
</View>
);
}
renderItem
const renderItem = ({ item }) => {
return (
<TouchableOpacity
onPress={() =>
navigation.navigate("Screen2", {name: item.name})}
>
<View style={{ flexDirection: "row", flex: 1 }}>
<View>
<Text
style={{
fontSize: 22,
paddingRight: 16,
color: "black",
fontFamily: "Medium",
left: 45,
top: 6,
}}
>
{item.country}
</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
};
I think you forgot to access props that's the reason booked-marked not working i change some code please check it's working or not.
StackNavigator.tsx
const RenderItem = (item) => {
const ifExists = (book) => {
if (bookmarks.filter((item) => item.id === book.id).length > 0) {
return true;
}
return false;
};
return (
<TouchableOpacity
onPress={() =>
ifExists(item) ? handleRemoveBookmark(item) : handleAddBookmark(item)
}
activeOpacity={0.7}
style={{
flexDirection: "row",
padding: 2,
backgroundColor: ifExists(i) ? "#F96D41" : "#2D3038",
borderRadius: 20,
alignItems: "center",
justifyContent: "center",
height: 40,
width: 40,
}}
>
<MaterialCommunityIcons
color={ifExists(i) ? "white" : "#64676D"}
size={24}
name={ifExists(i) ? "bookmark-outline" : "bookmark"}
/>
</TouchableOpacity>
);
};
I am new to react native. I have created An app. and In this app I am able to captured image from camera and gallery also but the problem is now . I want that captured Image To store in Array And want to show that image On specific screen. I want 3-5 images to store and show on that screen
here is my code
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View, Button, Image } from 'react-native';
import { Camera } from 'expo-camera';
import { Ionicons } from '#expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
export default function Add({ navigation }) {
const [cameraPermission, setCameraPermission] = useState(null);
const [galleryPermission, setGalleryPermission] = useState(null);
const [camera, setCamera] = useState(null);
const [imageUri, setImageUri] = useState(null);
const [type, setType] = useState(Camera.Constants.Type.back);
const [imageArray,setImageArray] = useState([])
const permisionFunction = async () => {
// here is how you can get the camera permission
const cameraPermission = await Camera.requestPermissionsAsync();
setCameraPermission(cameraPermission.status === 'granted');
const imagePermission = await ImagePicker.getMediaLibraryPermissionsAsync();
console.log(imagePermission.status);
setGalleryPermission(imagePermission.status === 'granted');
if (
imagePermission.status !== 'granted' &&
cameraPermission.status !== 'granted'
) {
alert('Permission for media access needed.');
}
};
useEffect(() => {
permisionFunction();
}, []);
const takePicture = async () => {
if (camera) {
const data = await camera.takePictureAsync(null);
console.log(data.uri);
setImageUri(data.uri);
}
};
const pickImage = async () => {
setImageArray([imageArray..] + result)
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
// allowsEditing: true,
// aspect: [1, 1],
quality: 1,
});
console.log(result);
if (!result.cancelled) {
setImageUri(result.uri);
}
};
return (
<View style={styles.container}>
<View style={styles.header}>
<Ionicons style={{paddingLeft:20}} name="arrow-back" size={40}
color="black" onPress={() => navigation.navigate("OtherInfo")} />
<Text style={{fontSize:20, paddingLeft: 70, paddingTop: 10}}>Get Image</Text>
</View>
<View style={styles.cameraContainer}>
<Camera
ref={(ref) => setCamera(ref)}
style={styles.fixedRatio}
type={type}
ratio={'1:1'}
/>
</View>
<Button title={'Take Picture'} onPress={takePicture} />
<Button title={'Gallery'} onPress={pickImage} />
{imageUri && <Image source={{ uri: imageUri }} style={{ flex: 1 }} />}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
cameraContainer: {
flex: 1,
flexDirection: 'row',
},
fixedRatio: {
flex: 1,
aspectRatio: 1,
},
button: {
flex: 0.1,
padding: 10,
alignSelf: 'flex-end',
alignItems: 'center',
},
header:{
flexDirection: 'row'
}
});
As I suggested in our previous chat, here is the solution:
Working Example: Expo Snack
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Button, Image, FlatList, Text } from 'react-native';
import { Camera } from 'expo-camera';
import { Ionicons } from '#expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
export default function Add() {
const [cameraPermission, setCameraPermission] = useState(null);
const [galleryPermission, setGalleryPermission] = useState(null);
const [camera, setCamera] = useState(null);
const [imageUri, setImageUri] = useState([]);
const [type, setType] = useState(Camera.Constants.Type.back);
const [imageArray, setImageArray] = useState([]);
const permisionFunction = async () => {
// here is how you can get the camera permission
const cameraPermission = await Camera.requestPermissionsAsync();
setCameraPermission(cameraPermission.status === 'granted');
const imagePermission = await ImagePicker.getMediaLibraryPermissionsAsync();
console.log(imagePermission.status);
setGalleryPermission(imagePermission.status === 'granted');
if (
imagePermission.status !== 'granted' &&
cameraPermission.status !== 'granted'
) {
alert('Permission for media access needed.');
}
};
useEffect(() => {
permisionFunction();
}, []);
const takePicture = async () => {
if (camera) {
const data = await camera.takePictureAsync(null);
console.log(data.uri);
setImageUri(data.uri);
setImageArray([...imageArray, data.uri]);
}
};
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 1,
});
console.log(result.uri);
if (!result.cancelled) {
setImageArray([...imageArray, result.uri]);
}
};
return (
<View style={styles.container}>
<Camera
ref={(ref) => setCamera(ref)}
style={styles.fixedRatio}
type={type}
/>
{imageArray.length > 0 && (
<View style={{ height: 110 }}>
<FlatList
horizontal
data={imageArray}
renderItem={({ item }) => (
<Image
source={{ uri: item }}
style={{ width: 100, height: 100, borderRadius: 10, margin: 5 }}
/>
)}
/>
</View>
)}
<Button title={'Take Picture'} onPress={takePicture} />
<Button title={'Gallery'} onPress={pickImage} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
fixedRatio: {
flex: 1,
},
});
I am implementing video playing app(like Instagram Reels or tik tok) using RecyclerListView in React-Native. In the development of app, I am facing the problem that all the videos in the list play simultaneously. I want to pause all other videos which are out of the screen and play only when scrolled to a particular video and video is visible on screen.
How to do it? I have tried a lot of things but could not find solution for RecyclerListView.
I have used react-native-video for playing video.
App.js
import VideoPlayer from './ViewVideo';
const fakeData = [
{
type: 'NORMAL',
item: {
uri: require('./images/likd2.mp4'),
},
},
{
type: 'NORMAL',
item: {
uri: require('./images/Linkd.mp4'),
},
},
{
type: 'NORMAL',
item: {
uri: require('./images/PlayDate.mp4'),
},
},
];
export default class Myworld extends React.Component {
dataProvider = new DataProvider((r1, r2) => {
return r1 !== r2;
}).cloneWithRows(fakeData);
layoutProvider = new LayoutProvider(
(i) => {
return this.dataProvider.getDataForIndex(i).type;
},
(type, dim) => {
switch (type) {
case 'NORMAL':
dim.width = '100%';
dim.height = '100%';
break;
default:
dim.width = '100%';
dim.height = '100%';
break;
}
},
);
rowRenderer = (type, data, index) => {
switch (type) {
case 'NORMAL':
return (
<View>
<VideoPlayer source={uri} />
</View>
);
}
};
render() {
return (
<>
<SafeAreaView style={styles.container}>
<RecyclerListView
style={styles.videolistcontainer}
rowRenderer={this.rowRenderer}
dataProvider={this.dataProvider}
layoutProvider={this.layoutProvider}
initialOffset={1}
pagingEnabled={true}
showsVerticalScrollIndicator={false}
/>
</SafeAreaView>
</>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
backgroundColor: '#14171A',
},
});
ViewVideo.js
const VideoPlayer = (props) => {
const [paused, setPause] = useState(false);
return (
<>
<TouchableOpacity
style={{height: height, width: width}}
onPress={() => setPause(!paused)}>
<Video
ref={(ref) => {
setVideoRef(ref);
}}
source={props.source}
style={styles.backgroundVideo}
resizeMode={'cover'}
onError={onError(videoRef)}
paused={paused}
onLoad={onLoad}
onProgress={onProgress}
onEnd={onEnd}
repeat={false}
rate={1.0}
volume={1.0}
muted={false}
onLayout={onVideoLayout}
/>
</TouchableOpacity>
</>
);
};
export default VideoPlayer;
const styles = StyleSheet.create({
backgroundVideo: {
position: 'absolute',
width: WP('100%'),
height: HP('100%'),
left: 0,
right: 0,
top: 0,
bottom: 0,
},
});
You can use AppState of React-Native.
It indicate the future state of app: active, background or inactive(IOS only)
Example:
import React, { useRef, useState, useEffect } from "react";
import { AppState, Text, View } from "react-native";
const AppInactiveHandleExample = () => {
const appState = useRef(AppState.currentState);
const [appStateVisible, setAppStateVisible] = useState(appState.current);
useEffect(() => {
AppState.addEventListener("change", handleAppStateChange);
// Return for clear the cache of action
return () => {
AppState.removeEventListener("change", handleAppStateChange);
};
}, []);
const handleAppStateChange = nextAppState => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === "active"
) {
// Execute here your action
onAppInactive()
}
appState.current = nextAppState;
setAppStateVisible(appState.current);
};
// Action executed when app was inactive or background
const onAppInactive = () => {
// Your code here
}
return (
<View>
<Text>Current state is: {appStateVisible}</Text>
</View>
);
};
export default AppInactiveHandleExample;
https://reactnative.dev/docs/appstate
Need some help running through my codes to make WebRTC work. I am just trying to make a call from one device to the other. Realise I'm not throughly understanding WebRTC. My app crashes when the answer is emitted back.
The socket/emitter I am working with is actually
FeathersJS services. It is serving as my backend, whenever I need to emit, I send a create service and in my backend, feathersJS emit a customEmitter ['offer' 'answer' 'ice-candidate']. Although I am able to receive all emitted events, I am unable to get WebRTC to work. The below are my React-native code:
import React, { useContext, useEffect, useState } from 'react';
import {
View, SafeAreaView, Button, StyleSheet,
} from 'react-native';
import {
RTCPeerConnection,
RTCSessionDescription,
RTCView,
mediaDevices,
} from 'react-native-webrtc';
import ContextApp from '../../context/contextApp';
import FeathersApp, { Services } from '../../Feathers';
const styles = StyleSheet.create({
container: {
backgroundColor: '#313131',
justifyContent: 'space-between',
alignItems: 'center',
height: '100%',
},
text: {
fontSize: 30,
},
rtcview: {
justifyContent: 'center',
alignItems: 'center',
height: '40%',
width: '80%',
backgroundColor: 'black',
},
rtc: {
width: '80%',
height: '100%',
},
toggleButtons: {
width: '100%',
flexDirection: 'row',
justifyContent: 'space-around',
},
});
const VideoCallPage = ( props: any ) => {
const { userId } = props;
const { authState } = useContext(ContextApp);
const callService = FeathersApp.service(Services.CALL);
const [callAlert, setCallAlert] = useState<boolean>(false);
const [offerData, setOfferData] = useState<any>();
const configuration = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
const localPC = new RTCPeerConnection(configuration);
const [localStream, setLocalStream] = useState<any>();
const [remoteStream, setRemoteStream] = useState<any>();
const sendToPeer = (messageType: any, payload: any) => {
callService.create({
callType: messageType,
sdp: payload,
caller: authState._id,
answerer: userId,
candidate: payload,
})
}
const createOffer = async () => {
const offer = await localPC.createOffer();
await localPC.setLocalDescription(offer).then(() => console.log('local offer succeeded'));
sendToPeer('offer', localPC.localDescription);
}
const createIceCandidate = () => {
localPC.onicecandidate = e => {
try {
console.log('localPC icecandidate:', e.candidate);
if (e.candidate) {
sendToPeer('ice-candidate', e.candidate)
}
} catch (err) {
console.error(`Error adding remotePC iceCandidate: ${err}`);
}
};
}
const startCall = async () => {
console.log('start call funtion')
const newStream = await mediaDevices.getUserMedia({
audio: true,
video: false,
});
setLocalStream(newStream);
createIceCandidate()
createOffer();
};
const recieveCall = async (incoming: any) => {
console.log('accepting call');
await localPC.setRemoteDescription(new RTCSessionDescription(incoming.sdp));
const answer = await localPC.createAnswer();
await localPC.setLocalDescription(answer).then(() => console.log('localPC desc succeeded'));
sendToPeer('answer', localPC.localDescription);
createIceCandidate();
localPC.addStream(localStream);
localPC.onaddstream = e => {
console.log('remotePC tracking with ', e);
if (e.stream && remoteStream !== e.stream) {
console.log('RemotePC received the stream', e.stream);
setRemoteStream(e.stream);
}
};
};
const recieveAnswer = (payload: any) => {
console.log('recieved answer: ', payload.candidate.sdp)
localPC.setRemoteDescription(new RTCSessionDescription(payload.candidate.sdp));
localPC.onaddstream = e => {
if (e.stream && remoteStream !== e.stream) {
console.log('RemotePC received the stream', e.stream);
setRemoteStream(e.stream);
}
};
}
// alert for any calls
useEffect(() => {
callService.on('ice-candidate', (incoming: any) => {
localPC.addIceCandidate(incoming.data.sdp);
localPC.addStream(localStream);
});
callService.on('offer', async (payload: any) => {
if (payload.data.answerer === authState._id) {
const newStreamForReciever = await mediaDevices.getUserMedia({
audio: true,
video: false,
});
setLocalStream(newStreamForReciever);
setOfferData(await payload.data);
setCallAlert(true);
}
});
callService.on('answer', async (payload: any) => {
if (payload.data.answerer === authState._id) {
console.log(userId, 'answered')
recieveAnswer(await payload.data);
}
});
// return (() => {
// callService.removeAllListener('ice-candidate');
// callService.removeAllListener('offer');
// callService.removeAllListener('answer');
// })
}, [callService]);
return (
<SafeAreaView style={styles.container}>
{!localStream && <Button title="Click to start CALL stream" onPress={startCall} />}
{callAlert && <Button title="Answer Call" onPress={() => recieveCall(offerData)} />}
{localStream && (
<View style={styles.toggleButtons}>
</View>
)}
<View style={styles.rtcview}>
{localStream && <RTCView style={styles.rtc} streamURL={localStream.toURL()} />}
</View>
<View style={styles.rtcview}>
{remoteStream && <RTCView style={styles.rtc} streamURL={remoteStream.toURL()} />}
</View>
</SafeAreaView>
)
};
export default VideoCallPage;
What is it that I am missing out??
Any guidance you can provide is greatly appreciated!
I am using FlatList to write an infinite scroll, but it keeps sending request to my server forever. please see the code blow. I don't find any article clarify when the next page will load, what exactly does the onEndReached will be triggered.
import React, { Component } from 'react';
import { View, Text, FlatList, StyleSheet, ActivityIndicator, AsyncStorage } from 'react-native';
import { connect } from 'react-redux';
import { loadOrders } from '../redux/modules/Order';
import OrderListItem from './OrderListItem';
import { forOwn, isEmpty, reduce } from 'lodash';
class OrderList extends Component {
constructor(props) {
super(props);
this.state = {
page: 1,
error: null,
};
}
componentDidMount() {
this.loadOrders();
}
loadOrders = () => {
const { page } = this.state;
AsyncStorage.getItem("userToken")
.then((value) => {
return `Bearer ${value}`;
})
.then((userToken) => {
return this.props.loadOrders(page, { Authorization: userToken });
})
.then((response) => {
this.setState({
error: response.error || null,
});
})
.catch(error => {
this.setState({ error});
})
;
}
handleLoadMore = () => {
this.loadOrders();
};
onPressItem = (id: string) => {
};
keyExtractor = (item, index) => `order-item-${item.id}`;
renderItem = ({item}) => (
<OrderListItem
order={item}
onPressItem={this.onPressItem}
/>
);
renderSeparator = () => {
return (
<View
style={{
height: 1,
width: "86%",
backgroundColor: "#CED0CE",
marginLeft: "14%"
}}
/>
);
};
renderFooter = () => {
if (!this.props.loading) return null;
return (
<View
style={{
paddingVertical: 20,
borderTopWidth: 1,
borderColor: "#CED0CE"
}}
>
<ActivityIndicator animating size="large" />
</View>
);
};
render() {
const { orders} = this.props;
if (orders.length> 0) {
return (
<View containerStyle={styles.container} >
<FlatList
data={orders}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
ListFooterComponent={this.renderFooter}
ItemSeparatorComponent={this.renderSeparator}
onEndReached={this.handleLoadMore}
onEndReachedThreshold={0.5}
/>
</View>
);
}
return <View>
<Text>empty</Text>
</View>
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
borderTopWidth: 0,
borderBottomWidth: 0
},
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#ccc'
}
});
const mapStateToProps = state => {
let order = state.get('order').toJS();
return {
orders: isEmpty(order.entities) ? [] : reduce(order.entities, (result, value) => {
result.push({ key: value.id, ...value });
return result;
}, []),
loading: order.loading
};
};
const mapDispatchToProps = {
loadOrders
};
export default connect(mapStateToProps, mapDispatchToProps)(OrderList);
the if part is false , but the onEndReached methods is still called, I must be insane.
the
Change this
onEndReachedThreshold={0.5}
to this:
onEndReachedThreshold={0}
Right now you're calling the end reached when you're halfway through. You can also try adding this to the FlatList:
legacyImplementation = {true}
If this still won't work I would recommend doing the 'pull' onRefresh. A nice example for you: https://www.youtube.com/watch?v=pHLFJs7jlI4
i met the problem too, in my case:
renderFooter somethings render null(height: 0) when loaded, but render ActivityIndicator when loading, and ActivityIndicator has its heigth bigger than 0(null's height)
when heigth change from 0 to ActivityIndicator's height, it will call onEndReached again
and you say the if part is false, i think its because it's not really false。
when code really run in FlatList, the if part is true, so it call onEndReached, and then the _scrollMetrics.contentLength or this._sentEndForContentLength has changed for some reason before your console in chrome. which makes the if part return false
above is all my thought for now, and i am still debugging for this problem, hope this answer will help you all