A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add likes support to feed and SongCover

Update feed API types to include trackUri, likesCount and liked Show
like button and counts in InteractionBar and expose onLike handler Use
useLike in SongCover to call like/unlike and pass uri/liked/likesCount
Forward like state from Feed to SongCover

authored by

Tsiry Sandratraina and committed by tangled.org 4bcc5391 fcab9520

+49 -6
+3
apps/web/src/api/feed.ts
··· 79 79 uri: string; 80 80 albumUri: string; 81 81 artistUri: string; 82 + trackUri: string; 82 83 xataVersion: number; 83 84 cover: string; 84 85 date: string; ··· 86 87 userDisplayName: string; 87 88 userAvatar: string; 88 89 tags: string[]; 90 + likesCount: number; 91 + liked: boolean; 89 92 id: string; 90 93 }; 91 94 }[];
+21 -4
apps/web/src/components/SongCover/InteractionBar/InteractionBar.tsx
··· 1 1 import HeartOutline from "../../Icons/HeartOutline"; 2 2 import HeartFilled from "../../Icons/Heart"; 3 3 4 - function InteractionBar() { 4 + export interface InteractionBarProps { 5 + likesCount: number; 6 + liked: boolean; 7 + onLike: () => void; 8 + } 9 + 10 + function InteractionBar({ likesCount, liked, onLike }: InteractionBarProps) { 5 11 return ( 6 12 <div className="absolute bottom-[-1px] left-0 h-[100px] w-full bg-[linear-gradient(rgba(22,24,35,0)_2.92%,rgba(22,24,35,0.5)_98.99%)] flex justify-start items-end p-[10px] rounded-b-[8px]"> 7 13 <div className="h-[40px] w-full flex items-center"> 8 - <span className="cursor-pointer" onClick={(e) => e.preventDefault()}> 9 - {true && <HeartOutline color="#fff" />} 10 - {false && <HeartFilled color="#fff" />} 14 + <span 15 + className="cursor-pointer" 16 + onClick={(e) => { 17 + e.preventDefault(); 18 + onLike(); 19 + }} 20 + > 21 + {!liked && <HeartOutline color="#fff" />} 22 + {liked && <HeartFilled color="#fff" />} 11 23 </span> 24 + {likesCount > 0 && ( 25 + <span className="ml-[5px] mt-[-4px] text-sm text-white"> 26 + {likesCount} 27 + </span> 28 + )} 12 29 </div> 13 30 </div> 14 31 );
+22 -2
apps/web/src/components/SongCover/SongCover.tsx
··· 1 1 import { css } from "@emotion/react"; 2 2 import styled from "@emotion/styled"; 3 3 import InteractionBar from "./InteractionBar"; 4 + import useLike from "../../hooks/useLike"; 4 5 5 6 const Cover = styled.img<{ size?: number }>` 6 7 border-radius: 8px; ··· 51 52 52 53 export type SongCoverProps = { 53 54 cover: string; 55 + uri?: string; 54 56 title?: string; 55 57 artist?: string; 56 58 size?: number; 59 + liked?: boolean; 60 + likesCount?: number; 57 61 withLikeButton?: boolean; 58 62 }; 59 63 60 64 function SongCover(props: SongCoverProps) { 61 - const { title, artist, cover, size, withLikeButton } = props; 65 + const { like, unlike } = useLike(); 66 + const { title, artist, cover, size, liked, likesCount, uri, withLikeButton } = 67 + props; 68 + const handleLike = async () => { 69 + if (!uri) return; 70 + if (liked) { 71 + await unlike(uri); 72 + } else { 73 + await like(uri); 74 + } 75 + }; 62 76 return ( 63 77 <CoverWrapper> 64 78 <div className={`relative h-[100%] w-[92%]`}> 65 - {withLikeButton && <InteractionBar />} 79 + {withLikeButton && ( 80 + <InteractionBar 81 + liked={!!liked} 82 + likesCount={likesCount || 0} 83 + onLike={handleLike} 84 + /> 85 + )} 66 86 <Cover src={cover} size={size} /> 67 87 </div> 68 88 <div className="mb-[13px] mt-[10px]">
+3
apps/web/src/pages/home/feed/Feed.tsx
··· 126 126 className="no-underline text-[var(--color-text-primary)]" 127 127 > 128 128 <SongCover 129 + uri={song.trackUri} 129 130 cover={song.cover} 130 131 artist={song.artist} 131 132 title={song.title} 133 + liked={song.liked} 134 + likesCount={song.likesCount} 132 135 withLikeButton 133 136 /> 134 137 </Link>