Skip to content

İletişim ve Mesajlaşma (Messaging System)

Eğitmen-öğrenci sohbeti, mesajlaşma tabloları, messages.tsx, görsel yükleme ve bildirimler.

Coursio’da öğrenciler, kayıtlı oldukları kursların eğitmenleriyle mesajlaşma üzerinden iletişim kurabilir; mesajlarda görsel paylaşımı ve bildirimler desteklenir. Bu sayfa conversations/messages tablo tasarımını, messages.tsx (öğrenci) ve instructor.messages.tsx (eğitmen) yapısını, api.chat-image-upload.ts ile resim yükleme akışını ve yeni mesaj / satış bildirimleri yönetimini açıklar.

Veritabanı: app/db/schema.ts

conversations (Konuşmalar)

AlanTipAçıklama
iduuidPrimary key.
participant1_idtext (FK → users)Genelde öğrenci.
participant2_idtext (FK → users)Genelde eğitmen.
last_messagetextSon mesaj önizlemesi (düz metin).
is_importantbooleanÖnemli işaretleme (eğitmen tarafı).
updated_attimestampSon güncelleme.

messages (Mesajlar)

AlanTipAçıklama
iduuidPrimary key.
conversation_iduuid (FK → conversations, CASCADE)Hangi konuşma.
sender_idtext (FK → users)Gönderen kullanıcı.
texttextMetin içeriği (HTML veya düz metin).
typetext'text', 'image', 'file'.
media_urltextGörsel/dosya URL’i (Bunny CDN).
created_attimestampGönderim zamanı.
is_deliveredbooleanİletildi.
is_readbooleanOkundu.

Yetki: Öğrencinin bir eğitmene mesaj atabilmesi için canStudentMessageInstructor(db, studentId, instructorId) kontrolü yapılır: öğrenci o eğitmene ait en az bir kursta iade edilmemiş kayıtlı olmalı veya aktif abone olup eğitmenin aboneliğe açık bir kursuna erişiyor olmalı (app/lib/can-message-instructor.ts).

Dosya: app/routes/messages.tsx
URL: /{lang}/messages (örn. /tr/messages).

Loader:

  1. Auth; yoksa login’e yönlendirme.
  2. Öğrenci ID = session.user.id. Konuşmalar: participant1_id = studentId (öğrenci her zaman participant1) ve canStudentMessageInstructor geçen satırlar; diğer taraf eğitmen (participant2).
  3. Seçili konuşma: URL ?conv=... veya listedeki ilk konuşma. Seçili konuşmanın mesajları: messages tablosundan conversation_id ile, created_at artan, limit 50; her mesajda id, text, senderId, createdAt, type, mediaUrl, isDelivered, isRead.
  4. Dönüş: user, conversations, initialMessages, initialSelectedId, lang.

Sayfa yapısı:

  • Sol panel: Arama, filtre (Tümü / Okunmamış / Önemli), konuşma listesi (avatar, ad, son mesaj önizlemesi, tarih, yıldız ile önemli işaretleme).
  • Sağ panel: Seçili konuşma — üstte karşı taraf bilgisi; mesaj listesi (metin + varsa type === 'image' için mediaUrl ile görsel; gönderene göre hizalama, teslim/okundu ikonu); altta metin girişi (ChatInput — Tiptap, HTML) ve görsel ekleme butonu (input file → api.chat-image-upload).
  • WebSocket: wss://host/api/chat?userId=...&convId=... ile canlı bağlantı; mesaj gönderimi { text, type: "text" } veya { type: "image", mediaUrl, text: "" }; gelen mesajlar NEW_MESSAGE ile listeye eklenir; MARK_READ, TOGGLE_IMPORTANT gibi olaylar gönderilir.

instructor.messages.tsx: Aynı arayüz; loader’da participant2_id = instructorId ile konuşmalar çekilir, diğer taraf öğrenci (participant1). Görsel yükleme ve WebSocket akışı öğrenci tarafıyla aynı mantıkta.

Görsel Paylaşımı: Mesajlarda Resim Yükleme

Section titled “Görsel Paylaşımı: Mesajlarda Resim Yükleme”

Endpoint: POST /api/chat-image-upload
Dosya: app/routes/api.chat-image-upload.ts

Akış:

  1. FormData: file alanında tek dosya; yoksa veya File değilse 400.
  2. Tip kontrolü: Sadece image/jpeg, image/png, image/gif, image/webp kabul edilir; aksi halde 400.
  3. Bunny Storage: Ortam değişkenleri BUNNY_STORAGE_ZONE, BUNNY_STORAGE_KEY, BUNNY_PULL_ZONE_URL; eksikse 500.
  4. Dosya adı: Date.now() + sanitize edilmiş orijinal ad; path: chat-media/{safeName}.
  5. Yükleme: PUT https://storage.bunnycdn.com/{storageZone}/{uploadPath} — header: AccessKey: storageKey, Content-Type: file.type; body: Uint8Array(file.arrayBuffer()).
  6. Yanıt: Başarılıysa { url: "{pullZoneUrl}/{uploadPath}" } (JSON); hata durumunda { error: "..." }.

