How Maximum days restriction in DateRangePicker for the React can be achieved?
I have tried to solve the requirement as below. Sharing if any finds this useful to use,
This restricts selection before and after from the range startDate by a maxDays.
import React, {useState, useEffect} from "react"
import {makeStyles} from "#material-ui/core/styles";
import { DateRangePicker } from 'react-date-range';
import 'react-date-range/dist/styles.css'; // main style file
import 'react-date-range/dist/theme/default.css';
import { addDays, subDays } from "date-fns"; // theme css file
const useStyles = makeStyles(theme => ({
root:{
// padding: 16
}
}))
const selectionRange = {
startDate: new Date(),
endDate: new Date(),
key: 'dateRange',
}
const DateRange = (props) => {
const classes = useStyles();
const {
dateRange,
onSelectPeriod,
hides,
minDate,
maxDate,
maxDays
} = props
const [period, setPeriod] = useState(selectionRange)
const [miDate, setMiDate] = useState(new Date(1970, 1, 1))
const [maDate, setMaDate] = useState(new Date(2100, 1, 1))
useEffect(()=>{
if(dateRange) {
setPeriod(dateRange)
}
if(hides)
hides.map((num, index) => {
(document.getElementsByClassName('rdrStaticRanges')[0]).childNodes[num-index].remove();
})
if(minDate != null) setMiDate(minDate)
if(maxDate != null) setMaDate(maxDate)
},[])
useEffect(()=>{
onSelectPeriod(period)
},[period])
const handleSelect = (ranges) => {
if(ranges === undefined) return
// console.log("DateRangePicker: ", ranges)
setPeriod(ranges.dateRange)
// set to restrict only maxDays range selection; post selection it reset to as initial for further selection
if(maxDays != null) {
if(ranges.dateRange.startDate.getTime() === ranges.dateRange.endDate.getTime()){
// REstrict maxDays before or after for selection
setMiDate(subDays(ranges.dateRange.startDate, maxDays-1))
const mDate = addDays(ranges.dateRange.startDate, maxDays-1)
if(mDate.getTime() <= maxDate.getTime()) setMaDate(mDate)
} else {
// RESET as INITIAL
if(minDate != null) {
setMiDate(minDate)
} else {
setMiDate(new Date(1970, 1, 1))
}
if(maxDate != null) {
setMaDate(maxDate)
} else {
setMaDate(new Date(2100, 1, 1))
}
}
}
}
return (
<div className={classes.root}>
<DateRangePicker
ranges={[period]}
onChange={handleSelect}
minDate={miDate}
maxDate={maDate}
/>
</div>
)
}
export default DateRange
Related
I'm trying to create a force-directed graph for mapping the interactions between courses in an institution. Using Next JS + TypeScript for my frontend.
Have tried several attempts at charting this out using react-flow, dagre, vis-network but am getting either a window : undefined error or just the damn alignment of nodes not being force-directed inside the box I have defined.
Before I move on with implementing d3-force right out of the box, can someone please recommend any alternative solution to this ?
Here's what my nodes & edges look like :
Here's my attempt with reactflow & dagre :
import React, { useCallback, useEffect, useState } from 'react';
import ReactFlow, {
addEdge,
useNodesState,
useEdgesState,
Edge,
Node,
Position,
ConnectionLineType,
ReactFlowProvider,
MiniMap,
Controls,
Background,
} from 'react-flow-renderer';
import dagre from 'dagre';
import { NodeData, useCourseNodes } from 'src/hooks/useCourseNodes';
import { useDepartment } from '#contexts/ActiveDepartmentContext';
import {
useUpdateActiveCourse,
} from '#contexts/ActiveCourseContext';
import { useDrawerOpen, useUpdateDrawerOpen } from '#contexts/DrawerContext';
const dagreGraph = new dagre.graphlib.Graph({directed:true});
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 10.2;
const nodeHeight = 6.6;
const getLayoutedElements = (
nodes: Node[],
edges:Edge[],
) => {
// const isHorizontal = direction === 'LR';
dagreGraph.setGraph( {width:900, height:900, nodesep:20, ranker:'longest-path' });
nodes.forEach((node: Node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge: Edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
// node.targetPosition = isHorizontal ? Position.Left : Position.Top;
// node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
node.targetPosition = Position.Top;
node.sourcePosition = Position.Bottom;
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
node.position = {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
};
console.log(nodeWithPosition)
return node;
})
return { layoutedNodes:nodes, layoutedEdges:edges };
};
const LayoutFlow = () => {
const activeDept = useDepartment();
const setActiveCourse = useUpdateActiveCourse();
const setDrawerOpen = useUpdateDrawerOpen()
const drawerOpen = useDrawerOpen();
const {courseList, edgeList} = useCourseNodes()
const { layoutedNodes, layoutedEdges } = getLayoutedElements(courseList, edgeList)
const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
const [edges, setEdges,onEdgesChange] = useEdgesState(layoutedEdges);
console.log(layoutedNodes)
const onConnect = useCallback(
(params) =>
setEdges((eds) =>
addEdge({ ...params, type: ConnectionLineType.SimpleBezier, animated: true }, eds),
),
[],
);
// ? For switching between layouts (horizontal & vertical) for phone & desktop
// const onLayout = useCallback(
// (direction) => {
// const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
// nodes,
// edges,
// direction
// );
// setNodes([...layoutedNodes]);
// setEdges([...layoutedEdges]);
// },
// [nodes, edges]
// );
// ? M1 - for force re-rendering react flow graph on state change - https://github.com/wbkd/react-flow/issues/1168
// ? M2 - (Applied currently in useEffect block below)for force re-rendering react flow graph on state change - https://github.com/wbkd/react-flow/issues/1168
useEffect(() => {
const {layoutedNodes, layoutedEdges} = getLayoutedElements(courseList, edgeList)
setNodes([...layoutedNodes]);
setEdges([...layoutedEdges]);
}, [activeDept, drawerOpen]);
return (
<div style={{ width: '100%', height: '100%' }} className="layoutflow">
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={(e: React.MouseEvent, node: Node<NodeData>) => {
e.preventDefault();
// created a copy of the node since we're only deleting the "label" property from the node object to conveniently map the rest of the data to the "data" property of the active course
const nodeCopy = JSON.parse(JSON.stringify(node))
const { data } = nodeCopy;
const { label } = data
delete data.label
setActiveCourse({
courseId: label,
data
});
setDrawerOpen(true);
}}
connectionLineType={ConnectionLineType.SimpleBezier}
fitView
>
<MiniMap />
<Controls />
{/* <Background /> */}
</ReactFlow>
</ReactFlowProvider>
<div className="controls">
{/* <button onClick={() => onLayout('TB')}>vertical layout</button>
<button onClick={() => onLayout('LR')}>horizontal layout</button> */}
</div>
</div>
);
};
export default LayoutFlow;
Here's my attempt with vis-network : (note : I did slightly modify edges to have from-to instead of source-target when working with this)
import { useCourseNodes } from "#hooks/useCourseNodes";
import React, { useEffect, useRef } from "react";
import { Network } from "vis-network";
const GraphLayoutFour: React.FC = () => {
const {courseList:nodes, edgeList:edges} = useCourseNodes()
// Create a ref to provide DOM access
const visJsRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const network =
visJsRef.current &&
new Network(visJsRef.current, { nodes, edges } );
// Use `network` here to configure events, etc
}, [visJsRef, nodes, edges]);
return typeof window !== "undefined" ? <div ref={visJsRef} /> : <p>NOT AVAILABLE</p>;
};
export default GraphLayoutFour;
Here's my attempt with react-sigma
import React, { ReactNode, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { UndirectedGraph } from "graphology";
import erdosRenyi from "graphology-generators/random/erdos-renyi";
import randomLayout from "graphology-layout/random";
import chroma from "chroma-js";
import { Attributes } from "graphology-types";
import { ControlsContainer, ForceAtlasControl, SearchControl, SigmaContainer, useLoadGraph, useRegisterEvents, useSetSettings, useSigma, ZoomControl } from "react-sigma-v2/lib/esm";
interface MyCustomGraphProps {
children?: ReactNode;
}
export const MyCustomGraph: React.FC<MyCustomGraphProps> = ({ children }) => {
const sigma = useSigma();
const registerEvents = useRegisterEvents();
const loadGraph = useLoadGraph();
const setSettings = useSetSettings();
const [hoveredNode, setHoveredNode] = useState<any>(null);
useEffect(() => {
// Create the graph
const graph = erdosRenyi(UndirectedGraph, { order: 100, probability: 0.2 });
randomLayout.assign(graph);
graph.nodes().forEach(node => {
graph.mergeNodeAttributes(node, {
label: "label",
size: Math.max(4, Math.random() * 10),
color: chroma.random().hex(),
});
});
loadGraph(graph);
// Register the events
registerEvents({
enterNode: event => setHoveredNode(event.node),
leaveNode: () => setHoveredNode(null),
});
}, []);
useEffect(() => {
setSettings({
nodeReducer: (node, data) => {
const graph = sigma.getGraph();
const newData: Attributes = { ...data, highlighted: data.highlighted || false };
if (hoveredNode) {
//TODO : add type safety
if (node === hoveredNode || (graph as any).neighbors(hoveredNode).includes(node)) {
newData.highlighted = true;
} else {
newData.color = "#E2E2E2";
newData.highlighted = false;
}
}
return newData;
},
edgeReducer: (edge, data) => {
const graph = sigma.getGraph();
const newData = { ...data, hidden: false };
//TODO : add type safety
if (hoveredNode && !(graph as any).extremities(edge).includes(hoveredNode)) {
newData.hidden = true;
}
return newData;
},
});
}, [hoveredNode]);
return <>{children}</>;
};
ReactDOM.render(
<React.StrictMode>
<SigmaContainer>
<MyCustomGraph />
<ControlsContainer position={"bottom-right"}>
<ZoomControl />
<ForceAtlasControl autoRunFor={2000} />
</ControlsContainer>
<ControlsContainer position={"top-right"}>
<SearchControl />
</ControlsContainer>
</SigmaContainer>
</React.StrictMode>,
document.getElementById("root"),
);
import { useCourseNodes } from '#hooks/useCourseNodes'
import dynamic from 'next/dynamic';
import React from 'react'
import { useSigma } from 'react-sigma-v2/lib/esm';
const GraphLayoutThree = () => {
const isBrowser = () => typeof window !== "undefined"
const { courseList, edgeList } = useCourseNodes()
const sigma = useSigma();
if(isBrowser) {
const SigmaContainer = dynamic(import("react-sigma-v2").then(mod => mod.SigmaContainer), {ssr: false});
const MyGraph = dynamic(import("./CustomGraph").then(mod => mod.MyCustomGraph), {ssr: false});
return (
<SigmaContainer style={{ height: "500px", width: "500px" }} >
<MyGraph/>
</SigmaContainer>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutThree
Here's my attempt with react-force-graph (note : I did slightly modify edges to have from-to instead of source-target when working with this)
import dynamic from "next/dynamic";
const GraphLayoutTwo = () => {
const isBrowser = () => typeof window !== "undefined"
if(isBrowser) {
const MyGraph = dynamic(import("./CustomGraphTwo").then(mod => mod.default), {ssr: false});
return (
<MyGraph/>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutTwo
import dynamic from "next/dynamic";
const GraphLayoutTwo = () => {
const isBrowser = () => typeof window !== "undefined"
if(isBrowser) {
const MyGraph = dynamic(import("./CustomGraphTwo").then(mod => mod.default), {ssr: false});
return (
<MyGraph/>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutTwo
To implement something similar we use react-graph-vis inside a nextjs application.
If you have the window is not defined error, just wrap the component and import it with dynamic
// components/graph.tsx
export const Graph = ({data, options, events, ...props}) => {
return (
<GraphVis
graph={transformData(data)}
options={options}
events={events}
/>
)
}
then in your page
// pages/index.ts
const Graph = dynamic(() => (import("../components/graph").then(cmp => cmp.Graph)), { ssr: false })
const Index = () => {
return (
<>
<Graph data={...} .... />
</>
)
}
export default Index;
I'm trying to migrate a code base from a class component to a functional component, but when I do that my history (browserHistory) and the redirects stop working.
I don't know if it's in the way I'm using useEffect, or forcing the history.push ? But I get a Warning: You cannot change <Router history> on Router and Connected Router. I get the right values from the state and redux and I can see the changes on the location when I console.log it is just that history.push that doesn't happen.
I'm using react-redux and connected-react-router.
What might the problem be?
Use case: Facebook API loads, when user signs in with Facebook, the user is redirected to another page.
This code works: class Component (old code)
class App extends Component {
constructor(props) {
super(props);
this.facebookInterval = undefined;
this.history = createBrowserHistory();
this.state = {
hasFB: false,
error: false,
};
}
/**
* Load the Facebook SDK
*
* #return {void}
*/
componentDidMount() {
this.facebookInterval = setInterval(() => {
if (window.FB !== undefined) {
this.setState({
hasFB: true,
});
clearInterval(this.facebookInterval);
}
}, 100);
window.fbAsyncInit = function () {
window.FB.init({
appId: process.env.REACT_APP_NEW_MESSENGER_APP_ID,
autoLogAppEvents: true,
xfbml: true,
version: 'v3.3',
});
};
(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) {
return;
}
js = d.createElement(s);
js.id = id;
js.src =
'https://connect.facebook.net/en_US/sdk.js?xfbml=1&version=v9.0&appId=' +
process.env.REACT_APP_NEW_MESSENGER_APP_ID;
fjs.parentNode.insertBefore(js, fjs);
})(document, 'script', 'facebook-jssdk');
}
// [NOTE]: This works when using the class component
// the redirect happens
componentWillUpdate = (nextProps, nextState) => {
console.log('nextProps.redirect', nextProps.redirect)
console.log('nextProps', nextProps)
console.log('nextState', nextState)
if (nextProps.redirect !== undefined && nextProps.redirect !== '/') {
this.history.push(nextProps.redirect);
}
};
render() {
const { t } = this.props;
if (this.state.hasFB) {
return (
<>
<Elements stripe={stripePromise}>
<ConnectedRouter history={this.history}>{routes}</ConnectedRouter>
</Elements>
</>
);
} else {
return null;
}
}
}
const mapStateToProps = (state) => {
let isLoading = state.siteR.isLoading ? 'open' : 'close';
let data = {
redirect: state.siteR.location,
isLoading: isLoading,
user: state.userR[state.userR.id],
id: state.userR.id,
};
if (state.userR[state.userR.id] !== undefined) {
data.models = state.userR[state.userR.id].models;
}
return data;
};
const mapDispatchToProps = (dispatch) => {
return {
// ommited_code
};
};
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withTranslation(),
)(App);
Same code, but on a functional component (new code)
const App = (props) => {
console.log('props', props)
// console.log('props.redirect', props.redirect)
const [hasFB, setHasFB] = useState(false)
const [error, setError] = useState(false)
const [redirect, setRedirect] = useState(props.redirect)
const history = createBrowserHistory()
let facebookInterval = undefined
const {t} = useTranslation()
const initializeFacebook = () => {
facebookInterval = setInterval(() => {
if (window.FB !== undefined) {
setHasFB(true)
clearInterval(facebookInterval);
}
}, 100);
window.fbAsyncInit = function () {
window.FB.init({
appId: process.env.REACT_APP_NEW_MESSENGER_APP_ID,
autoLogAppEvents: true,
xfbml: true,
version: 'v3.3',
});
};
(function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) {
return;
}
js = d.createElement(s);
js.id = id;
js.src =
'https://connect.facebook.net/en_US/sdk.js?xfbml=1&version=v9.0&appId=' +
process.env.REACT_APP_NEW_MESSENGER_APP_ID;
fjs.parentNode.insertBefore(js, fjs);
})(document, 'script', 'facebook-jssdk');
}
// [NOTE]: I get the right values from props.redirect
// and the state
// the redirect just doesnt happen
const handleRedirect = () => {
if (props.redirect !== undefined && props.redirect !== '/') {
console.log('redirect on setRedirect', redirect)
setRedirect(history.push(props.redirect))
}
}
useEffect(() => {
initializeFacebook()
handleRedirect()
console.log('redirect on setRedirect', redirect)
}, [])
if (hasFB) {
return (
<>
<Elements stripe={stripePromise}>
<ConnectedRouter history={history}>{routes}</ConnectedRouter>
</Elements>
</>
);
} else {
return null;
}
}
const mapStateToProps = (state) => {
let isLoading = state.siteR.isLoading ? 'open' : 'close';
let showModelSelect = state.modelR.showModelSelect ? 'open' : 'close';
let showModelAdd = state.modelR.showModelAdd ? 'open' : 'close';
let data = {
redirect: state.siteR.location,
isLoading: isLoading,
user: state.userR[state.userR.id],
id: state.userR.id,
};
if (state.userR[state.userR.id] !== undefined) {
data.models = state.userR[state.userR.id].models;
}
return data;
};
const mapDispatchToProps = (dispatch) => {
return {
// ommited_code
};
};
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withTranslation(),
)(App);
The routes I'm trying to switch to/from
const routes = (
<div>
<Switch>
<SentryRoute exact path='/' component={UserLogin} />
<SentryRoute exact path='/user' component={Profile} />
{/* // omited_code */}
</Switch>
</div>
)
export default routes;
From the documentation, it looks like you might be able to use the additional params in the connect function.
import { push } from 'connected-react-router';
/* code stuffs */
props.push('/home');
export default connect(null, { push })(Component);
https://github.com/supasate/connected-react-router/blob/master/FAQ.md#with-react-redux
I am trying to create a scene with react-three/fiber and react-three/drei. I want to use a PerspectiveCamera and be able to pan/zoom/rotate with the mouse, but I am also trying to add some buttons that can update the camera position and target in order to have different views (eg. top view, bottom view, side view, etc). I have achieved the latter part and my buttons seem to be working as I update the target x,y,z and position x,y,z using props.
The only problem is that the camera is not responding to the mouse so I only get a fixed camera position and target.
I have included all the scene codes below.
import React,{ useRef, useState, useEffect} from 'react'
import * as THREE from 'three';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { PerspectiveCamera, Icosahedron, OrbitControls } from '#react-three/drei'
import { Canvas, useThree } from "#react-three/fiber";
function VisualizationComponent(props) {
const width = window.innerWidth;
const height = window.innerHeight;
const [controls, setControls] = useState(null);
const [threeState, setThreeState] = useState(null);
const [treeStateInitialized, setThreeStateInitialized] = useState(false);
useEffect(()=>{
if(threeState){
_.forOwn(props.objects, (value, key) => {
threeState.scene.current.add(value);
});
}
return () => {
if(controls) controls.dispose();
}
},[])
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const { objects } = props
const prevState = usePrevious({objects});
const mainCamera = useRef();
useEffect(() => {
if(!threeState) return;
if (
!treeStateInitialized ||
shouldUpdateObjects(props.objects, prevState.objects)
) {
setThreeStateInitialized(true);
garbageCollectOldObjects();
addDefaultObjects();
_.forOwn(props.objects, (value, key) => {
threeState.scene.add(value);
});
}
})
const addDefaultObjects = () => {
if (threeState) {
var hemiLight = new THREE.HemisphereLight( 0xffffbb, 0x080820, 0.2 );
hemiLight.position.set( 0, 0, 1 );
threeState.scene.add( hemiLight );
}
}
const garbageCollectOldObjects = () => {
while (threeState && threeState.scene.children.length) {
const oldObject = threeState.scene.children[0];
oldObject.traverse((child) => {
if (child.geometry) {
child.geometry?.dispose();
if(child.material && Array.isArray(child.material)){
child.material.forEach(d => d.dispose());
}else{
child.material?.dispose();
}
}
});
threeState.scene.remove(oldObject);
}
}
const shouldUpdateObjects = (currentObjects,nextObjects) => {
const result = false;
let currentDigest = 1;
let nextDigest = 1;
_.forIn(currentObjects, (value, key) => {
currentDigest *= value.id;
});
_.forIn(nextObjects, (value, key) => {
nextDigest *= value.id;
});
return currentDigest !== nextDigest;
}
const hasAncestorWhichDisablesThreeJs = (element) => {
if (!element) return false;
let isEditable = false;
for (let i = 0; i < element.classList.length; i++) {
if (element.classList[i] === 'disable-threejs-controls') {
isEditable = true;
}
}
return isEditable ||
hasAncestorWhichDisablesThreeJs(element.parentElement);
}
const initializeScene = (state) => {
setThreeState(state);
addDefaultObjects();
}
return (
<div
id="threejs-controllers-div"
className='threejs-container'
onMouseOver={ (e) => {
const target = e.target;
if (!target || !controls) return true;
if (hasAncestorWhichDisablesThreeJs(target)) {
controls.enabled = false;
} else {
controls.enabled = true;
}
} }
>
<Canvas
className='threejs'
onCreated={ (state) => {initializeScene(state)}}
shadows={true}
gl={
{
'shadowMap.enabled' : true,
'alpha' : true
}
}
>
<PerspectiveCamera
makeDefault
ref={mainCamera}
position-x={props.cameraX || 0}
position-y={props.cameraY || -20}
position-z={props.cameraZ || 20}
up={[0, 0, 1]}
fov={ 15 }
aspect={ width / height }
near={ 1 }
far={ 10000 }
visible={false}
controls={controls}
/>
<OrbitControls
ref={controls}
camera={mainCamera.current}
domElement={document.getElementById("threejs-controllers-div")}
enabled={true}
enablePan={true}
enableZoom={true}
enableRotate={true}
target-x={props.targetX || 0}
target-y={props.targetY || 0}
target-z={props.targetZ || 0}
/>
</Canvas>
<div className='threejs-react-container'>
{ props.children }
</div>
</div>
)
}
VisualizationComponent.propTypes = {
children: PropTypes.node.isRequired,
objects: PropTypes.object.isRequired,
cameraX: PropTypes.number,
cameraY: PropTypes.number,
cameraZ: PropTypes.number,
targetX: PropTypes.number,
targetY: PropTypes.number,
targetZ: PropTypes.number,
};
export default withRouter(VisualizationComponent);
I have flatlist having images, videos. for images, I defined duration to show and then move to the next item in flatlist, but in case of the video once a video ended then move to next item.
I am using Redux for currentlyPlayingIndex and flatlist datasource.
If I have only images in flatlist it is working fine, but if I have a video, on video end I need to pass event from child to its parent. The parent calls the same method to move to the next index as for image duration end but the increment of currentlyPlayingIndex by one is not happening in case of video end.
parent component code or flatlist handler
import React, { Component } from 'react'
import { TouchableWithoutFeedback, View,StatusBar,Dimensions,FlatList } from 'react-native'
import {FileType,getFileType,getFileExtension} from '../services/FileManagerService'
import VideoPlayer from '../components/PlayerTypes/VideoPlayer'
import ImagePlayer from '../components/PlayerTypes/ImagePlayer'
import PdfPlayer from '../components/PlayerTypes/PdfPlayer'
import { ScaledSheet } from 'react-native-size-matters';
import NothingTpPlay from '../components/PlayerTypes/NothingTpPlay'
//redux
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import * as Actions from '../database/actions/ScheduleActions.js'; //Import your actions
import * as Animatable from 'react-native-animatable';
import constants from '../config/constants'
import KeepAwake from 'react-native-keep-awake';
import AudioPlayer from '../components/PlayerTypes/AudioPlayer'
import { showToastMessage } from '../utils/ToastMessage'
import I18n from "../locales/i18n-js";
import WebsitePlayer from '../components/PlayerTypes/WebsitePlayer'
import FullScreen from "../NativeBridgingHeader/FullScreen";
let deviceWidth = Dimensions.get('window').width
let deviceHeight = Dimensions.get('window').height
class PlaylistPlayerScreen extends Component {
constructor() {
super();
this.state = {
currentVisibleIndex:-1
}
this.playNextFile = this.playNextFile.bind(this)
this.videoEnded = this.videoEnded.bind(this)
this.schedulePlayDurationTimer = this.schedulePlayDurationTimer.bind(this)
this.viewabilityConfig = {
waitForInteraction: false,
itemVisiblePercentThreshold: 99,
}
}
static navigationOptions = {
header: null,
};
onViewableItemsChanged = ({ viewableItems }) => {
// viewableItems will show you what items are in view
// console.log("onViewableItemsChanged called" + JSON.stringify(viewableItems))
if(viewableItems.length >= 1) {
const visibleFileIndex = viewableItems[0].index
// console.log("visible index " + visibleFileIndex)
this.setState({currentVisibleIndex:visibleFileIndex})
const file = this.props.schedulesFiles[visibleFileIndex]
const fileType = getFileType(file)
console.log("file type is " + fileType)
if (fileType == FileType.Video) {
console.log("video file type")
} else {
this.schedulePlayDurationTimer(visibleFileIndex)
}
}
}
getItemLayout = (data, index) => ({
length: deviceWidth,
offset: deviceWidth * index,
index,
})
componentDidMount(){
this.props.getScheduleFiles()
}
shouldComponentUpdate(nextProps, nextState) {
return true
}
componentDidUpdate(){
console.log("componentDidUpdate")
}
schedulePlayDurationTimer(file_index) {
const file = this.props.schedulesFiles[file_index]
const playDuration = file.play_duration_in_milliseconds
this.timer = setTimeout(() => {
clearTimeout(this.timer)
this.playNextFile()
}, playDuration);
}
videoEnded = () => {
console.log("video ended")
this.playNextFile()
}
playNextFile = () => {
if(this.props.currentlyPlayingIndex == (this.props.schedulesFiles.length - 1)) {
//last file played
this.props.getScheduleFiles()
this.props.playNextFile(this.props.schedulesFiles,this.props.currentlyPlayingIndex)
this.listRef.scrollToIndex({animated: false, index: this.props.currentlyPlayingIndex})
} else {
console.log("playNextFile current index " + this.props.currentlyPlayingIndex)
this.props.playNextFile(this.props.schedulesFiles,this.props.currentlyPlayingIndex)
console.log("playNextFile next index " + this.props.currentlyPlayingIndex)
this.listRef.scrollToIndex({animated: true, index: this.props.currentlyPlayingIndex})
}
}
_renderItem = ({item, index}) => {
return (
this.renderPlayer(item,index)
);
}
renderPlayer(file,index) {
switch (getFileType(file)) {
case FileType.Video:
return <VideoPlayer file={file} onEnd={this.videoEnded} currentIndex={index} currentVisibleIndex={this.state.currentVisibleIndex} />
case FileType.Audio:
return <AudioPlayer file={file} onEnd={this.playNextFile} />
case FileType.Image:
return <ImagePlayer file={file} onEnd={this.playNextFile} />
case FileType.Pdf:
return <PdfPlayer file={file} onEnd={this.playNextFile} />
case FileType.WebpageContent:
return <WebsitePlayer file={file} onEnd={this.playNextFile} />
default:
showToastMessage(
I18n.t('ErrorMessage.FormatNotSupported', {
name: getFileExtension(file).toUpperCase()
})
)
this.playNextFile()
}
}
render() {
if(this.props.schedulesFiles.length > 0 ) {
return (
<View style={{flex:1}}>
<StatusBar hidden={true} />
<FlatList
style={{flex:1}}
bounces={false}
removeClippedSubviews={true}
scrollEnabled={false}
showsHorizontalScrollIndicator={false}
ref={el => this.listRef = el}
horizontal={true}
keyExtractor={(item, index) => index.toString()}
data={this.props.schedulesFiles}
renderItem={this._renderItem}
onViewableItemsChanged={this.onViewableItemsChanged}
viewabilityConfig={this.viewabilityConfig}
getItemLayout={this.getItemLayout}
initialNumToRender={2}
maxToRenderPerBatch={2}
windowSize={this.props.schedulesFiles.length}
/>
<KeepAwake />
</View>
)
}else {
return (
<TouchableWithoutFeedback delayLongPress={constants.REVEAL_SIDE_BAR_MENU_PRESS_DURATION} onLongPress={() => this.props.navigation.openDrawer()}>
<View style={styles.container}>
<NothingTpPlay/>
<KeepAwake />
</View>
</TouchableWithoutFeedback>
)
}
}
}
const styles = ScaledSheet.create({
container: {
flex:1,
backgroundColor : 'white',
}
});
//redux binding
// The function takes data from the app current state,
// and insert/links it into the props of our component.
// This function makes Redux know that this component needs to be passed a piece of the state
function mapStateToProps(state, props) {
return {
loading: state.scheduleReducer.loading,
schedulesFiles: state.scheduleReducer.data,
currentlyPlayingIndex: state.scheduleReducer.nextFileIndex,
}
}
// Doing this merges our actions into the component’s props,
// while wrapping them in dispatch() so that they immediately dispatch an Action.
// Just by doing this, we will have access to the actions defined in out actions file (action/home.js)
function mapDispatchToProps(dispatch) {
return bindActionCreators(Actions, dispatch);
}
//Connect everything
export default connect(mapStateToProps, mapDispatchToProps)(PlaylistPlayerScreen);
Render method for video player child component code is:
<VideoPlayer file={file} onEnd={this.videoEnded} currentIndex={index} currentVisibleIndex={this.state.currentVisibleIndex} />
VideoPlayer.js relevant code
export default class VideoPlayer extends React.Component {
constructor() {
super();
this.state = {
}
this.videoEnded = this.videoEnded.bind(this)
}
videoEnded() {
if (this.props.shouldRepeat == true) {
} else {
this.video.paused = true
this.video.seek(0)
}
this.props.onEnd()
}
render() {
return (
<Video
ref={ref => {
this.video = ref;
}}
onError={this.videoEnded}
minLoadRetryCount={1}
useTextureView={true}
controls={false}
style={ContainerStyle.playerTypeStyle}
onEnd={this.videoEnded}
repeat={this.props.shouldRepeat}
playInBackground={false}
playWhenInactive={false}
ignoreSilentSwitch={"ignore"}
resizeMode={this.props.file.resize_mode}
source={{uri:getFileAbsolutePath(this.props.file)}}
paused={this.props.currentIndex != this.props.currentVisibleIndex}
/>
)
}
}
Reducer code
import { combineReducers } from 'redux';
import { SCHEDULE_REFRESHED,PLAY_NEXT_FILE } from "../actions/ScheduleActions.js" //Import the actions types constant we defined in our actions
let dataState = { data: [], loading:true };
const scheduleReducer = (state = dataState, action) => {
switch (action.type) {
case SCHEDULE_REFRESHED:
state = Object.assign({}, state, { data: action.data, nextFileIndex:action.nextFileIndex });
return state;
case PLAY_NEXT_FILE:
state = Object.assign({}, state, { nextFileIndex: action.nextFileIndex});
return state;
default:
return state;
}
}
// Combine all the reducers
const rootReducer = combineReducers({
scheduleReducer
// ,[ANOTHER REDUCER], [ANOTHER REDUCER] ....
})
export default rootReducer;
Action code
//gets called initially on app launch to get files to be played
export function getScheduleFiles(){
return (dispatch) => {
getOfflineNextScheduleFiles().then((files)=>{//get offline files/schedule first
plainFiles = convertToArray(files)
dispatch({type: SCHEDULE_REFRESHED, data:plainFiles,nextFileIndex:0});
}).catch((error)=>{//if offline schedules is not available to play, refresh online
triggerPlaylistsRefresh().then((files)=>{
plainFiles = convertToArray(files)
dispatch({type: SCHEDULE_REFRESHED, data:plainFiles,nextFileIndex:0});
}).catch((error)=>{
console.log("nothing to play")
dispatch({type: PLAY_NEXT_FILE, nextFileIndex:0});
showToastMessage(I18n.t("ErrorMessage.NoSchedulesAvailableForCurrentTimeError"))
})
})
}
}
//get called from PlaylistPlayerScreen after each file played
export function playNextFile(files,filePlayedIndex){
return (dispatch) => {
if(filePlayedIndex < files.length-1) {
dispatch({type: PLAY_NEXT_FILE, nextFileIndex:filePlayedIndex+1});
}else {
console.log("all files played")
dispatch({type: PLAY_NEXT_FILE, nextFileIndex:0});
}
}
}
This is the smart component:
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Filter from '../Filter';
import Sort from '../Sort';
import { getItems, selectItem, reverseItems, findItems } from '../../actions/items';
import './app.css';
const App = ({filterList, sortList, onGetItems, onFindItems, reverseItems, onSelectItem}) => {
onGetItems();
return (
<div>
<Filter items={filterList} findText={onFindItems} reverseItems={reverseItems} selectItem={onSelectItem} />
<Sort items={sortList} selectItem={onSelectItem} />
</div>
)}
function mapStateToProps(state) {
return {
filterList: state.items.filter(item => item.name.includes(state.filter.toLowerCase())),
sortList: state.items,
}
}
function matchDispatchToProps(dispatch) {
return bindActionCreators({
onGetItems: getItems,
onSelectItem: selectItem,
onFindItems: findItems,
reverseItems: reverseItems
}, dispatch)}
export default connect(mapStateToProps, matchDispatchToProps)(App);
and actions:
let items = [];
(function onGetItems() {
let xhr = new XMLHttpRequest();
xhr.open('GET', '/items.json', false);
xhr.send();
if (xhr.status !== 200) {
console.log(xhr.status + ': ' + xhr.statusText);
} else {
items = JSON.parse(xhr.responseText.toLowerCase());
items.sort(function(a, b) {
if (a.name > b.name) return 1;
if (a.name < b.name) return -1;
return 0;
});
}
})();
export const getItems = () => dispatch => {
dispatch({ type: 'ADD_ITEMS', payload: items });
}
export const selectItem = (item) => {
console.log(item);
return {
type: "ITEM_SELECTED",
payload: item
}
};
export const reverseItems = (items) => {
console.log(items)
return {
type: "REVERSE_ITEMS",
payload: items.reverse()
}
};
export const findItems = (items) => {
return {
type: "FIND_ITEMS",
payload: items
}
};
and 2 reducers:
const initialState = '';
export default function filter(state = initialState, action) {
switch (action.type) {
case 'FIND_ITEMS': return action.payload;
default: return state
}
}
const initialState = [];
export default function items(state = initialState, action) {
switch (action.type) {
case 'ADD_ITEMS': return action.payload;
default: return state
}
}
The action reverseItems reverses the array, but the problem is that it doesn't rewrite state because it's formed by another action.
I realize that it's a basic issue, but I can't get how to do that.
Replace your
items.sort() statement with [...items].sort() which creates a new reference to the array and allows the re-rendering of the component. The sort function sorts the array using the same reference and does not cause a re-render.
Try to use Redux Thunk for your async calls.
You can dispatch an action, for example RECEIVED_ITEMS after your http request.