Add shop-chat push notifications and a "send shop to users" endpoint
- shop-chat messages (buyer <-> shop owner) never sent any push notification at all — sendShopChatPushNotification (chatHelpers.js) now fires on every send, mirroring the existing regular-chat push path; data payload carries shop_id/buyer_id/sender_role so clients can route directly to the thread - New POST /shops/:id/send, mirroring the existing shop-product send endpoint — lets a user share a shop itself (not just one product) into a regular chat message as a sharedShop payload Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ const ShopModel = require('../../../models/ShopModel')
|
||||
const ShopChatMessageModel = require('../../../models/ShopChatMessageModel')
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
const { optimizeImageFile } = require('../../../utils/imageOptimize')
|
||||
const { sendShopChatPushNotification } = require('../../../helpers/chatHelpers')
|
||||
let optimizeVideoFaststart = async () => {}
|
||||
try {
|
||||
({ optimizeVideoFaststart } = require('../../../utils/postVideoProcessing'))
|
||||
@@ -200,16 +201,27 @@ const sendShopChatMessage = async (req, res, next) => {
|
||||
const { content } = req.body
|
||||
if (!content || !content.trim()) return chatError(res, 422, 'متن پیام الزامی است')
|
||||
|
||||
const senderRole = isOwner ? 'shop' : 'buyer'
|
||||
const message = await ShopChatMessageModel.create({
|
||||
shop: shop._id,
|
||||
buyer: buyerId,
|
||||
senderRole: isOwner ? 'shop' : 'buyer',
|
||||
senderRole,
|
||||
senderId: req.user._id,
|
||||
content: content.trim(),
|
||||
readByShop: isOwner,
|
||||
readByBuyer: !isOwner
|
||||
})
|
||||
|
||||
sendShopChatPushNotification({
|
||||
shop,
|
||||
senderId: req.user._id,
|
||||
senderRole,
|
||||
buyerId,
|
||||
receiverId: isOwner ? buyerId : shop.owner
|
||||
}).catch((err) => {
|
||||
console.error('[shopChatController] push notification failed:', err.message)
|
||||
})
|
||||
|
||||
return res.status(201).json({ message })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
@@ -251,10 +263,11 @@ const sendShopChatFileMessage = async (req, res, next) => {
|
||||
}
|
||||
const fileUrl = `/shop-chat/${uniqueFileName}`
|
||||
|
||||
const senderRole = isOwner ? 'shop' : 'buyer'
|
||||
const message = await ShopChatMessageModel.create({
|
||||
shop: shop._id,
|
||||
buyer: buyerId,
|
||||
senderRole: isOwner ? 'shop' : 'buyer',
|
||||
senderRole,
|
||||
senderId: req.user._id,
|
||||
content: content && content.trim() ? content.trim() : null,
|
||||
file: fileUrl,
|
||||
@@ -263,6 +276,16 @@ const sendShopChatFileMessage = async (req, res, next) => {
|
||||
readByBuyer: !isOwner
|
||||
})
|
||||
|
||||
sendShopChatPushNotification({
|
||||
shop,
|
||||
senderId: req.user._id,
|
||||
senderRole,
|
||||
buyerId,
|
||||
receiverId: isOwner ? buyerId : shop.owner
|
||||
}).catch((err) => {
|
||||
console.error('[shopChatController] push notification failed:', err.message)
|
||||
})
|
||||
|
||||
return res.status(201).json({ message })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable camelcase */
|
||||
const mongoose = require('mongoose')
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const jwt = require('jsonwebtoken')
|
||||
@@ -7,8 +8,12 @@ const ShopProductListingModel = require('../../../models/ShopProductListingModel
|
||||
const ShopRatingModel = require('../../../models/ShopRatingModel')
|
||||
const ShopFollowModel = require('../../../models/ShopFollowModel')
|
||||
const OrderModel = require('../../../models/OrderModel')
|
||||
const MessageModel = require('../../../models/MessageModel')
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
const { maskPhoneNumbers } = require('../../../utils/phoneNumberMasking')
|
||||
const { optimizeImageFile } = require('../../../utils/imageOptimize')
|
||||
const { viewerIsBlockedBy } = require('../../../utils/blockVisibility')
|
||||
const { enrichMessage, emitChatEvents } = require('../../../helpers/chatHelpers')
|
||||
|
||||
const MS_DAY = 24 * 60 * 60 * 1000
|
||||
const ANALYTICS_RANGE_DAYS = { '7d': 7, '30d': 30, '90d': 90, all: null }
|
||||
@@ -400,6 +405,76 @@ const submitShop = async (req, res, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
/** عینِ sendProductToUsers توی shopProductController.js — پیامهای چتِ معمولی
|
||||
* (MessageModel) با یه پیامِ sharedShop برای هرکدوم از گیرندهها؛ برای ارسالِ خودِ
|
||||
* فروشگاه (نه یه کالای خاص) از دکمهی «ارسال» کنارِ اشتراکگذاری */
|
||||
const sendShopToUsers = async (req, res, next) => {
|
||||
try {
|
||||
const senderId = req.user._id
|
||||
const { receiverIds } = req.body
|
||||
|
||||
if (!Array.isArray(receiverIds) || !receiverIds.length) {
|
||||
return shopError(res, 400, 'حداقل یک گیرنده انتخاب کنید')
|
||||
}
|
||||
|
||||
const uniqueReceivers = [
|
||||
...new Set(
|
||||
receiverIds
|
||||
.map(String)
|
||||
.filter((id) => mongoose.Types.ObjectId.isValid(id) && id !== String(senderId))
|
||||
)
|
||||
].slice(0, 50)
|
||||
|
||||
if (!uniqueReceivers.length) {
|
||||
return shopError(res, 400, 'گیرنده معتبر یافت نشد')
|
||||
}
|
||||
|
||||
const shop = await ShopModel.findById(req.params.id)
|
||||
if (!shop) return shopError(res, 404, 'فروشگاه یافت نشد')
|
||||
|
||||
const sharedShop = {
|
||||
shopId: String(shop._id),
|
||||
name: shop.name,
|
||||
logo: shop.logo || '',
|
||||
url: `https://modstagram.com/shops/profile/${shop._id}`
|
||||
}
|
||||
|
||||
const content = JSON.stringify({ sharedShop })
|
||||
const io = req.app.get('io')
|
||||
|
||||
let sentCount = 0
|
||||
for (const receiverId of uniqueReceivers) {
|
||||
const receiver = await UserModel.findById(receiverId).select('blocked_users').lean()
|
||||
if (!receiver) continue
|
||||
if (viewerIsBlockedBy(receiver, senderId)) continue
|
||||
|
||||
const newMessage = new MessageModel({
|
||||
senderId,
|
||||
receiverId,
|
||||
content
|
||||
})
|
||||
await newMessage.save()
|
||||
|
||||
if (io) {
|
||||
const formatted = await enrichMessage(newMessage)
|
||||
emitChatEvents(io, formatted, senderId, receiverId)
|
||||
}
|
||||
sentCount += 1
|
||||
}
|
||||
|
||||
if (!sentCount) {
|
||||
return shopError(res, 400, 'ارسال به هیچ گیرندهای انجام نشد')
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
message: `فروشگاه به ${sentCount} نفر ارسال شد`,
|
||||
sentCount
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createDraftShop,
|
||||
getMyShops,
|
||||
@@ -412,5 +487,6 @@ module.exports = {
|
||||
updateShopLocation,
|
||||
updateShopContact,
|
||||
toggleShopFollow,
|
||||
submitShop
|
||||
submitShop,
|
||||
sendShopToUsers
|
||||
}
|
||||
|
||||
@@ -105,6 +105,32 @@ async function sendChatPushNotification(senderId, receiverId) {
|
||||
})
|
||||
}
|
||||
|
||||
// چتِ فروشگاه (خریدار <-> مالکِ فروشگاه) قبلاً هیچ پوشی نمیفرستاد — کاملاً جدا از
|
||||
// sendChatPushNotification بالا چون فرستنده میتونه «فروشگاه» باشه، نه یه کاربرِ ساده،
|
||||
// پس تایتل/عکسِ نوتیف باید از روی نقشِ فرستنده (shop یا buyer) انتخاب بشه
|
||||
async function sendShopChatPushNotification({ shop, senderId, senderRole, receiverId, buyerId }) {
|
||||
let title
|
||||
let image
|
||||
if (senderRole === 'shop') {
|
||||
title = shop?.name || 'فروشگاه'
|
||||
image = buildStorageUrl(shop?.logo) || undefined
|
||||
} else {
|
||||
const sender = await UserModel.findById(senderId).select('first_name last_name user_name profile_image').lean()
|
||||
if (!sender) return
|
||||
title = `${sender.first_name || ''} ${sender.last_name || ''}`.trim() || sender.user_name || 'کاربر'
|
||||
image = buildStorageUrl(sender.profile_image) || undefined
|
||||
}
|
||||
await sendPushToUser(receiverId, {
|
||||
title,
|
||||
description: 'یک پیام جدید برای شما ارسال کرد',
|
||||
type: 'shop_chat_message',
|
||||
// buyer_id همیشه لازمه تا مالکِ فروشگاه (که میتونه چند نخِ گفتگو با خریدارهای مختلف
|
||||
// داشته باشه) بدونه دقیقاً کدوم نخ رو باز کنه؛ برای خودِ خریدار هم همون id خودشه
|
||||
data: { shop_id: String(shop?._id || ''), sender_role: senderRole, buyer_id: String(buyerId || '') },
|
||||
image
|
||||
})
|
||||
}
|
||||
|
||||
function parseForwardedContent(content) {
|
||||
if (!content) return { body: '', forwardedFrom: null }
|
||||
|
||||
@@ -138,5 +164,6 @@ module.exports = {
|
||||
formatTimeOnly,
|
||||
enrichMessage,
|
||||
emitChatEvents,
|
||||
parseForwardedContent
|
||||
parseForwardedContent,
|
||||
sendShopChatPushNotification
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ const {
|
||||
updateShopLocation,
|
||||
updateShopContact,
|
||||
toggleShopFollow,
|
||||
submitShop
|
||||
submitShop,
|
||||
sendShopToUsers
|
||||
} = require('../../../controllers/application/shops/shopController')
|
||||
const {
|
||||
getCart,
|
||||
@@ -48,5 +49,6 @@ router.patch('/:id/location', [blockCheck], updateShopLocation)
|
||||
router.patch('/:id/contact', [blockCheck], updateShopContact)
|
||||
router.post('/:id/follow', [blockCheck], toggleShopFollow)
|
||||
router.post('/:id/submit', [blockCheck], submitShop)
|
||||
router.post('/:id/send', [blockCheck], sendShopToUsers)
|
||||
|
||||
module.exports = router
|
||||
|
||||
Reference in New Issue
Block a user