İstemci (messages.tsx / instructor.messages.tsx):

  • Görsel ekleme butonu ile <input type="file" accept="image/*" /> tetiklenir.
  • Seçilen dosya FormData ile POST /api/chat-image-upload’a gönderilir.
  • Dönen url ile WebSocket’e { type: "image", mediaUrl: url, text: "" } gönderilir; sunucu tarafında mesaj kaydı oluşturulur (type: 'image', media_url: url).
  • Mesaj listesinde msg.type === 'image' && msg.mediaUrl ile resim gösterilir; tıklanınca büyük önizleme (Dialog) açılabilir.

Okunmamış mesaj sayısı (badge):

  • Hook: app/hooks/useChatNotifications.tsuseChatNotifications(userId).
  • GraphQL: İlk yükleme ve periyodik (örn. 45 sn) / sayfa odaklanınca unreadMessagesCount sorgulanır; sayı artarsa bildirim tetiklenir.
  • WebSocket: wss://host/api/chat/notifications?userId=... — gelen olaylar:
    • NEW_MESSAGE: Okunmamış sayacı artırır; tarayıcı gizliyse Notification API (izin varsa) ve toast ile “Yeni mesajınız var” gösterilir.
    • UPDATE_BADGE: Okundu güncellemesi; sayaç düşer veya data.count ile güncellenir.

Navbar: Öğrenci/eğitmen menüsünde “Mesajlar” linki (/messages veya /instructor/messages); okunmamış sayı bu hook ile badge’de gösterilir. “Bildirimler” linki /{lang}/notifications (uygulamada öğrenci bildirim sayfası varsa oraya gider).

Satış ve yeni öğrenci bildirimleri e-posta ile yönetilir; WebSocket/uygulama içi bildirim değildir.

Webhook (checkout.session.completed) içinde:

  • Öğrenciye: Satın alma makbuzu — sendPurchaseReceiptEmailWithInvoice (öğrenci e-posta, ad, makbuz kalemleri, fatura eki).
  • Eğitmene: Yeni öğrenci bilgisi — sendNewStudentJoinedEmail (eğitmen e-posta, ad, kurs adı, öğrenci adı vb.).

Dosya: app/lib/email.ts — Resend (veya yapılandırılmış SMTP) ile gönderim.

Admin paneli: app/routes/admin.tsx — “Son Hareketler” kutusu ve “Tüm Bildirimler” modalı.

  • Veri: getAdminNotificationCounts, getAdminRecentActivities, getAdminAllActivities (app/lib/db-queries.ts) — bekleyen kurs onayları, ödeme talepleri vb. sayılar ve son hareket listesi (ödeme talepleri + kurs onay talepleri).
  • API: “Tümünü gör” için GET /api/admin/activities — en fazla 500 kayıt JSON döner.
  • Bu bildirimler yeni mesaj değil; admin işleri (onay bekleyen kurs, payout talepleri) içindir.
KonuAçıklama
Tablolarconversations (participant1=öğrenci, participant2=eğitmen, last_message, is_important); messages (conversation_id, sender_id, text, type, media_url, is_delivered, is_read).
YetkicanStudentMessageInstructor: öğrenci eğitmene ait kursta kayıtlı veya abonelik ile erişiyor olmalı.
messages.tsxÖğrenci mesaj sayfası; konuşma listesi + seçili sohbet + WebSocket + ChatInput + görsel yükleme.
instructor.messages.tsxEğitmen tarafı aynı UI; participant2=eğitmen ile konuşmalar.
Görsel yüklemePOST /api/chat-image-upload → Bunny Storage (chat-media/); dönen URL WebSocket ile type: "image" mesajı olarak gönderilir.
Mesaj bildirimiuseChatNotifications: GraphQL unreadMessagesCount + WebSocket /api/chat/notifications (NEW_MESSAGE / UPDATE_BADGE); tarayıcı bildirimi + toast.
Satış bildirimiE-posta: sendPurchaseReceiptEmailWithInvoice (öğrenci), sendNewStudentJoinedEmail (eğitmen) — webhook içinde.
Admin bildirimSon hareketler + Tüm bildirimler modalı; ödeme talepleri ve kurs onayları.

Canlı mesajlaşma için WebSocket sunucusu /api/chat (ve /api/chat/notifications) Durable Object veya ayrı bir WebSocket servisi ile implemente edilir; dokümantasyon bu sayfada özetlenen istemci ve tablo/API sözleşmelerine odaklanır.

  • app/routes/messages.tsx — Öğrenci mesajlar sayfası.
  • app/routes/instructor.messages.tsx — Eğitmen mesajlar sayfası.
  • app/routes/api.chat-image-upload.ts — Sohbet görsel yükleme (Bunny Storage).
  • app/lib/can-message-instructor.ts — Öğrenci–eğitmen mesaj yetkisi.
  • app/lib/db-queries.ts — getAdminNotificationCounts, getAdminRecentActivities, getAdminAllActivities.
  • app/routes/admin.tsx — Son hareketler ve Tüm bildirimler modalı.
  • app/routes/api.admin.activities.ts — GET /api/admin/activities.