From 388d7eff36da01b9a23c18e5dc605cd882daacbd Mon Sep 17 00:00:00 2001 From: sahilkashyap64 Date: Mon, 11 May 2026 23:31:48 +0530 Subject: [PATCH] feat: switch match group messaging to SMS composer --- .../components/MatchDetailsModal.jsx | 207 ++++++------------ src/play-dates/utils/participants.js | 16 +- 2 files changed, 85 insertions(+), 138 deletions(-) diff --git a/src/play-dates/components/MatchDetailsModal.jsx b/src/play-dates/components/MatchDetailsModal.jsx index 550d178a..d7cfc108 100644 --- a/src/play-dates/components/MatchDetailsModal.jsx +++ b/src/play-dates/components/MatchDetailsModal.jsx @@ -27,17 +27,14 @@ import { import PlayerAvatar from "./PlayerAvatar"; import { cancelMatch, - createMatchMessage, deleteMatchNotification, getShareLink, joinMatch, leaveMatch, - listMatchMessages, listMatchNotifications, notifyMatchPlayers, removeParticipant, searchPlayers, - sendMatchPlayerDirectMessage, updateMatch, } from "../services/matches"; import { rejectInvite } from "../services/invites"; @@ -553,6 +550,45 @@ const getInviteStatus = (invite) => { return ""; }; +const openSmsComposer = (recipients, onToast) => { + if (!Array.isArray(recipients) || recipients.length === 0) { + return; + } + + try { + const ua = + typeof navigator !== "undefined" && navigator.userAgent + ? navigator.userAgent + : ""; + const isAndroid = /Android/i.test(ua); + const isAppleMobile = /(iPad|iPhone|iPod)/i.test(ua); + + let url = "sms:"; + if (recipients.length > 0) { + if (isAndroid) { + const path = recipients.map((value) => encodeURIComponent(value)).join(";"); + const addresses = encodeURIComponent(recipients.join(";")); + url = `smsto:${path}?addresses=${addresses}`; + } else if (isAppleMobile) { + const addresses = encodeURIComponent(recipients.join(",")); + url = `sms:&addresses=${addresses}`; + } else { + const path = recipients.map((value) => encodeURIComponent(value)).join(","); + url = `sms:${path}`; + } + } + + onToast?.(isAppleMobile ? "Opening Messages..." : "Opening messages..."); + + if (typeof window !== "undefined") { + window.location.href = url; + } + } catch (error) { + console.error(error); + onToast?.("We couldn't open messages", "error"); + } +}; + const ACCEPTED_INVITE_STATUSES = new Set([ "accepted", "confirmed", @@ -808,10 +844,6 @@ const MatchDetailsModal = ({ const [notifyResults, setNotifyResults] = useState([]); const [notifySelected, setNotifySelected] = useState([]); const [notifySending, setNotifySending] = useState(false); - const [messageText, setMessageText] = useState(""); - const [messages, setMessages] = useState([]); - const [messagesLoading, setMessagesLoading] = useState(false); - const [messageSending, setMessageSending] = useState(false); const googleApiKey = import.meta.env.VITE_GOOGLE_API_KEY; const shareCopyTimeoutRef = useRef(null); @@ -1191,31 +1223,10 @@ const MatchDetailsModal = ({ } }, [isHost, isOpenMatch, matchId]); - const loadMatchMessages = useCallback(async () => { - if (!matchId || !isJoined) { - setMessages([]); - return; - } - try { - setMessagesLoading(true); - const data = await listMatchMessages(matchId); - setMessages(Array.isArray(data?.messages) ? data.messages : []); - } catch (error) { - console.error("Failed to load match messages", error); - setMessages([]); - } finally { - setMessagesLoading(false); - } - }, [isJoined, matchId]); - useEffect(() => { loadMatchNotifications(); }, [loadMatchNotifications]); - useEffect(() => { - loadMatchMessages(); - }, [loadMatchMessages]); - useEffect(() => { if (!isOpen || !isHost || !isOpenMatch) return undefined; const query = notifySearch.trim(); @@ -1328,58 +1339,6 @@ const MatchDetailsModal = ({ [matchId, onToast], ); - const handleSendGroupMessage = useCallback(async () => { - const body = messageText.trim(); - if (!matchId || !body) return; - try { - setMessageSending(true); - const response = await createMatchMessage(matchId, { body }); - setMessageText(""); - if (response?.message) { - setMessages((prev) => [...prev, response.message]); - } else { - await loadMatchMessages(); - } - onToast?.("Message posted."); - } catch (error) { - console.error(error); - onToast?.( - error?.response?.data?.message || - error?.response?.data?.error || - error?.message || - "Failed to send message", - "error", - ); - } finally { - setMessageSending(false); - } - }, [loadMatchMessages, matchId, messageText, onToast]); - - const handleSendDm = useCallback( - async (player) => { - if (!matchId || !player?.playerId) return; - const body = window.prompt(`Message ${player.name}`); - if (!body || !body.trim()) return; - try { - await sendMatchPlayerDirectMessage(matchId, player.playerId, { - body: body.trim(), - }); - onToast?.(`Message sent to ${player.name}.`); - loadMatchMessages(); - } catch (error) { - console.error(error); - onToast?.( - error?.response?.data?.message || - error?.response?.data?.error || - error?.message || - "Failed to send direct message", - "error", - ); - } - }, - [loadMatchMessages, matchId, onToast], - ); - useEffect(() => { if ((!isHost || isArchived || isCancelled) && isEditing) { setIsEditing(false); @@ -1962,10 +1921,31 @@ const MatchDetailsModal = ({ return Math.max(remainingSpots, 0); }, [remainingSpots]); - const canMessageParticipants = committedParticipants.length > 1; + const participantPhoneRecipients = useMemo(() => { + if (!isHost) return []; + const seen = new Set(); + return committedParticipants.reduce((numbers, participant) => { + if (!participant) return numbers; + if (match?.host_id && participantMatchesMember(participant, match.host_id)) { + return numbers; + } + const phoneRaw = getParticipantPhone(participant); + const normalized = normalizePhoneValue(phoneRaw); + if (!normalized || seen.has(normalized)) { + return numbers; + } + seen.add(normalized); + numbers.push(normalized); + return numbers; + }, []); + }, [committedParticipants, isHost, match?.host_id]); + + const canMessageParticipants = participantPhoneRecipients.length > 0; const messageParticipantsDescription = canMessageParticipants - ? "Post a group update to confirmed players." - : "Add another confirmed player to enable group messages."; + ? participantPhoneRecipients.length === 1 + ? "Start a group text with the confirmed player." + : "Start a group text with your confirmed players." + : "Add player phone numbers to enable group texts."; const pendingInvitesList = useMemo(() => { if (pendingInvitees.length === 0) return []; @@ -2895,29 +2875,22 @@ const MatchDetailsModal = ({

- Message group + Message participants

- Post a match-scoped update for everyone confirmed on this roster. + {messageParticipantsDescription}

-
-
- setMessageText(event.target.value)} - placeholder={canMessageParticipants ? "Write a group update..." : messageParticipantsDescription} - disabled={!canMessageParticipants || messageSending} - className="min-h-10 flex-1 rounded-xl border border-emerald-100 bg-white px-3 py-2 text-sm font-semibold text-gray-700 outline-none focus:border-emerald-300 focus:ring-2 focus:ring-emerald-100 disabled:opacity-60" - />
@@ -3035,46 +3008,6 @@ const MatchDetailsModal = ({ )} - {isJoined && ( -
-
-
- -

Match messages

-
- -
- {messagesLoading ? ( -

Loading messages...

- ) : messages.length === 0 ? ( -

No messages yet.

- ) : ( -
- {messages.map((message) => { - const sender = message.sender || {}; - return ( -
-

- {sender.full_name || sender.name || `Player ${message.sender_id}`} - {message.recipient_id ? " ยท Direct message" : ""} -

-

- {message.body} -

-
- ); - })} -
- )} -
- )} - {isHost && matchPrivacy === "private" && pendingInvitesList.length > 0 && ( renderPendingInvites() )} diff --git a/src/play-dates/utils/participants.js b/src/play-dates/utils/participants.js index e636ba28..89ce5028 100644 --- a/src/play-dates/utils/participants.js +++ b/src/play-dates/utils/participants.js @@ -315,7 +315,17 @@ const isInactiveStatus = (value) => { ].includes(normalized); }; -const CONFIRMED_STATUS_TOKENS = new Set(["confirm", "confirmed"]); +const CONFIRMED_STATUS_TOKENS = new Set([ + "confirm", + "confirmed", + "accept", + "accepted", + "join", + "joined", + "host", + "hosting", + "attending", +]); const isConfirmedStatus = (value) => { if (!value) return false; @@ -367,6 +377,10 @@ const hasConfirmedIndicator = (invite) => { return true; } + if (invite.joined === true || invite.is_joined === true || invite.isJoined === true) { + return true; + } + return hasAnyValue(invite, [ "confirmed_at", "confirmedAt",