Skip to content

Commit

Permalink
Feat/short videos module UI (#176)
Browse files Browse the repository at this point in the history
* feat: add short videos module and nostr video component

* feat: add ShortVideo module to router

* add short videos actions

* add inner video styles

* replace mock videos data with hook
  • Loading branch information
lindsaymoralesb authored Oct 14, 2024
1 parent 2c9d2a2 commit 0b31fc3
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 5 deletions.
1 change: 1 addition & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"expo": "~51.0.28",
"expo-application": "^5.9.1",
"expo-auth-session": "^5.5.2",
"expo-av": "~14.0.7",
"expo-camera": "^15.0.16",
"expo-clipboard": "~6.0.3",
"expo-constants": "^16.0.2",
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/app/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import GroupChatGroupRequest from '../modules/Group/memberAction/ViewRequest';
import GroupChat from '../modules/Group/message/GroupMessage';
import AuthSidebar from '../modules/Layout/auth-sidebar';
import Sidebar from '../modules/Layout/sidebar';
import RightSidebar from '../modules/Layout/RightSideBar';
import ShortVideosModule from '../modules/ShortVideos';
// import RightSidebar from '../modules/Layout/RightSideBar';

// Components
Expand Down Expand Up @@ -305,6 +307,8 @@ const MainNavigator: React.FC = () => {
<MainStack.Screen name="ImportKeys" component={ImportKeys} />

<MainStack.Screen name="Wallet" component={Wallet} />

<MainStack.Screen name="ShortVideos" component={ShortVideosModule} />
<MainStack.Screen name="Onboarding" component={Onboarding} />
</MainStack.Navigator>
);
Expand Down
80 changes: 80 additions & 0 deletions apps/mobile/src/components/NostrVideo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {NostrEvent} from '@nostr-dev-kit/ndk';
import {ResizeMode, Video} from 'expo-av';
import React, {useEffect, useState} from 'react';
import {Pressable, TouchableOpacity, View} from 'react-native';

import {BookmarkIcon, LikeIcon, RepostIcon} from '../../assets/icons';
import {useStyles} from '../../hooks';
import stylesheet from './styles';

const NostrVideo = ({item, shouldPlay}: {shouldPlay: boolean; item: NostrEvent}) => {
const video = React.useRef<Video | null>(null);
const [status, setStatus] = useState<any>(null);
const styles = useStyles(stylesheet);

useEffect(() => {
if (!video.current) return;

if (shouldPlay) {
video.current.playAsync();
} else {
video.current.pauseAsync();
video.current.setPositionAsync(0);
}
}, [shouldPlay]);

const extractVideoURL = (event: NostrEvent) => {
return event?.tags?.find((tag) => tag?.[0] === 'url')?.[1] || '';
};

const handleLike = () => {
console.log('like');
//todo: integrate hook
};

const handleRepost = () => {
console.log('like');
//todo: integrate hook
};

const handleBookmark = () => {
console.log('like');
//todo: integrate hook
};

return (
<>
<Pressable
onPress={() =>
status.isPlaying ? video.current?.pauseAsync() : video.current?.playAsync()
}
>
<View style={styles.videoContainer}>
<Video
ref={video}
source={{uri: extractVideoURL(item)}}
style={styles.video}
isLooping
resizeMode={ResizeMode.COVER}
useNativeControls={false}
onPlaybackStatusUpdate={(status) => setStatus(() => status)}
videoStyle={styles.innerVideo}
/>
</View>
</Pressable>
<View style={styles.actionsContainer}>
<TouchableOpacity onPress={handleLike}>
<LikeIcon width={20} height={20} color="white" />
</TouchableOpacity>
<TouchableOpacity onPress={handleRepost}>
<RepostIcon width={20} height={20} color="white" />
</TouchableOpacity>
<TouchableOpacity style={{width: 15}} onPress={handleBookmark}>
<BookmarkIcon width={15} height={20} color="white" />
</TouchableOpacity>
</View>
</>
);
};

export default NostrVideo;
26 changes: 26 additions & 0 deletions apps/mobile/src/components/NostrVideo/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {Dimensions} from 'react-native';

import {ThemedStyleSheet} from '../../styles';

export default ThemedStyleSheet(() => ({
videoContainer: {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height - 60,
},
video: {
width: '100%',
height: '100%',
},
innerVideo: {
height: Dimensions.get('window').height - 60,
},
actionsContainer: {
position: 'absolute',
right: 20,
bottom: 40,
display: 'flex',
flexDirection: 'column',
gap: 20,
alignItems: 'center',
},
}));
60 changes: 60 additions & 0 deletions apps/mobile/src/modules/ShortVideos/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {NostrEvent} from '@nostr-dev-kit/ndk';
import {useGetVideos} from 'afk_nostr_sdk';
import React, {useRef, useState} from 'react';
import {FlatList, Text, View} from 'react-native';

import {InfoIcon} from '../../assets/icons';
import NostrVideo from '../../components/NostrVideo';
import {useStyles, useTheme} from '../../hooks';
import {mockEvents} from '../../utils/dummyData';
import stylesheet from './styles';

const ShortVideosModule = () => {
const styles = useStyles(stylesheet);
const {theme} = useTheme();
const [currentViewableItemIndex, setCurrentViewableItemIndex] = useState(0);
const viewabilityConfig = {viewAreaCoveragePercentThreshold: 50};
const videos = useGetVideos();
const [videosEvents, setVideosEvents] = useState<NostrEvent[]>(
videos?.data?.pages?.flat() as NostrEvent[],
);

const fetchNostrEvents = async () => {
// This mock should be replaced with actual implementation (hook integration to get videos)
setVideosEvents(mockEvents);
};

const onViewableItemsChanged = ({viewableItems}: any) => {
if (viewableItems.length > 0) {
setCurrentViewableItemIndex(viewableItems[0].index ?? 0);
}
};

const viewabilityConfigCallbackPairs = useRef([{viewabilityConfig, onViewableItemsChanged}]);

return (
<View style={styles.container}>
{videosEvents.length > 0 ? (
<FlatList
style={styles.list}
data={videosEvents}
renderItem={({item, index}) => (
<NostrVideo item={item} shouldPlay={index === currentViewableItemIndex} />
)}
keyExtractor={(item, index) => item.content + index}
pagingEnabled
horizontal={false}
showsVerticalScrollIndicator={false}
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
/>
) : (
<View style={styles.noDataContainer}>
<InfoIcon width={30} height={30} color={theme.colors.primary} />
<Text style={styles.noDataText}>No videos uploaded yet.</Text>
</View>
)}
</View>
);
};

export default ShortVideosModule;
24 changes: 24 additions & 0 deletions apps/mobile/src/modules/ShortVideos/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {ThemedStyleSheet} from '../../styles';

export default ThemedStyleSheet((theme) => ({
container: {
flex: 1,
height: '100%',
},
list: {
height: '100%',
},
noDataContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 20,
marginTop: 20,
},
noDataText: {
color: theme.colors.text,
textAlign: 'center',
fontSize: 14,
fontWeight: 'bold',
},
}));
5 changes: 1 addition & 4 deletions apps/mobile/src/types/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,9 @@ export type MainStackParams = {
Wallet: undefined;
Portfolio: undefined;
Ramp: undefined;
Onboarding:undefined;
Onboarding: undefined;
};


export type HomeBottomStackParams = {
Feed: undefined;
UserProfile: {publicKey: string};
Expand Down Expand Up @@ -323,8 +322,6 @@ export type OnboardingWalletScreen = CompositeScreenProps<

export type DrawerStackNavigationProps = DrawerNavigationProp<MainStackParams>;



/** TODO delete */
export type DegensAppStackParams = {
// Home: NavigatorScreenParams<HomeBottomStackParams>;
Expand Down
Loading

0 comments on commit 0b31fc3

Please sign in to comment.