Skip to content

Commit

Permalink
Add ability to view friends' liked songs (#171)
Browse files Browse the repository at this point in the history
* Remove genre requirement for hydrating tracks

* Extend song view to support viewing friends' songs

* Add ability to view friends' liked songs in feed view
  • Loading branch information
Advayp authored Feb 23, 2025
1 parent 7906ca8 commit dbaa6f5
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 53 deletions.
43 changes: 35 additions & 8 deletions frontend/app/main/(tabs)/feed.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,46 @@
import { getFeed } from '@/services/friends';
import SongView from '@/components/SongView';
import { useFeed } from '@/hooks/useFeed';
import { useSpotify } from '@/hooks/useSpotify';
import { useEffect } from 'react';
import { SafeAreaView, StyleSheet, Text, View } from 'react-native';
import {
Dimensions,
FlatList,
SafeAreaView,
StyleSheet,
View,
} from 'react-native';

const Feed = () => {
const { songs } = useFeed();
const { accessToken } = useSpotify();

useEffect(() => {
const loadFeed = async () => {
getFeed();
};
loadFeed();
});
console.log(accessToken);
}, [accessToken]);

return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Friends</Text>
{accessToken && (
<FlatList
data={songs}
renderItem={({ item }) => (
<SongView
accessToken={accessToken!}
songId={item.trackID}
genre={item.genre}
username={item.username}
/>
)}
keyExtractor={(item, index) => index.toString()}
snapToAlignment="start"
decelerationRate="fast"
snapToInterval={Dimensions.get('window').height}
viewabilityConfig={{
itemVisiblePercentThreshold: 25,
}}
/>
)}
</View>
</SafeAreaView>
);
Expand Down
96 changes: 61 additions & 35 deletions frontend/components/SongView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useTrack } from '@/hooks/useTrack';
import { shortenString } from '@/utils/misc';
import { MaterialIcons, FontAwesome } from '@expo/vector-icons';
import React, { useState } from 'react';
import { MaterialIcons, FontAwesome, createIconSet } from '@expo/vector-icons';
import React, { useEffect, useState } from 'react';
import {
View,
Text,
Expand All @@ -11,26 +11,50 @@ import {
TouchableOpacity,
} from 'react-native';
import { likeTrack, dislikeTrack } from '@/services/songs';
import { Genre } from '@/types';
import { Genre, SpotifyTrack } from '@/types';
import { hydrateTrackInfo } from '@/services/spotify';

type SongViewProps = {
songId: string;
accessToken: string;
genre: Genre;
username?: string;
};

const SongView: React.FC<SongViewProps> = ({ songId, accessToken, genre }) => {
const { track, isLoading } = useTrack(songId, genre, accessToken);

const SongView: React.FC<SongViewProps> = ({
songId,
accessToken,
username,
}) => {
const [status, setStatus] = useState<'liked' | 'disliked' | null>(null);
const { track, isLoading } = useTrack(songId, accessToken);

if (isLoading) {
return <Text>Loading...</Text>;
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontSize: 20, fontWeight: 'bold', color: 'white' }}>
Loading...
</Text>
</View>
);
}

