- MessageInput: paperclip button (team-only, controlled by teamUserIds
from ThreadDetail) uploads through the media service and hands the URL
to the WebSocket; Enter now inserts a newline — sending is button-only
since accidental Enter-sends were noisy for staff replies.
- MessageBubble: renders image messages inline (tap to open), shows the
coworker's username at the top of team-side bubbles so staff can tell
who typed what.
- MessageList / ThreadChatPage: any message from a store team member
(owner or active staff) renders on the "our" side of the bubble even
for a viewer who isn't the sender, using team_user_ids from
ThreadDetail. Delete option stays gated to the actual sender.
- ThreadsPage: inbox now shows smart-relative timestamps
(today HH:MM / دیروز HH:MM / N روز پیش / Persian date), plus a
reply-state icon per row (red bell = customer waiting, blue double-check
= you replied and they've seen it, nothing = you replied not yet seen).
Image-only last messages preview as "📷 تصویر".
- Push notifications: on mount we re-POST the current PushSubscription so
a browser-rotated sub or a backend row deleted after a 410 auto-heals.
- useWebSocket: SendMessagePayload and NewMessageEvent carry image_url so
the server relay can round-trip images.
- API types: MessageList.imageUrl, LastMessage.imageUrl,
ThreadList.lastMessageFromTeam/otherPartySeenLast, ThreadDetail.teamUserIds.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- app/hooks/useStoreOwnership.ts: the old redirect logic sent owners to /
the moment useMyStores briefly resolved to []. That empty array came from
useMyStores's dead `if (!token) return []` inside the queryFn, so any
transient auth blip left the store page thinking the user has no stores.
Now compares usernames case-insensitively, only redirects while the query
is settled (!isLoading && !isFetching && !isError), and never bounces to
home on empty — worst case the seller sees an empty dashboard rather than
a silent kick-out.
- app/requestHandler/use-seller-hooks.ts: removed the unreachable
`if (!token) return []` in useMyStores and added the token to the
queryKey so a login switch stops serving the previous user's cached list.
- app/routes/store.$storeId._index.tsx: mount StorePushPromptCard at the
top of the store dashboard.
- app/routes/store.$storeId.tsx: sidebar "پیامها" badge reads
badges.sellerChat[storeId] (per-store) instead of the buyer-side
badges.chat, matching the badge-serializer split.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- app/components/MyLayout.tsx / app/components/home/HomePageTopBar.tsx /
app/routes/profile._index.tsx: the BadgeCounts type was declared with
snake_case fields (order_updates, seller_orders) while the API's global
camelCase renderer sends orderUpdates / sellerOrders on the wire — every
read went to undefined, so the "buyer activity" dot on the profile icon
and the order-badge on the store dashboard were silently stuck at 0.
Fixed by renaming the interface to match the wire and adding sellerChat
(per-store roll-up) so the store dashboard sidebar reads
badges.sellerChat[storeId] instead of the buyer-side chat count.
- app/hooks/useBadgeCounts.ts: interface + EMPTY constant updated to
camelCase + new sellerChat map.
- app/utils/api-error-handler.ts: 403 no longer force-redirects to /logout.
It used to force-log-out on ANY 403/401, so an approved-but-unbadged
owner (Ehsan / Noelabel) whose store dashboard fired the Instagram-tab
query got a 403 → forced logout → the store page vanished mid-load
and the seller landed on the home screen convinced they'd been kicked
off their own store. Only 401 signs the session out now.
- app/components/store/StorePushPromptCard.tsx: new dashboard-level card
that nudges the store owner/staff to enable browser push. Unlike the
global buyer banner it's not dismissible-forever; "بعداً یادآوری کن"
hides for 24h and it comes back until the user actually subscribes.
Handles the permission="denied" path with an in-browser-settings hint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Review fixes:
- useMyStores throws on a failed response instead of resolving to [] — a
transient 401/blip no longer looks like "no stores" and bounces an owner off
their own dashboard (query stays in error/retry, keeps prior data).
- Add useRequireOwner() guard and apply it to the owner-only routes
(financial-dashboard + its cash-funds/reports/transactions sub-routes and
shipping-method) so staff who hit those URLs directly are redirected to the
dashboard. (Financial data was already backend-protected; this is the matching
client-side gate.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- useMyStores + team hooks (invite by phone / list / remove) in use-seller-hooks.
- useStoreOwnership rewritten to allow staff (not just the owner) via my-stores;
exposes `role` so callers can gate owner-only UI, and no longer redirects
members away from a store they help manage.
- Store settings: owner-only items (financial, shipping, edit store, instagram
sync, team) hidden from staff; new "مدیریت اعضای تیم" entry (owner-only).
- New /store/:storeId/team page: invite by phone + members list with pending/
active status + remove (owner-only, redirects staff). MyLayout keeps store mode
on the team route.
- StoreForm edit is owner-only (redirects staff). Profile "enter my store" now
routes owners to their store and staff to the store they manage.
- Chat already split earlier: seller inbox vs personal buyer inbox.
Depends on the seller-team backend; ships on this branch, not main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The backend view returns {"public_key": ...} but the global
CamelCaseJSONRenderer rewrites it to {"publicKey": ...} on the wire,
so destructuring `public_key` always resolved to undefined and the
subscribe path bailed silently. Browsers never registered with the
backend, no PushSubscription rows were ever created, and no push
notifications fired.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces three events without a full notification center:
- service worker (public/sw.js) gains push + notificationclick handlers
(RTL, focuses an existing tab or opens the target URL).
- usePushNotifications hook: permission + VAPID subscribe/unsubscribe
against the backend push endpoints.
- opt-in UI both ways: a one-time dismissible post-login banner
(PushPermissionBanner, rendered in MyLayout) and a permanent toggle
(PushSettingToggle) on buyer profile settings and seller store settings.
- useBadgeCounts polls /api/notif/v1/badges/; small red badges on the
bottom-nav profile icon (chat + order updates), the profile sidebar
(پیامها / سفارشها) and the seller dashboard sidebar. Visiting orders
clears its badge.
Degrades gracefully: if the backend endpoints are absent the counts fall
back to zero and nothing throws.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>