├── .gitignore ├── src ├── components │ ├── InstagramStories │ │ ├── InstagramStories.styles.ts │ │ └── index.tsx │ ├── Icon │ │ ├── index.tsx │ │ └── close.tsx │ ├── Footer │ │ ├── Footer.styles.ts │ │ └── index.tsx │ ├── Content │ │ ├── Content.styles.ts │ │ └── index.tsx │ ├── Animation │ │ ├── Animation.styles.ts │ │ └── index.tsx │ ├── Progress │ │ ├── Progress.styles.ts │ │ ├── index.tsx │ │ └── item.tsx │ ├── Modal │ │ ├── Modal.styles.ts │ │ ├── gesture.tsx │ │ └── index.tsx │ ├── List │ │ ├── List.styles.ts │ │ └── index.tsx │ ├── Image │ │ ├── Image.styles.ts │ │ ├── video.tsx │ │ └── index.tsx │ ├── Avatar │ │ ├── Avatar.styles.ts │ │ └── index.tsx │ ├── Header │ │ ├── Header.styles.ts │ │ └── index.tsx │ ├── AvatarList │ │ └── index.tsx │ └── Loader │ │ └── index.tsx ├── core │ ├── dto │ │ ├── helpersDTO.ts │ │ ├── instagramStoriesDTO.ts │ │ └── componentsDTO.ts │ ├── constants │ │ └── index.ts │ └── helpers │ │ └── storage.ts ├── assets │ └── images │ │ ├── demo.gif │ │ └── logo.png ├── index.tsx └── declarations.d.ts ├── commitlint.config.js ├── .husky └── commit-msg ├── babel.config.js ├── tsconfig.json ├── LICENSE ├── .github └── workflows │ ├── public.yml │ └── release.yml ├── .eslintrc.js ├── package.json ├── jest.setup.js ├── CHANGELOG.md ├── tests └── index.test.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | /dist -------------------------------------------------------------------------------- /src/components/InstagramStories/InstagramStories.styles.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /src/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import Close from './close'; 2 | 3 | export { Close }; 4 | -------------------------------------------------------------------------------- /src/core/dto/helpersDTO.ts: -------------------------------------------------------------------------------- 1 | export interface ProgressStorageProps { 2 | [key: string]: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdwingo/react-native-instagram-stories/HEAD/src/assets/images/demo.gif -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birdwingo/react-native-instagram-stories/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | npm run test -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | presets: ['module:metro-react-native-babel-preset'], 4 | plugins: ['react-native-reanimated/plugin'], 5 | }; 6 | 7 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create( { 4 | container: { 5 | position: 'absolute', 6 | bottom: 0, 7 | left: 0, 8 | right: 0, 9 | }, 10 | } ); 11 | -------------------------------------------------------------------------------- /src/components/Content/Content.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create( { 4 | container: { 5 | position: 'absolute', 6 | top: 0, 7 | left: 0, 8 | bottom: 0, 9 | right: 0, 10 | }, 11 | } ); 12 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import InstagramStories from './components/InstagramStories'; 2 | import { InstagramStoriesProps, InstagramStoriesPublicMethods } from './core/dto/instagramStoriesDTO'; 3 | 4 | export type { InstagramStoriesProps, InstagramStoriesPublicMethods }; 5 | export default InstagramStories; 6 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.gif'; 3 | 4 | declare interface Keyframe { 5 | composite?: 'accumulate' | 'add' | 'auto' | 'replace'; 6 | easing?: string; 7 | offset?: number | null; 8 | [property: string]: string | number | null | undefined; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Animation/Animation.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create( { 4 | container: StyleSheet.absoluteFillObject, 5 | absolute: { 6 | position: 'absolute', 7 | top: 0, 8 | left: 0, 9 | }, 10 | cube: { 11 | justifyContent: 'center', 12 | }, 13 | } ); 14 | -------------------------------------------------------------------------------- /src/components/Progress/Progress.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create( { 4 | container: { 5 | position: 'absolute', 6 | top: 16, 7 | left: 16, 8 | height: 2, 9 | flexDirection: 'row', 10 | gap: 4, 11 | }, 12 | item: { 13 | height: 3, 14 | borderRadius: 8, 15 | overflow: 'hidden', 16 | }, 17 | } ); 18 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { HEIGHT, WIDTH } from '../../core/constants'; 3 | 4 | export default StyleSheet.create( { 5 | container: { 6 | flex: 1, 7 | }, 8 | absolute: { 9 | position: 'absolute', 10 | top: 0, 11 | left: 0, 12 | width: WIDTH, 13 | height: HEIGHT, 14 | }, 15 | bgAnimation: StyleSheet.absoluteFillObject, 16 | } ); 17 | -------------------------------------------------------------------------------- /src/components/List/List.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { WIDTH } from '../../core/constants'; 3 | 4 | export default StyleSheet.create( { 5 | container: { 6 | borderRadius: 8, 7 | overflow: 'hidden', 8 | width: WIDTH, 9 | }, 10 | content: { 11 | position: 'absolute', 12 | top: 0, 13 | left: 0, 14 | bottom: 0, 15 | right: 0, 16 | zIndex: 3, 17 | }, 18 | } ); 19 | -------------------------------------------------------------------------------- /src/components/Image/Image.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create( { 4 | container: { 5 | position: 'absolute', 6 | top: 0, 7 | left: 0, 8 | bottom: 0, 9 | right: 0, 10 | alignItems: 'center', 11 | justifyContent: 'center', 12 | zIndex: 2, 13 | }, 14 | image: { 15 | alignItems: 'center', 16 | justifyContent: 'center', 17 | }, 18 | } ); 19 | -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { AVATAR_OFFSET } from '../../core/constants'; 3 | 4 | export default StyleSheet.create( { 5 | container: { 6 | flexDirection: 'row', 7 | alignItems: 'center', 8 | }, 9 | avatar: { 10 | left: AVATAR_OFFSET, 11 | top: AVATAR_OFFSET, 12 | position: 'absolute', 13 | }, 14 | name: { 15 | alignItems: 'center', 16 | }, 17 | } ); 18 | -------------------------------------------------------------------------------- /src/components/Modal/gesture.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { PanGestureHandler, PanGestureHandlerProps, gestureHandlerRootHOC } from 'react-native-gesture-handler'; 3 | 4 | const GestureHandler = gestureHandlerRootHOC( 5 | ( { children, onGestureEvent } : PanGestureHandlerProps ) => ( 6 | {children} 7 | ), 8 | ); 9 | 10 | export default memo( GestureHandler ); 11 | -------------------------------------------------------------------------------- /src/components/Header/Header.styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create( { 4 | container: { 5 | position: 'absolute', 6 | left: 16, 7 | top: 32, 8 | }, 9 | containerFlex: { 10 | flexDirection: 'row', 11 | justifyContent: 'space-between', 12 | alignItems: 'center', 13 | }, 14 | left: { 15 | flexDirection: 'row', 16 | alignItems: 'center', 17 | gap: 12, 18 | flex: 1, 19 | }, 20 | avatar: { 21 | borderWidth: 1.5, 22 | borderColor: '#FFF', 23 | overflow: 'hidden', 24 | }, 25 | } ); 26 | -------------------------------------------------------------------------------- /src/components/Icon/close.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from 'react'; 2 | import { Path, Svg } from 'react-native-svg'; 3 | import { IconProps } from '../../core/dto/componentsDTO'; 4 | 5 | const Close: FC = ( { color } ) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default memo( Close ); 12 | -------------------------------------------------------------------------------- /src/core/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | 3 | export const { width: WIDTH, height: HEIGHT } = Dimensions.get( 'screen' ); 4 | 5 | export const STORAGE_KEY = '@birdwingo/react-native-instagram-stories'; 6 | 7 | export const DEFAULT_COLORS = [ '#F7B801', '#F18701', '#F35B04', '#F5301E', '#C81D4E', '#8F1D4E' ]; 8 | export const LOADER_COLORS = [ '#FFF' ]; 9 | export const SEEN_LOADER_COLORS = [ '#2A2A2C' ]; 10 | export const PROGRESS_COLOR = '#00000099'; 11 | export const PROGRESS_ACTIVE_COLOR = '#FFFFFF'; 12 | export const BACKGROUND_COLOR = '#000000'; 13 | export const CLOSE_COLOR = '#00000099'; 14 | 15 | export const LOADER_ID = 'gradient'; 16 | export const LOADER_URL = `url(#${LOADER_ID})`; 17 | 18 | export const STROKE_WIDTH = 2; 19 | 20 | export const AVATAR_SIZE = 60; 21 | export const AVATAR_OFFSET = 5; 22 | export const STORY_AVATAR_SIZE = 26; 23 | 24 | export const STORY_ANIMATION_DURATION = 800; 25 | export const ANIMATION_DURATION = 10000; 26 | export const LONG_PRESS_DURATION = 500; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "lib": [ 6 | "ES2021" 7 | ], 8 | "allowJs": true, 9 | "jsx": "react-native", 10 | "declaration": true, 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "declarationDir": "./dist", 14 | "noEmit": true, 15 | "incremental": true, 16 | "isolatedModules": true, 17 | "strict": true, 18 | "noImplicitAny": true, 19 | "moduleResolution": "node", 20 | "baseUrl": "./src", 21 | "paths": { 22 | "~/*": [ 23 | "*" 24 | ] 25 | }, 26 | "types": ["react"], 27 | "allowSyntheticDefaultImports": true, 28 | "esModuleInterop": true, 29 | "skipLibCheck": false, 30 | "resolveJsonModule": true, 31 | "noUncheckedIndexedAccess": true 32 | }, 33 | "include": [ 34 | "src/**/*.ts", 35 | "src/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules", 39 | "modules", 40 | "babel.config.js", 41 | "metro.config.js", 42 | "jest.config.js", 43 | "commitlint.config.js", 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Birdwingo 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. 22 | -------------------------------------------------------------------------------- /src/components/Progress/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from 'react'; 2 | import { View } from 'react-native'; 3 | import ProgressItem from './item'; 4 | import { WIDTH } from '../../core/constants'; 5 | import ProgressStyles from './Progress.styles'; 6 | import { StoryProgressProps } from '../../core/dto/componentsDTO'; 7 | 8 | const Progress: FC = ( { 9 | progress, active, activeStory, length, 10 | progressActiveColor, progressColor, progressContainerStyle, 11 | } ) => { 12 | 13 | const width = ( ( 14 | WIDTH - ProgressStyles.container.left * 2 ) - ( length - 1 ) 15 | * ProgressStyles.container.gap ) / length; 16 | 17 | return ( 18 | 19 | {[ ...Array( length ).keys() ].map( ( val ) => ( 20 | 30 | ) )} 31 | 32 | ); 33 | 34 | }; 35 | 36 | export default memo( Progress ); 37 | -------------------------------------------------------------------------------- /src/components/Progress/item.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from 'react'; 2 | import { View } from 'react-native'; 3 | import Animated, { useAnimatedStyle } from 'react-native-reanimated'; 4 | import { StoryProgressItemProps } from '../../core/dto/componentsDTO'; 5 | import ProgressStyles from './Progress.styles'; 6 | import { PROGRESS_ACTIVE_COLOR, PROGRESS_COLOR } from '../../core/constants'; 7 | 8 | const AnimatedView = Animated.createAnimatedComponent( View ); 9 | 10 | const ProgressItem: FC = ( { 11 | progress, active, activeStory, index, width, 12 | progressActiveColor = PROGRESS_ACTIVE_COLOR, progressColor = PROGRESS_COLOR, 13 | } ) => { 14 | 15 | const animatedStyle = useAnimatedStyle( () => { 16 | 17 | if ( !active.value || activeStory.value < index ) { 18 | 19 | return { width: 0 }; 20 | 21 | } 22 | 23 | if ( activeStory.value > index ) { 24 | 25 | return { width }; 26 | 27 | } 28 | 29 | return { width: width * progress.value }; 30 | 31 | } ); 32 | 33 | return ( 34 | 35 | 38 | 39 | ); 40 | 41 | }; 42 | 43 | export default memo( ProgressItem ); 44 | -------------------------------------------------------------------------------- /src/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FC, memo, useState, useMemo, 3 | } from 'react'; 4 | import { View } from 'react-native'; 5 | import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; 6 | import { StoryContentProps } from '../../core/dto/componentsDTO'; 7 | import ContentStyles from './Footer.styles'; 8 | 9 | const StoryFooter: FC = ( { stories, active, activeStory } ) => { 10 | 11 | const [ storyIndex, setStoryIndex ] = useState( 0 ); 12 | 13 | const onChange = async () => { 14 | 15 | 'worklet'; 16 | 17 | const index = stories.findIndex( ( item ) => item.id === activeStory.value ); 18 | if ( active.value && index >= 0 && index !== storyIndex ) { 19 | 20 | runOnJS( setStoryIndex )( index ); 21 | 22 | } 23 | 24 | }; 25 | 26 | useAnimatedReaction( 27 | () => active.value, 28 | ( res, prev ) => res !== prev && onChange(), 29 | [ active.value, onChange ], 30 | ); 31 | 32 | useAnimatedReaction( 33 | () => activeStory.value, 34 | ( res, prev ) => res !== prev && onChange(), 35 | [ activeStory.value, onChange ], 36 | ); 37 | 38 | const footer = useMemo( () => stories[storyIndex]?.renderFooter?.(), [ storyIndex ] ); 39 | 40 | return footer ? {footer} : null; 41 | 42 | }; 43 | 44 | export default memo( StoryFooter ); 45 | -------------------------------------------------------------------------------- /src/components/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FC, memo, useState, useMemo, 3 | } from 'react'; 4 | import { View } from 'react-native'; 5 | import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; 6 | import { StoryContentProps } from '../../core/dto/componentsDTO'; 7 | import ContentStyles from './Content.styles'; 8 | 9 | const StoryContent: FC = ( { stories, active, activeStory } ) => { 10 | 11 | const [ storyIndex, setStoryIndex ] = useState( 0 ); 12 | 13 | const onChange = async () => { 14 | 15 | 'worklet'; 16 | 17 | const index = stories.findIndex( ( item ) => item.id === activeStory.value ); 18 | if ( active.value && index >= 0 && index !== storyIndex ) { 19 | 20 | runOnJS( setStoryIndex )( index ); 21 | 22 | } 23 | 24 | }; 25 | 26 | useAnimatedReaction( 27 | () => active.value, 28 | ( res, prev ) => res !== prev && onChange(), 29 | [ active.value, onChange ], 30 | ); 31 | 32 | useAnimatedReaction( 33 | () => activeStory.value, 34 | ( res, prev ) => res !== prev && onChange(), 35 | [ activeStory.value, onChange ], 36 | ); 37 | 38 | const content = useMemo( () => stories[storyIndex]?.renderContent?.(), [ storyIndex ] ); 39 | 40 | return content ? {content} : null; 41 | 42 | }; 43 | 44 | export default memo( StoryContent ); 45 | -------------------------------------------------------------------------------- /src/core/helpers/storage.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | import { STORAGE_KEY } from '../constants'; 3 | import { ProgressStorageProps } from '../dto/helpersDTO'; 4 | 5 | export const clearProgressStorage = async () => { 6 | 7 | try { 8 | 9 | const AsyncStorage = require( '@react-native-async-storage/async-storage' ).default; 10 | 11 | return AsyncStorage.removeItem( STORAGE_KEY ); 12 | 13 | } catch ( error ) { 14 | 15 | return null; 16 | 17 | } 18 | 19 | }; 20 | 21 | export const getProgressStorage = async (): Promise => { 22 | 23 | try { 24 | 25 | const AsyncStorage = require( '@react-native-async-storage/async-storage' ).default; 26 | 27 | const progress = await AsyncStorage.getItem( STORAGE_KEY ); 28 | 29 | return progress ? JSON.parse( progress ) : {}; 30 | 31 | } catch ( error ) { 32 | 33 | return {}; 34 | 35 | } 36 | 37 | }; 38 | 39 | export const setProgressStorage = async ( user: string, lastSeen: string ) => { 40 | 41 | const progress = await getProgressStorage(); 42 | progress[user] = lastSeen; 43 | 44 | try { 45 | 46 | const AsyncStorage = require( '@react-native-async-storage/async-storage' ).default; 47 | 48 | await AsyncStorage.setItem( STORAGE_KEY, JSON.stringify( progress ) ); 49 | 50 | return progress; 51 | 52 | } catch ( error ) { 53 | 54 | return {}; 55 | 56 | } 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /.github/workflows/public.yml: -------------------------------------------------------------------------------- 1 | name: NPM Package Publish 2 | on: 3 | release: 4 | types: [created] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: '16.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - run: npm ci 16 | - run: npm run build 17 | - run: | 18 | npm version ${{ github.event.release.tag_name }} --no-git-tag-version --allow-same-version && \ 19 | npm publish --access public 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | slackNotification: 23 | name: Slack Notification 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Slack Notification 28 | uses: rtCamp/action-slack-notify@v2 29 | env: 30 | SLACK_CHANNEL: frontend 31 | SLACK_COLOR: ${{ job.status }} 32 | SLACK_ICON: https://raw.githubusercontent.com/birdwingo/react-native-instagram-stories/main/src/assets/images/logo.png 33 | SLACK_MESSAGE: Publish Release ${{ github.event.release.tag_name }} ${{ job.status == 'success' && 'has been successful' || 'has been failed' }} 34 | SLACK_TITLE: 'Instagram stories publish release :rocket:' 35 | SLACK_USERNAME: NPM 36 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Github Release 2 | on: 3 | pull_request: 4 | types: [closed] 5 | branch: main 6 | jobs: 7 | release: 8 | if: github.event.pull_request.merged == true 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | token: ${{ secrets.AUTH_TOKEN }} 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: '16.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - run: npm ci 19 | - name: Set up git user for release 20 | run: | 21 | git config --global user.email "actions@github.com" 22 | git config --global user.name "GitHub Actions" 23 | - run: npm run release 24 | - name: Push changes 25 | run: git push --follow-tags origin main 26 | - run: npm run build 27 | - run: npm run test 28 | - name: Get version from package-lock.json 29 | id: get_version 30 | run: echo "::set-output name=version::$(node -p "require('./package-lock.json').version")" 31 | - name: Get changelog 32 | id: get_changelog 33 | run: | 34 | CHANGELOG=$(awk '/^### \[[0-9]+\.[0-9]+\.[0-9]+\]/{if (version!="") {exit}; version=$2} version!="" {print}' CHANGELOG.md) 35 | echo "::set-output name=changelog::${CHANGELOG}" 36 | - name: Create Release 37 | uses: actions/create-release@master 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.AUTH_TOKEN }} 40 | with: 41 | tag_name: "v${{ steps.get_version.outputs.version }}" 42 | release_name: "v${{ steps.get_version.outputs.version }}" -------------------------------------------------------------------------------- /src/components/Image/video.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FC, memo, useRef, useState, 3 | } from 'react'; 4 | import { LayoutChangeEvent } from 'react-native'; 5 | import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; 6 | import { StoryVideoProps } from '../../core/dto/componentsDTO'; 7 | import { WIDTH } from '../../core/constants'; 8 | 9 | const StoryVideo: FC = ( { 10 | source, paused, isActive, onLoad, onLayout, ...props 11 | } ) => { 12 | 13 | try { 14 | 15 | // eslint-disable-next-line global-require 16 | const Video = require( 'react-native-video' ).default; 17 | 18 | const ref = useRef( null ); 19 | 20 | const [ pausedValue, setPausedValue ] = useState( paused.value ); 21 | 22 | const start = () => { 23 | 24 | ref.current?.seek( 0 ); 25 | ref.current?.resume?.(); 26 | 27 | }; 28 | 29 | useAnimatedReaction( 30 | () => paused.value, 31 | ( res, prev ) => res !== prev && runOnJS( setPausedValue )( res ), 32 | [ paused.value ], 33 | ); 34 | 35 | useAnimatedReaction( 36 | () => isActive.value, 37 | ( res ) => res && runOnJS( start )(), 38 | [ isActive.value ], 39 | ); 40 | 41 | return ( 42 |