return (
<View style={styles.container}>
<View style={styles.content}>
{username && (
<Text
style={{
color: 'white',
fontSize: 30,
fontWeight: 'bold',
marginBottom: 20,
marginTop: -50,
}}
>
{username} liked...
</Text>
)}
{track?.album?.images?.[0]?.url && (
<View style={styles.imageContainer}>
<Image
Expand All @@ -45,34 +69,36 @@ const SongView: React.FC<SongViewProps> = ({ songId, accessToken, genre }) => {
{artist.name}
</Text>
))}
<View style={styles.iconContainer}>
<TouchableOpacity
style={styles.iconButton}
onPress={() => {
setStatus(status === 'disliked' ? null : 'disliked');
dislikeTrack(songId);
}}
>
<FontAwesome
name={status === 'disliked' ? 'thumbs-down' : 'thumbs-o-down'}
size={32}
color="#a568ff"
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.iconButton}
onPress={() => {
setStatus(status === 'liked' ? null : 'liked');
likeTrack(songId);
}}
>
<MaterialIcons
name={status === 'liked' ? 'favorite' : 'favorite-border'}
size={32}
color="#a568ff"
/>
</TouchableOpacity>
</View>
{!username && (
<View style={styles.iconContainer}>
<TouchableOpacity
style={styles.iconButton}
onPress={() => {
setStatus(status === 'disliked' ? null : 'disliked');
dislikeTrack(songId);
}}
>
<FontAwesome
name={status === 'disliked' ? 'thumbs-down' : 'thumbs-o-down'}
size={32}
color="#a568ff"
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.iconButton}
onPress={() => {
setStatus(status === 'liked' ? null : 'liked');
likeTrack(songId);
}}
>
<MaterialIcons
name={status === 'liked' ? 'favorite' : 'favorite-border'}
size={32}
color="#a568ff"
/>
</TouchableOpacity>
</View>
)}
</View>
</View>
);
Expand Down
57 changes: 57 additions & 0 deletions frontend/hooks/useFeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { getFeed } from '@/services/friends';
import { Genre, Playlist, Song } from '@/types';
import { useEffect, useState } from 'react';

export const useFeed = () => {
const [songs, setSongs] = useState<Array<Song & { username: string }>>([]);
const [playlists, setPlaylists] = useState<
Array<Playlist & { username: string }>
>([]);
const [genres, setGenres] = useState<Array<Genre & { username: string }>>([]);

const fetchFeed = async () => {
console.log('fetching feed');
let newSongs: Array<Song & { username: string }> = [];
let newPlaylists: Array<Playlist & { username: string }> = [];
let newGenres: Array<Genre & { username: string }> = [];

const feed = await getFeed();
feed.forEach((element) => {
newSongs = [
...newSongs,
...element.likedSongs.map((song) => ({
...song,
username: element.username,
})),
];
newPlaylists = [
...newPlaylists,
...element.playlists.map((playlist) => ({
...playlist,
username: element.username,
})),
];
newGenres = [
...newGenres,
...element.preferences.map((preference) => ({
...preference,
username: element.username,
})),
];
});

console.log(newSongs);
console.log(newPlaylists);
console.log(newGenres);

setSongs(newSongs);
setPlaylists(newPlaylists);
setGenres(newGenres);
};

useEffect(() => {
fetchFeed();
}, []);

return { songs, playlists, genres, fetchFeed };
};
4 changes: 2 additions & 2 deletions frontend/hooks/useTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { Genre, SpotifyTrack } from '@/types';
import { useEffect, useState } from 'react';
import { hydrateTrackInfo } from '@/services/spotify';

export const useTrack = (songId: string, genre: Genre, accessToken: string) => {
export const useTrack = (songId: string, accessToken: string) => {
const [track, setTrack] = useState<SpotifyTrack | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const fetchTrack = async () => {
const track = await hydrateTrackInfo(songId, genre, accessToken);
const track = await hydrateTrackInfo(songId, accessToken);
setTrack(track);
setIsLoading(false);
};
Expand Down
10 changes: 3 additions & 7 deletions frontend/services/spotify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { SpotifyTrack, SpotifyArtist, SpotifyImage, Genre } from '@/types';

export const hydrateTrackInfo = async (
songId: string,
genre: Genre,
accessToken: string
): Promise<SpotifyTrack> => {
const response = await fetch(`https://api.spotify.com/v1/tracks/${songId}`, {
Expand All @@ -13,18 +12,15 @@ export const hydrateTrackInfo = async (

const data = await response.json();

const res = {
return {
name: data.name,
id: data.id,
artists: data.artists as SpotifyArtist[],
artists: data.artists,
album: {
name: data.album.name,
id: data.album.id,
images: data.album.images as SpotifyImage[],
images: data.album.images,
},
duration_ms: data.duration_ms,
genre: genre.value,
};

return res;
};
1 change: 0 additions & 1 deletion frontend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export interface SpotifyTrack {
artists: SpotifyArtist[];
album: SpotifyAlbum;
duration_ms: number;
genre: string;
}

export interface Playlist {
Expand Down

0 comments on commit dbaa6f5

Please sign in to comment.