├── .DS_Store ├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json └── src ├── .DS_Store ├── assets ├── .DS_Store └── avatars │ └── no_avatar.png ├── components ├── .DS_Store ├── avatar │ └── view │ │ ├── avatarStyles.js │ │ └── avatarView.js ├── button │ └── view │ │ └── buttonView.js ├── index.js ├── stories │ └── view │ │ ├── storiesStyles.js │ │ └── storiesView.js ├── story │ └── view │ │ ├── storyStyles.js │ │ └── storyView.js ├── storyItem │ └── view │ │ ├── storyItemStyles.js │ │ └── storyItemView.js ├── storyList │ └── view │ │ ├── storyListStyles.js │ │ └── storyListView.js └── storyListItem │ └── view │ ├── storyListItemStyles.js │ └── storyListItemView.js └── index.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ue/react-native-story/adcb8bb2aa60ce38d875c838b048a07938c81080/.DS_Store -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ue] 4 | patreon: ugurerdal 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 uğur erdal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-story 2 | 3 | A react native component instagram stories 4 | 5 | [![License MIT](http://img.shields.io/badge/license-MIT-orange.svg?style=flat)](https://raw.githubusercontent.com/ue/react-native-story/master/LICENSE) 6 | [ ![NPM version](http://img.shields.io/npm/v/react-native-story.svg?style=flat)](https://www.npmjs.com/package/react-native-story) 7 | 8 | ## Installation 9 | 10 | - 1.Run `npm i react-native-story --save` or `yarn add react-native-story` 11 | - 2.`import Story from 'react-native-story'` 12 | 13 | ![Screenshots](https://media.giphy.com/media/f9RH5B7kYeFvQFhRq7/giphy.gif) 14 | 15 | ## Getting started 16 | 17 | Add `react-native-story` to your js file. 18 | 19 | `import Story from 'react-native-story'` 20 | 21 | Inside your component's render method, use Story: 22 | 23 | ```javascript 24 | const stories = [ 25 | { 26 | id: "4", 27 | source: require("../../../assets/stories/4.jpg"), 28 | user: "Ugur Erdal", 29 | avatar: require("../../../assets/avatars/ugurerdal.png") 30 | }, 31 | { 32 | id: "2", 33 | source: require("../../../assets/stories/2.jpg"), 34 | user: "Mustafa", 35 | avatar: require("../../../assets/avatars/mustafa.png") 36 | }, 37 | { 38 | id: "5", 39 | source: require("../../../assets/stories/5.jpg"), 40 | user: "Emre Yilmaz", 41 | avatar: require("../../../assets/avatars/emre.png") 42 | }, 43 | { 44 | id: "3", 45 | source: require("../../../assets/stories/3.jpg"), 46 | user: "Cenk Gun", 47 | avatar: require("../../../assets/avatars/cenk.png") 48 | }, 49 | ]; 50 | 51 | render() { 52 | return ( 53 | 62 | } 63 | /> 64 | } 65 | ``` 66 | 67 | ## API 68 | 69 | | Props | Type | Optional | Default | Description | 70 | | -------------------- | ------ | -------- | --------- | -------------------------------------- | 71 | | id | string | required | - | Json story data must have this | 72 | | stories | object | required | - | As above example | 73 | | unPressedBorderColor | string | true | "#e95950" | Unpressed Border color | 74 | | pressedBorderColor | string | true | "#ebebeb" | Pressed border color | 75 | | footerComponent | jsx | true | - | Bottom of the stories footer component | 76 | 77 | 78 | Thanx for the help 79 | - [@wcandillon](https://github.com/wcandillon) 80 | https://snack.expo.io/@wcandillon/instagram-stories 81 | 82 | **MIT Licensed UE** 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-story", 3 | "description": "Instagram story for react-native", 4 | "main": "src/index.js", 5 | "scripts": { 6 | "test": "mocha --require react-native-mock/mock.js --compilers js:babel-core/register --recursive tests/**/*.test.js", 7 | "test:watch": "npm test -- --watch", 8 | "test:cover": "istanbul cover -x *.test.js _mocha -- -R spec --require react-native-mock/mock.js --compilers js:babel-core/register 'tests/**/*.test.js'", 9 | "test:report": "cat ./coverage/lcov.info | codecov && rm -rf ./coverage", 10 | "lint": "eslint src/**/*.js tests/**/*.js", 11 | "lintfix": "eslint --fix src/**/*.js tests/**/*.js", 12 | "format": "prettier-eslint --write src/**/*.js tests/**/*.js", 13 | "precommit": "lint-staged" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/ue/react-native-story" 18 | }, 19 | "keywords": [ 20 | "react-native", 21 | "instagram", 22 | "story", 23 | "snapchat" 24 | ], 25 | "author": "Ugur ERDAL", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.comue/react-native-story/issues" 29 | }, 30 | "homepage": "https://github.com/ue/react-native-story", 31 | "config": { 32 | "commitizen": { 33 | "path": "node_modules/cz-conventional-changelog" 34 | } 35 | }, 36 | "peerDependencies": { 37 | "react": ">=15.0.0", 38 | "react-native": ">=0.40.0" 39 | }, 40 | "devDependencies": { 41 | "babel": "^6.23.0", 42 | "babel-eslint": "^10.0.1", 43 | "babel-jest": "^20.0.3", 44 | "babel-preset-react-native-stage-0": "^1.0.1", 45 | "chai": "^4.0.2", 46 | "codecov.io": "^0.1.6", 47 | "cz-conventional-changelog": "^2.0.0", 48 | "enzyme": "^2.8.2", 49 | "eslint": "^4.0.0", 50 | "eslint-config-standard": "^10.2.1", 51 | "eslint-config-standard-react": "^5.0.0", 52 | "eslint-plugin-import": "^2.3.0", 53 | "eslint-plugin-node": "^5.0.0", 54 | "eslint-plugin-promise": "^3.5.0", 55 | "eslint-plugin-react": "^7.1.0", 56 | "eslint-plugin-standard": "^3.0.1", 57 | "husky": "^0.13.4", 58 | "istanbul": "^1.1.0-alpha.1", 59 | "lint-staged": "^4.0.0", 60 | "mocha": "^5.2.0", 61 | "prettier-eslint-cli": "^4.1.1", 62 | "prop-types": "^15.5.10", 63 | "react-dom": "^15.5.4", 64 | "react-native-mock": "^0.3.1", 65 | "semantic-release": "^15.13.3" 66 | }, 67 | "lint-staged": { 68 | "src/**/*.js": [ 69 | "prettier-eslint --write", 70 | "git add" 71 | ], 72 | "tests/**/*.js": [ 73 | "prettier-eslint --write", 74 | "git add" 75 | ] 76 | }, 77 | "version": "0.1.2", 78 | "directories": { 79 | "test": "tests" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ue/react-native-story/adcb8bb2aa60ce38d875c838b048a07938c81080/src/.DS_Store -------------------------------------------------------------------------------- /src/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ue/react-native-story/adcb8bb2aa60ce38d875c838b048a07938c81080/src/assets/.DS_Store -------------------------------------------------------------------------------- /src/assets/avatars/no_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ue/react-native-story/adcb8bb2aa60ce38d875c838b048a07938c81080/src/assets/avatars/no_avatar.png -------------------------------------------------------------------------------- /src/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ue/react-native-story/adcb8bb2aa60ce38d875c838b048a07938c81080/src/components/.DS_Store -------------------------------------------------------------------------------- /src/components/avatar/view/avatarStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | flexDirection: "row", 6 | padding: 16, 7 | alignItems: "center" 8 | }, 9 | avatar: { 10 | width: 36, 11 | height: 36, 12 | borderRadius: 36 / 2, 13 | marginRight: 16, 14 | }, 15 | username: { 16 | color: "white", 17 | fontSize: 16 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/avatar/view/avatarView.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from "react"; 3 | import { View, Image, Text, SafeAreaView } from "react-native"; 4 | import styles from "./avatarStyles"; 5 | import type { ImageSourcePropType } from "react-native/Libraries/Image/ImageSourcePropType"; 6 | import DEFAULT_AVATAR from "../../../assets/avatars/no_avatar.png"; 7 | 8 | export default class Avatar extends React.PureComponent { 9 | render() { 10 | const { user, avatar: source } = this.props; 11 | return ( 12 | 13 | 14 | 19 | {user} 20 | 21 | 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/button/view/buttonView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Text, TouchableOpacity } from "react-native"; 3 | 4 | // Constants 5 | 6 | // Components 7 | 8 | class ButtonView extends Component { 9 | /* Props 10 | * ------------------------------------------------ 11 | * @prop { type } name - Description.... 12 | */ 13 | 14 | constructor(props) { 15 | super(props); 16 | this.state = {}; 17 | } 18 | 19 | // Component Life Cycles 20 | 21 | // Component Functions 22 | 23 | render() { 24 | const { text, onPress } = this.props; 25 | 26 | return ( 27 | onPress()} style={{ backgroundColor: "tomato"}}> 28 | {text} 29 | 30 | ); 31 | } 32 | } 33 | 34 | export default ButtonView; 35 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Story } from "./story/view/storyView"; 2 | export { default as StoryItem } from "./storyItem/view/storyItemView"; 3 | export { default as Stories } from "./stories/view/storiesView"; 4 | export { default as StoryList } from "./storyList/view/storyListView"; 5 | export { 6 | default as StoryListItem 7 | } from "./storyListItem/view/storyListItemView"; 8 | -------------------------------------------------------------------------------- /src/components/stories/view/storiesStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | backgroundColor: "white" 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/stories/view/storiesView.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { 3 | StyleSheet, 4 | Animated, 5 | Dimensions, 6 | Platform, 7 | View, 8 | ActivityIndicator 9 | } from "react-native"; 10 | import styles from "./storiesStyles"; 11 | import { StoryItem } from "../../"; 12 | 13 | const { width } = Dimensions.get("window"); 14 | const perspective = width; 15 | const angle = Math.atan(perspective / (width / 2)); 16 | const ratio = Platform.OS === "ios" ? 2 : 1.2; 17 | 18 | export default class Stories extends PureComponent { 19 | stories = []; 20 | 21 | state = { 22 | x: new Animated.Value(0), 23 | ready: false 24 | }; 25 | 26 | constructor(props) { 27 | super(props); 28 | this.stories = props.stories.map(() => React.createRef()); 29 | } 30 | 31 | async componentDidMount() { 32 | const { x } = this.state; 33 | await x.addListener(() => 34 | this.stories.forEach((story, index) => { 35 | const offset = index * width; 36 | const inputRange = [offset - width, offset + width]; 37 | const translateX = x 38 | .interpolate({ 39 | inputRange, 40 | outputRange: [width / ratio, -width / ratio], 41 | extrapolate: "clamp" 42 | }) 43 | .__getValue(); 44 | 45 | const rotateY = x 46 | .interpolate({ 47 | inputRange, 48 | outputRange: [`${angle}rad`, `-${angle}rad`], 49 | extrapolate: "clamp" 50 | }) 51 | .__getValue(); 52 | 53 | const parsed = parseFloat( 54 | rotateY.substr(0, rotateY.indexOf("rad")), 55 | 10 56 | ); 57 | const alpha = Math.abs(parsed); 58 | const gamma = angle - alpha; 59 | const beta = Math.PI - alpha - gamma; 60 | const w = width / 2 - ((width / 2) * Math.sin(gamma)) / Math.sin(beta); 61 | const translateX2 = parsed > 0 ? w : -w; 62 | 63 | const style = { 64 | transform: [ 65 | { perspective }, 66 | { translateX }, 67 | { rotateY }, 68 | { translateX: translateX2 } 69 | ] 70 | }; 71 | story.current.setNativeProps({ style }); 72 | }) 73 | ); 74 | } 75 | 76 | _handleSelectedStoryOnLoaded = () => { 77 | this.setState({ ready: true }); 78 | }; 79 | 80 | _handleSwipeLeftRight = () => { 81 | alert("swipe"); 82 | }; 83 | 84 | render() { 85 | const { x, ready } = this.state; 86 | const { stories, selectedStory, footerComponent } = this.props; 87 | 88 | return ( 89 | 90 | {!ready && ( 91 | 100 | 101 | 102 | )} 103 | {stories 104 | .map((story, i) => ( 105 | 110 | 116 | 117 | )) 118 | .reverse()} 119 | 139 | 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/components/story/view/storyStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Platform } from "react-native"; 2 | import { Dimensions } from "react-native"; 3 | 4 | export default StyleSheet.create({ 5 | storyListContainer: { 6 | marginTop: 50 7 | }, 8 | modal: { 9 | height: Dimensions.get("window").height, 10 | width: Dimensions.get("window").width, 11 | flex: 1 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/story/view/storyView.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import { View } from "react-native"; 3 | import Modal from "react-native-modalbox"; 4 | 5 | // Components 6 | import { StoryList, Stories } from "../../../components"; 7 | 8 | import styles from "./storyStyles"; 9 | 10 | class StoryListView extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | isModalOpen: false, 15 | orderedStories: null, 16 | selectedStory: null 17 | }; 18 | } 19 | 20 | // Component Life Cycles 21 | 22 | // Component Functions 23 | _handleStoryItemPress = (item, index) => { 24 | const { stories } = this.props; 25 | 26 | this.setState({ selectedStory: item }); 27 | 28 | const _stories = Array.from(stories); 29 | 30 | const rest = _stories.splice(index); 31 | const first = _stories; 32 | 33 | const orderedStories = rest.concat(first); 34 | 35 | this.setState({ orderedStories }); 36 | this.setState({ isModalOpen: true }); 37 | }; 38 | 39 | render() { 40 | const { 41 | stories, 42 | footerComponent, 43 | unPressedBorderColor, 44 | pressedBorderColor 45 | } = this.props; 46 | const { isModalOpen, orderedStories, selectedStory } = this.state; 47 | 48 | return ( 49 | 50 | 51 | 57 | 58 | this.setState({ isModalOpen: false })} 62 | position="center" 63 | swipeToClose 64 | swipeArea={250} 65 | backButtonClose 66 | coverScreen={true} 67 | > 68 | 73 | 74 | 75 | ); 76 | } 77 | } 78 | 79 | export default StoryListView; 80 | -------------------------------------------------------------------------------- /src/components/storyItem/view/storyItemStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Platform } from "react-native"; 2 | import { Dimensions } from "react-native"; 3 | 4 | export default StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | }, 8 | image: { 9 | ...StyleSheet.absoluteFillObject, 10 | width: null, 11 | height: Dimensions.get("window").height, 12 | }, 13 | footer: { 14 | flexDirection: 'row', 15 | alignItems: 'center', 16 | justifyContent: 'space-between', 17 | padding: 16, 18 | }, 19 | }); -------------------------------------------------------------------------------- /src/components/storyItem/view/storyItemView.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Fragment, PureComponent } from "react"; 3 | import { View, Image } from "react-native"; 4 | 5 | import styles from "./storyItemStyles"; 6 | 7 | import Avatar from "../../avatar/view/avatarView"; 8 | 9 | export default class extends PureComponent { 10 | render() { 11 | const { 12 | story: { source, user, avatar, id }, 13 | selectedStory, 14 | handleSelectedStoryOnLoaded, 15 | footerComponent 16 | } = this.props; 17 | return ( 18 | 19 | 20 | 22 | selectedStory && 23 | selectedStory.id === id && 24 | handleSelectedStoryOnLoaded() 25 | } 26 | style={styles.image} 27 | {...{ source }} 28 | /> 29 | 30 | 31 | {footerComponent && ( 32 | {footerComponent} 33 | )} 34 | 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/storyList/view/storyListStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | //flex: 1 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /src/components/storyList/view/storyListView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { View, FlatList } from "react-native"; 3 | // Components 4 | import { StoryListItem } from "../../../components"; 5 | import styles from "./storyListStyles"; 6 | 7 | class StoryListView extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = {}; 11 | } 12 | 13 | // Component Life Cycles 14 | 15 | // Component Functions 16 | 17 | render() { 18 | const { 19 | stories, 20 | handleStoryItemPress, 21 | unPressedBorderColor, 22 | pressedBorderColor 23 | } = this.props; 24 | 25 | return ( 26 | 27 | index.toString()} 29 | data={stories} 30 | horizontal 31 | renderItem={({ item, index }) => ( 32 | 34 | handleStoryItemPress && handleStoryItemPress(item, index) 35 | } 36 | unPressedBorderColor={unPressedBorderColor} 37 | pressedBorderColor={pressedBorderColor} 38 | item={item} 39 | /> 40 | )} 41 | /> 42 | 43 | ); 44 | } 45 | } 46 | 47 | export default StoryListView; 48 | -------------------------------------------------------------------------------- /src/components/storyListItem/view/storyListItemStyles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | marginVertical: 5 6 | }, 7 | unPressedAvatar: { 8 | borderColor: "#e95950" 9 | }, 10 | pressedAvatar: { 11 | borderColor: "#ebebeb" 12 | }, 13 | avatarWrapper: { 14 | borderWidth: 2, 15 | justifyContent: "center", 16 | alignItems: "center", 17 | borderColor: "#e95950", 18 | margin: 8, 19 | borderRadius: 57 / 2, 20 | height: 57, 21 | width: 57 22 | }, 23 | avatar: { 24 | height: 50, 25 | width: 50, 26 | borderRadius: 50 / 2, 27 | borderColor: "white", 28 | borderWidth: 1 29 | }, 30 | itemText: { 31 | textAlign: "center", 32 | fontSize: 9 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/storyListItem/view/storyListItemView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { View, Image, TouchableOpacity, Text } from "react-native"; 3 | 4 | // Constants 5 | import DEFAULT_AVATAR from "../../../assets/avatars/no_avatar.png"; 6 | 7 | // Components 8 | import styles from "./storyListItemStyles"; 9 | 10 | class StoryListItemView extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | isPressed: false 15 | }; 16 | } 17 | 18 | // Component Life Cycles 19 | 20 | // Component Functions 21 | _handleItemPress = item => { 22 | const { handleStoryItemPress } = this.props; 23 | 24 | if (handleStoryItemPress) handleStoryItemPress(item); 25 | 26 | this.setState({ isPressed: true }); 27 | }; 28 | 29 | render() { 30 | const { item, unPressedBorderColor, pressedBorderColor } = this.props; 31 | const { isPressed } = this.state; 32 | 33 | return ( 34 | 35 | this._handleItemPress(item)} 37 | style={[ 38 | styles.avatarWrapper, 39 | !isPressed 40 | ? { 41 | borderColor: unPressedBorderColor 42 | ? unPressedBorderColor 43 | : "#e95950" 44 | } 45 | : { 46 | borderColor: pressedBorderColor 47 | ? pressedBorderColor 48 | : "#ebebeb" 49 | } 50 | ]} 51 | > 52 | 57 | 58 | {item.user} 59 | 60 | ); 61 | } 62 | } 63 | 64 | export default StoryListItemView; 65 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Story } from "./components"; 2 | 3 | export default Story; 4 | --------------------------------------------------------------------------------