Skip to content

Commit 0fb942d

Browse files
committed
feat: implement plan-based feature access and limits for group music, covering max members, queue size, and chat, with client-side upgrade dialogs.
1 parent 7faa649 commit 0fb942d

8 files changed

Lines changed: 244 additions & 30 deletions

File tree

client/src/Context/GroupMusicContext.jsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useSocket } from "@/Context/ChatContext"
22
import { useProfile } from "@/Context/Context"
33
import { ensureHttpsForDownloadUrls } from "@/Pages/Music/Common"
4+
import UpgradeDialog from "@/components/UpgradeDialog"
45
import axios from "axios"
56
import _ from "lodash"
67
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
@@ -47,6 +48,11 @@ export function GroupMusicProvider({ children }) {
4748
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false)
4849
const [connectionState, setConnectionState] = useState("disconnected")
4950
const [isRejoining, setIsRejoining] = useState(false)
51+
const [upgradeDialog, setUpgradeDialog] = useState({
52+
open: false,
53+
feature: "default",
54+
message: "",
55+
})
5056

5157
// Refs
5258
const syncIntervalRef = useRef(null)
@@ -251,6 +257,16 @@ export function GroupMusicProvider({ children }) {
251257
(song) => {
252258
if (!currentGroup?.id || !user) return
253259

260+
const maxQueueSize = currentGroup?.settings?.maxQueueSize || 3
261+
if (queue.length >= maxQueueSize) {
262+
setUpgradeDialog({
263+
open: true,
264+
feature: "queueLimit",
265+
message: `Free plan allows only ${maxQueueSize} songs in queue. Upgrade to PRO for up to 50.`,
266+
})
267+
return
268+
}
269+
254270
const securedSong = ensureHttpsForDownloadUrls(song)
255271

256272
socket.emit("add-to-queue", {
@@ -268,7 +284,7 @@ export function GroupMusicProvider({ children }) {
268284
setSearchResults([])
269285
toast.success("Added to queue")
270286
},
271-
[currentGroup?.id, user, socket],
287+
[currentGroup?.id, currentGroup?.settings?.maxQueueSize, user, socket, queue.length],
272288
)
273289

274290
// Play song now (inserts at current position)
@@ -809,6 +825,22 @@ export function GroupMusicProvider({ children }) {
809825
setMessages((prev) => [...prev, message])
810826
})
811827

828+
socket.on("group-full", ({ maxMembers, message }) => {
829+
setUpgradeDialog({
830+
open: true,
831+
feature: "groupMembers",
832+
message: message || `Group is full (${maxMembers} members max)`,
833+
})
834+
})
835+
836+
socket.on("feature-locked", ({ feature, message }) => {
837+
setUpgradeDialog({
838+
open: true,
839+
feature: feature || "default",
840+
message: message || "This feature requires PRO plan",
841+
})
842+
})
843+
812844
return () => {
813845
socket.off("sync-state")
814846
socket.off("queue-updated")
@@ -824,6 +856,8 @@ export function GroupMusicProvider({ children }) {
824856
socket.off("member-left")
825857
socket.off("group-disbanded")
826858
socket.off("new-message")
859+
socket.off("group-full")
860+
socket.off("feature-locked")
827861
}
828862
}, [
829863
socket,
@@ -920,6 +954,12 @@ export function GroupMusicProvider({ children }) {
920954
<GroupMusicContext.Provider value={contextValue}>
921955
{children}
922956
<audio ref={audioRef} />
957+
<UpgradeDialog
958+
open={upgradeDialog.open}
959+
onOpenChange={(open) => setUpgradeDialog((prev) => ({ ...prev, open }))}
960+
feature={upgradeDialog.feature}
961+
customMessage={upgradeDialog.message}
962+
/>
923963
</GroupMusicContext.Provider>
924964
)
925965
}

client/src/Pages/Music/GroupMusic.jsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, useCallback, useMemo, memo } from "react"
22
import { Card, CardContent, CardHeader } from "@/components/ui/card"
33
import { useProfile } from "@/Context/Context"
44
import { useGroupMusic } from "@/Context/GroupMusicContext"
5+
import { useFeatureAccess } from "@/hooks/useFeatureAccess"
56
import { AnimatePresence, motion } from "framer-motion"
67
import { cn } from "@/lib/utils"
78

@@ -21,6 +22,7 @@ import "./music.css"
2122

2223
const GroupMusic = () => {
2324
const { user } = useProfile()
25+
const { canChat, maxGroupMembers } = useFeatureAccess()
2426
const {
2527
currentGroup,
2628
isPlaying,
@@ -49,7 +51,6 @@ const GroupMusic = () => {
4951
joinGroup,
5052
leaveGroup,
5153
sendMessage,
52-
// Queue functions
5354
playNow,
5455
addToQueue,
5556
skipSong,
@@ -62,13 +63,11 @@ const GroupMusic = () => {
6263

6364
const [isQrCodeOpen, setQrCodeOpen] = useState(false)
6465

65-
// Active queue count = current song (if any) + upcoming songs
6666
const activeQueueCount = useMemo(
6767
() => (currentQueueItem ? 1 : 0) + upcomingQueue.length,
6868
[currentQueueItem, upcomingQueue.length],
6969
)
7070

71-
// Memoize callbacks
7271
const handleSearchChange = useCallback(
7372
(query) => {
7473
setSearchQuery(query)
@@ -85,9 +84,10 @@ const GroupMusic = () => {
8584
const handleCloseGroupModal = useCallback(() => setIsGroupModalOpen(false), [setIsGroupModalOpen])
8685
const handleOpenQueue = useCallback(() => setIsQueueOpen(true), [setIsQueueOpen])
8786

88-
// Memoize user id
8987
const userId = useMemo(() => user?.userid, [user?.userid])
9088

89+
const groupMaxMembers = currentGroup?.maxMembers || maxGroupMembers
90+
9191
return (
9292
<div className="mx-auto max-w-7xl px-2 md:px-4 py-2 md:py-6">
9393
<motion.div
@@ -127,7 +127,6 @@ const GroupMusic = () => {
127127
transition={{ duration: 0.4 }}
128128
className="space-y-3 md:space-y-6"
129129
>
130-
{/* Now Playing */}
131130
<NowPlayingCard
132131
currentSong={currentSong}
133132
isPlaying={isPlaying}
@@ -146,20 +145,21 @@ const GroupMusic = () => {
146145
queueCount={activeQueueCount}
147146
/>
148147

149-
{/* Chat & Members Grid */}
150148
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3 md:gap-6">
151149
<div className="lg:col-span-2">
152150
<GroupChat
153151
messages={messages}
154152
currentUserId={userId}
155153
onSendMessage={sendMessage}
154+
locked={!canChat}
156155
/>
157156
</div>
158157
<div>
159158
<MembersList
160159
members={groupMembers}
161160
currentUserId={userId}
162161
createdBy={currentGroup?.createdBy}
162+
maxMembers={groupMaxMembers}
163163
/>
164164
</div>
165165
</div>
@@ -170,7 +170,6 @@ const GroupMusic = () => {
170170
</Card>
171171
</motion.div>
172172

173-
{/* Modals & Sheets */}
174173
{isGroupModalOpen && (
175174
<GroupModal
176175
isOpen={isGroupModalOpen}
@@ -197,7 +196,6 @@ const GroupMusic = () => {
197196
<QRCodeDialog isOpen={isQrCodeOpen} onClose={handleCloseQrCode} group={currentGroup} />
198197
)}
199198

200-
{/* Queue Sheet */}
201199
<QueueSheet />
202200
</div>
203201
)

client/src/Pages/Music/GroupMusic/GroupChat.jsx

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { memo, useRef, useEffect, useCallback, useMemo } from "react"
1+
import { memo, useRef, useEffect, useCallback, useMemo, useState } from "react"
22
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
33
import { ScrollArea } from "@/components/ui/scroll-area"
44
import { Input } from "@/components/ui/input"
55
import { Button } from "@/components/ui/button"
66
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
7-
import { MessageCircle, Send } from "lucide-react"
7+
import { MessageCircle, Send, Lock, Sparkles } from "lucide-react"
88
import { cn } from "@/lib/utils"
9+
import UpgradeDialog from "@/components/UpgradeDialog"
910

10-
// Activity Message Component (system messages for queue events)
1111
const ActivityMessage = memo(({ msg }) => (
1212
<div className="flex justify-center py-1">
1313
<p className="text-xs text-muted-foreground/70 bg-green-950/30 px-3 py-1 rounded-full">
@@ -16,7 +16,6 @@ const ActivityMessage = memo(({ msg }) => (
1616
</div>
1717
))
1818

19-
// Chat Message Component
2019
const ChatMessage = memo(({ msg, isOwn }) => (
2120
<div className={cn("flex gap-2", isOwn ? "justify-end" : "justify-start")}>
2221
{!isOwn && (
@@ -49,15 +48,39 @@ const ChatMessage = memo(({ msg, isOwn }) => (
4948
</div>
5049
))
5150

52-
// Empty State
5351
const EmptyState = memo(() => (
5452
<div className="h-full flex flex-col items-center justify-center text-center p-4">
5553
<MessageCircle className="h-8 w-8 md:h-12 md:w-12 text-muted-foreground/30 mb-2" />
5654
<p className="text-muted-foreground text-xs md:text-sm">No messages yet</p>
5755
</div>
5856
))
5957

60-
// Messages List with activity message support
58+
const ChatLockedOverlay = memo(({ onUpgrade }) => (
59+
<div
60+
className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-background/80 backdrop-blur-sm rounded-lg cursor-pointer"
61+
onClick={onUpgrade}
62+
>
63+
<div className="flex flex-col items-center gap-3 p-6 text-center">
64+
<div className="p-3 rounded-full bg-primary/10">
65+
<Lock className="h-6 w-6 text-primary" />
66+
</div>
67+
<div>
68+
<p className="font-semibold text-sm">Group Chat is a PRO feature</p>
69+
<p className="text-xs text-muted-foreground mt-1">
70+
Tap to upgrade and chat with your group
71+
</p>
72+
</div>
73+
<Button
74+
size="sm"
75+
className="gap-1.5 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white"
76+
>
77+
<Sparkles className="h-3.5 w-3.5" />
78+
Upgrade to PRO
79+
</Button>
80+
</div>
81+
</div>
82+
))
83+
6184
const MessagesList = memo(({ messages, currentUserId }) => {
6285
if (messages.length === 0) {
6386
return <EmptyState />
@@ -76,12 +99,11 @@ const MessagesList = memo(({ messages, currentUserId }) => {
7699
)
77100
})
78101

79-
// Main GroupChat Component
80-
const GroupChat = ({ messages, currentUserId, onSendMessage }) => {
102+
const GroupChat = ({ messages, currentUserId, onSendMessage, locked = false }) => {
81103
const inputRef = useRef(null)
82104
const scrollRef = useRef(null)
105+
const [showUpgrade, setShowUpgrade] = useState(false)
83106

84-
// Auto-scroll to bottom on new messages
85107
useEffect(() => {
86108
if (scrollRef.current) {
87109
const scrollContainer = scrollRef.current.querySelector("[data-radix-scroll-area-viewport]")
@@ -92,12 +114,13 @@ const GroupChat = ({ messages, currentUserId, onSendMessage }) => {
92114
}, [messages.length])
93115

94116
const handleSend = useCallback(() => {
117+
if (locked) return
95118
const message = inputRef.current?.value?.trim()
96119
if (message) {
97120
onSendMessage(message)
98121
inputRef.current.value = ""
99122
}
100-
}, [onSendMessage])
123+
}, [onSendMessage, locked])
101124

102125
const handleKeyPress = useCallback(
103126
(e) => {
@@ -109,20 +132,22 @@ const GroupChat = ({ messages, currentUserId, onSendMessage }) => {
109132
[handleSend],
110133
)
111134

112-
// Memoize message count (exclude activity messages from count)
113135
const userMessageCount = useMemo(
114136
() => messages.filter((m) => m.type !== "activity").length,
115137
[messages],
116138
)
117139

118140
return (
119-
<Card className="h-full flex flex-col border-border/50 shadow-lg overflow-hidden">
141+
<Card className="h-full flex flex-col border-border/50 shadow-lg overflow-hidden relative">
142+
{locked && <ChatLockedOverlay onUpgrade={() => setShowUpgrade(true)} />}
143+
120144
<CardHeader className="py-2 md:pb-3 px-3 md:px-6 border-b border-border/50">
121145
<CardTitle className="text-sm md:text-lg flex items-center gap-2">
122146
<div className="p-1 md:p-1.5 rounded-full bg-primary/10">
123147
<MessageCircle className="h-3 w-3 md:h-4 md:w-4 text-primary" />
124148
</div>
125149
Chat
150+
{locked && <Lock className="h-3 w-3 text-muted-foreground" />}
126151
{userMessageCount > 0 && (
127152
<span className="text-xs font-normal text-muted-foreground">({userMessageCount})</span>
128153
)}
@@ -138,20 +163,24 @@ const GroupChat = ({ messages, currentUserId, onSendMessage }) => {
138163
<div className="flex gap-2">
139164
<Input
140165
ref={inputRef}
141-
placeholder="Type a message..."
166+
placeholder={locked ? "PRO feature" : "Type a message..."}
142167
onKeyPress={handleKeyPress}
168+
disabled={locked}
143169
className="rounded-full bg-background border-border/50 h-9 md:h-10 text-sm"
144170
/>
145171
<Button
146172
onClick={handleSend}
147173
size="icon"
174+
disabled={locked}
148175
className="rounded-full shrink-0 h-9 w-9 md:h-10 md:w-10"
149176
>
150177
<Send className="h-3.5 w-3.5 md:h-4 md:w-4" />
151178
</Button>
152179
</div>
153180
</div>
154181
</CardContent>
182+
183+
<UpgradeDialog open={showUpgrade} onOpenChange={setShowUpgrade} feature="realtimeChat" />
155184
</Card>
156185
)
157186
}

client/src/Pages/Music/GroupMusic/MembersList.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ const MemberItem = memo(({ member, isCreator, isCurrentUser }) => (
6161
))
6262

6363
// Main MembersList Component
64-
const MembersList = ({ members, currentUserId, createdBy }) => {
65-
// Memoize member count
64+
const MembersList = ({ members, currentUserId, createdBy, maxMembers }) => {
6665
const memberCount = useMemo(() => members.length, [members.length])
6766

6867
return (
@@ -77,6 +76,7 @@ const MembersList = ({ members, currentUserId, createdBy }) => {
7776
</div>
7877
<Badge variant="secondary" className="font-normal text-xs">
7978
{memberCount}
79+
{maxMembers ? `/${maxMembers}` : ""}
8080
</Badge>
8181
</CardTitle>
8282
</CardHeader>

0 commit comments

Comments
 (0)