├── .expo-shared └── assets.json ├── .gitignore ├── App.js ├── README.md ├── app.json ├── assets ├── icon.png └── splash.png ├── babel.config.js ├── package-lock.json └── package.json /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | web-report/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { 3 | StyleSheet, 4 | Text, 5 | View, 6 | Dimensions, 7 | TouchableWithoutFeedback, 8 | Image, 9 | Modal, 10 | Animated, 11 | StatusBar, 12 | Button, 13 | TouchableOpacity, 14 | BackHandler, 15 | } from 'react-native'; 16 | import { Video } from 'expo-av'; 17 | import Constants from 'expo-constants'; 18 | import { Ionicons } from '@expo/vector-icons'; 19 | import * as VideoThumbnails from 'expo-video-thumbnails'; 20 | import { LinearGradient } from 'expo-linear-gradient'; 21 | const { width, height } = Dimensions.get('window'); 22 | const screenRatio = height / width; 23 | 24 | export default function App() { 25 | // THE CONTENT 26 | const [content, setContent] = useState([ 27 | { 28 | content: 29 | 'https://firebasestorage.googleapis.com/v0/b/instagram-clone-f3106.appspot.com/o/1.jpg?alt=media&token=63304587-513b-436d-a228-a6dc0680a16a', 30 | type: 'image', 31 | finish: 0, 32 | }, 33 | { 34 | content: 35 | 'https://firebasestorage.googleapis.com/v0/b/instagram-clone-f3106.appspot.com/o/2.mp4?alt=media&token=fcd41460-a441-4841-98da-d8f9a714dd4d', 36 | type: 'video', 37 | finish: 0, 38 | }, 39 | { 40 | content: 41 | 'https://firebasestorage.googleapis.com/v0/b/instagram-clone-f3106.appspot.com/o/3.jpg?alt=media&token=326c1809-adc2-4a23-b9c3-8995df7fcccd', 42 | type: 'image', 43 | finish: 0, 44 | }, 45 | { 46 | content: 47 | 'https://firebasestorage.googleapis.com/v0/b/instagram-clone-f3106.appspot.com/o/4.jpg?alt=media&token=e9c5bead-4d9f-40d9-b9fa-c6bc12dd6134', 48 | type: 'image', 49 | finish: 0, 50 | }, 51 | { 52 | content: 53 | 'https://firebasestorage.googleapis.com/v0/b/instagram-clone-f3106.appspot.com/o/5.jpg?alt=media&token=7dcb6c3a-8080-4448-bb2c-c9594e70e572', 54 | type: 'image', 55 | finish: 0, 56 | }, 57 | { 58 | content: 59 | 'https://firebasestorage.googleapis.com/v0/b/instagram-clone-f3106.appspot.com/o/6.jpg?alt=media&token=1121dc71-927d-4517-9a53-23ede1e1b386', 60 | type: 'image', 61 | finish: 0, 62 | }, 63 | { 64 | content: 65 | 'https://firebasestorage.googleapis.com/v0/b/instagram-clone-f3106.appspot.com/o/7.jpg?alt=media&token=7e92782a-cd84-43b6-aba6-6fe6269eded6', 66 | type: 'image', 67 | finish: 0, 68 | }, 69 | { 70 | content: 71 | 'https://firebasestorage.googleapis.com/v0/b/instagram-clone-f3106.appspot.com/o/8.mp4?alt=media&token=5b6af212-045b-4195-9d65-d1cab613bd7f', 72 | type: 'video', 73 | finish: 0, 74 | }, 75 | { 76 | content: 77 | 'https://firebasestorage.googleapis.com/v0/b/instagram-clone-f3106.appspot.com/o/9.jpg?alt=media&token=0a382e94-6f3f-4d4e-932f-e3c3f085ebc3', 78 | type: 'image', 79 | finish: 0, 80 | }, 81 | ]); 82 | // i use modal for opening the instagram stories 83 | const [modalVisible, setModalVisible] = useState(false); 84 | // for get the duration 85 | const [end, setEnd] = useState(0); 86 | // current is for get the current content is now playing 87 | const [current, setCurrent] = useState(0); 88 | // if load true then start the animation of the bars at the top 89 | const [load, setLoad] = useState(false); 90 | // progress is the animation value of the bars content playing the current state 91 | const progress = useRef(new Animated.Value(0)).current; 92 | 93 | // I WAS THINKING TO GET THE VIDEO THUMBNAIL BEFORE THE VIDEO LOADS UP 94 | 95 | // const [thumbnail, setThumbnail] = useState(''); 96 | // useEffect(() => { 97 | // generateThumbnail(); 98 | // }, []); 99 | // generateThumbnail = async () => { 100 | // for (let i = 0; i < content.length; i++) { 101 | // if (content[i].type == 'video') { 102 | // try { 103 | // const { uri } = await VideoThumbnails.getThumbnailAsync( 104 | // content[i].content, 105 | // { 106 | // time: 0, 107 | // } 108 | // ); 109 | // console.log(i + ' ' + content[i].content); 110 | // console.log(i + ' ' + uri); 111 | // let data = [...content]; 112 | // content[i].thumbnail = uri; 113 | // setContent(data); 114 | // } catch (e) { 115 | // console.log(i + ' ' + e); 116 | // } 117 | // } 118 | // } 119 | // }; 120 | 121 | // start() is for starting the animation bars at the top 122 | function start(n) { 123 | // checking if the content type is video or not 124 | if (content[current].type == 'video') { 125 | // type video 126 | if (load) { 127 | Animated.timing(progress, { 128 | toValue: 1, 129 | duration: n, 130 | }).start(({ finished }) => { 131 | if (finished) { 132 | next(); 133 | } 134 | }); 135 | } 136 | } else { 137 | // type image 138 | Animated.timing(progress, { 139 | toValue: 1, 140 | duration: 5000, 141 | }).start(({ finished }) => { 142 | if (finished) { 143 | next(); 144 | } 145 | }); 146 | } 147 | } 148 | 149 | // handle playing the animation 150 | function play() { 151 | start(end); 152 | } 153 | 154 | // next() is for changing the content of the current content to +1 155 | function next() { 156 | // check if the next content is not empty 157 | if (current !== content.length - 1) { 158 | let data = [...content]; 159 | data[current].finish = 1; 160 | setContent(data); 161 | setCurrent(current + 1); 162 | progress.setValue(0); 163 | setLoad(false); 164 | } else { 165 | // the next content is empty 166 | close(); 167 | } 168 | } 169 | 170 | // previous() is for changing the content of the current content to -1 171 | function previous() { 172 | // checking if the previous content is not empty 173 | if (current - 1 >= 0) { 174 | let data = [...content]; 175 | data[current].finish = 0; 176 | setContent(data); 177 | setCurrent(current - 1); 178 | progress.setValue(0); 179 | setLoad(false); 180 | } else { 181 | // the previous content is empty 182 | close(); 183 | } 184 | } 185 | 186 | // closing the modal set the animation progress to 0 187 | function close() { 188 | progress.setValue(0); 189 | setLoad(false); 190 | setModalVisible(false); 191 | } 192 | 193 | return ( 194 | 195 | 196 | {/* MODAL */} 197 | 198 | 199 | 200 | {/* check the content type is video or an image */} 201 | {content[current].type == 'video' ? ( 202 | 241 | 247 | 257 | {/* ANIMATION BARS */} 258 | 265 | {content.map((index, key) => { 266 | return ( 267 | // THE BACKGROUND 268 | 278 | {/* THE ANIMATION OF THE BAR*/} 279 | 286 | 287 | ); 288 | })} 289 | 290 | {/* END OF ANIMATION BARS */} 291 | 292 | 301 | {/* THE AVATAR AND USERNAME */} 302 | 303 | 310 | 317 | kikidding 318 | 319 | 320 | {/* END OF THE AVATAR AND USERNAME */} 321 | {/* THE CLOSE BUTTON */} 322 | { 324 | close(); 325 | }} 326 | > 327 | 336 | 337 | 338 | 339 | {/* END OF CLOSE BUTTON */} 340 | 341 | {/* HERE IS THE HANDLE FOR PREVIOUS AND NEXT PRESS */} 342 | 343 | previous()}> 344 | 345 | 346 | next()}> 347 | 348 | 349 | 350 | {/* END OF THE HANDLE FOR PREVIOUS AND NEXT PRESS */} 351 | 352 | 353 | 354 | {/* END OF MODAL */} 355 | setModalVisible(true)}> 356 | 366 | 375 | 388 | 389 | 390 | 391 | 392 | Click the image to open instagram story clone 393 | 394 | 395 | ); 396 | } 397 | 398 | const styles = StyleSheet.create({ 399 | container: { 400 | flex: 1, 401 | backgroundColor: '#fff', 402 | alignItems: 'center', 403 | justifyContent: 'center', 404 | }, 405 | containerModal: { 406 | flex: 1, 407 | backgroundColor: '#000', 408 | }, 409 | backgroundContainer: { 410 | position: 'absolute', 411 | 412 | top: 0, 413 | bottom: 0, 414 | left: 0, 415 | right: 0, 416 | }, 417 | }); 418 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instagram story with react native and expo 2 | ### with expo-av 3 | ![thumbnail](https://miro.medium.com/max/700/1*5AGVrbnz-IvA6xDsMQEaPQ.jpeg) 4 | ###### Read it full here : (https://medium.com/@kikidding/instagram-stories-clone-with-react-native-expo-e68683c9faaa) 5 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Instagram Story Clone", 4 | "slug": "instagramstoryclone", 5 | "platforms": [ 6 | "ios", 7 | "android", 8 | "web" 9 | ], 10 | "version": "1.0.0", 11 | "orientation": "portrait", 12 | "icon": "./assets/icon.png", 13 | "splash": { 14 | "image": "./assets/splash.png", 15 | "resizeMode": "contain", 16 | "backgroundColor": "#ffffff" 17 | }, 18 | "updates": { 19 | "fallbackToCacheTimeout": 0 20 | }, 21 | "assetBundlePatterns": [ 22 | "**/*" 23 | ], 24 | "ios": { 25 | "supportsTablet": true 26 | }, 27 | "description": "Instagram story clone with expo av", 28 | "githubUrl": "https://github.com/codingki/instagram-story-react-native-expo" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/instagram-story-react-native-expo/9af097738a6df58bcd9574cce8b20c5a74f13905/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/instagram-story-react-native-expo/9af097738a6df58bcd9574cce8b20c5a74f13905/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "~37.0.3", 12 | "expo-av": "~8.1.0", 13 | "react": "~16.9.0", 14 | "react-dom": "~16.9.0", 15 | "react-native": "https://github.com/expo/react-native/archive/sdk-37.0.1.tar.gz", 16 | "react-native-video": "^4.4.5", 17 | "react-native-web": "~0.11.7", 18 | "expo-video-thumbnails": "~4.1.0", 19 | "expo-linear-gradient": "~8.1.0" 20 | }, 21 | "devDependencies": { 22 | "babel-preset-expo": "~8.1.0", 23 | "@babel/core": "^7.8.6" 24 | }, 25 | "private": true 26 | } 27 | --------------------------------------------------------------------------------