Messages v2 foto — RTK Query
~5 dkMobil / WebKararlı
Mesajlaşma v2 fotoğraf gönderimi mobil
Mesajlasma V2 - React Native (RTK Query) Kullanim ve Migration Rehberi
Migration rehberi
Yapılandırma sırası için Migration şablonu. Yeni düzenlemelerde Amaç → Önkoşullar → Endpoint → Request/Response → Hata kodları → Client adımları → İlgili sayfalar bölümlerini tercih edin.
Bu doküman, mesaj gönderme akisinin v2ye alınmasi ve fotograf gönderiminin mobil tarafta (React Native + RTK Query) entegrasyonu için hazırlandı.
1) Kisa Ozet
- Yeni route acilmadi, ayni endpointin
v2versiyonu eklendi. - Mesaj gönderimi:
POST /api/v1/messages-> mevcut davranis (geri uyumluluk)POST /api/v2/messages-> yeni davranis (text + fotograf)
- Chat gecmisi endpointi degismedi:
GET /api/v1/messages/chat/:chatId
- Socket eventi degismedi:
/chatnamespace,newMessageeventi
2) V2 Backend Sozlesmesi
2.1 Endpoint
- URL:
POST /api/v2/messages - Auth:
Bearer <JWT>zorunlu - Content-Type:
- Text mesaj için
application/json - Fotograf mesaji için
multipart/form-data
- Text mesaj için
2.2 Request Kurallari
- Ortak alanlar:
type:'text' | 'media'chatId?: mevcut sohbet IDtargetUserId?: sohbet yoksa hedef kullanıcı IDcontent?: text icerigi veya media caption
- Is kurallari:
chatIdyoksatargetUserIdzorunlutype='text':contentzorunlufileolmamali
type='media':filezorunlu (tek dosya)contentopsiyonel (caption)
2.3 Fotograf Dogrulama ve Optimizasyon
- Izinli mime type:
image/jpegimage/jpgimage/pngimage/webp
- Maksimum dosya boyutu:
10MB - Sunucu tarafi optimizasyon:
- EXIF orientation duzeltme
- Max uzun kenar:
2048 - Cikti formati:
webp(quality=82)
- Depolama yolu:
messages/photos
2.4 Ornek Response (MessageResponseDto)
{
"_id": "67e4f4f0d56e3a0ddbfab123",
"chatId": "67e4f3ffd56e3a0ddbfab111",
"senderId": "67e4f2aed56e3a0ddbfab001",
"type": "media",
"content": "aksamdan bir kare",
"media": {
"url": "https://cdn.example.com/messages/photos/8c7f.webp",
"type": "image",
"name": "IMG_1123.jpg",
"size": 148220,
"width": 1440,
"height": 1080
},
"creditTransfer": null,
"readBy": ["67e4f2aed56e3a0ddbfab001"],
"createdAt": "2026-03-09T10:10:10.000Z",
"updatedAt": "2026-03-09T10:10:10.000Z"
}3) RTK Query Entegrasyonu (React Native)
3.1 Tipler
export type MessageType = 'text' | 'media' | 'credit_transfer';
export type MessageMedia = {
url: string;
type: 'image' | 'video' | 'audio' | 'file';
name?: string;
size?: number;
duration?: number;
width?: number;
height?: number;
thumbnail_url?: string;
};
export type Message = {
_id: string;
chatId: string;
senderId: string;
type: MessageType;
content?: string | null;
media?: MessageMedia | null;
creditTransfer?: {
amount: number;
recipientId: string;
senderId: string;
} | null;
readBy: string[];
createdAt: string;
updatedAt: string;
};
export type MessageListResponse = {
messages: Message[];
participants: Array<{
_id: string;
username?: string;
}>;
page: number;
limit: number;
total: number;
totalPages: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
};
export type BaseResponseDto<T> = {
isSuccess: boolean;
statusCode: number;
data: T;
errors?: string[];
timestamp: string;
};3.2 Base API (ornek)
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import type { RootState } from '../store';
export const baseApi = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: `${process.env.EXPO_PUBLIC_API_URL}/api`,
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.accessToken;
if (token) headers.set('Authorization', `Bearer ${token}`);
return headers;
},
}),
tagTypes: ['Messages'],
endpoints: () => ({}),
});3.3 Messages API (v2 send + mevcut history)
import { baseApi } from './baseApi';
import type { BaseResponseDto, Message, MessageListResponse } from '@/types/messages';
type GetChatMessagesArgs = {
chatId: string;
page?: number;
limit?: number;
};
type SendTextMessageV2Args = {
chatId?: string;
targetUserId?: string;
content: string;
};
type SendPhotoMessageV2Args = {
chatId?: string;
targetUserId?: string;
content?: string; // caption
file: {
uri: string;
name: string;
type: 'image/jpeg' | 'image/jpg' | 'image/png' | 'image/webp';
};
};
const unwrap = <T>(response: BaseResponseDto<T> | T): T => {
if (response && typeof response === 'object' && 'data' in (response as any)) {
return (response as BaseResponseDto<T>).data;
}
return response as T;
};
export const messagesApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getChatMessages: builder.query<MessageListResponse, GetChatMessagesArgs>({
query: ({ chatId, page = 1, limit = 20 }) => ({
url: `v1/messages/chat/${chatId}`,
params: { page, limit },
}),
transformResponse: (res: BaseResponseDto<MessageListResponse> | MessageListResponse) =>
unwrap(res),
providesTags: (_result, _error, arg) => [{ type: 'Messages', id: arg.chatId }],
}),
sendTextMessageV2: builder.mutation<Message, SendTextMessageV2Args>({
query: (body) => ({
url: 'v2/messages',
method: 'POST',
body: {
type: 'text',
chatId: body.chatId,
targetUserId: body.targetUserId,
content: body.content,
},
}),
transformResponse: (res: BaseResponseDto<Message> | Message) => unwrap(res),
invalidatesTags: (result) =>
result?.chatId ? [{ type: 'Messages', id: result.chatId }] : [],
}),
sendPhotoMessageV2: builder.mutation<Message, SendPhotoMessageV2Args>({
query: ({ chatId, targetUserId, content, file }) => {
const form = new FormData();
form.append('type', 'media');
if (chatId) form.append('chatId', chatId);
if (targetUserId) form.append('targetUserId', targetUserId);
if (content) form.append('content', content);
form.append('file', file as any);
return {
url: 'v2/messages',
method: 'POST',
body: form,
};
},
transformResponse: (res: BaseResponseDto<Message> | Message) => unwrap(res),
invalidatesTags: (result) =>
result?.chatId ? [{ type: 'Messages', id: result.chatId }] : [],
}),
}),
});
export const {
useGetChatMessagesQuery,
useSendTextMessageV2Mutation,
useSendPhotoMessageV2Mutation,
} = messagesApi;3.4 React Native kritik not (FormData)
FormDataile dosya gönderirkenContent-Typeheader'ini elle set etmeyin.fetchBaseQueryboundary'yi kendışı olusturur.- RN image picker cevabindan
uri,name,typemap edilmelidir.
Ornek map:
const photoFile = {
uri: asset.uri,
name: asset.fileName ?? `photo-${Date.now()}.jpg`,
type: (asset.type ?? 'image/jpeg') as 'image/jpeg' | 'image/jpg' | 'image/png' | 'image/webp',
};4) Migration Plani (v1 -> v2)
4.1 Etkilenen akislari ayir
- Degismeyenler
GET /api/v1/messages/chat/:chatId- Socket
newMessagedinleme POST /api/v1/messages/credit-transfer
- Degisen
- Mesaj gönderme mutasyonu (artık
v2/messages)
- Mesaj gönderme mutasyonu (artık
4.2 API katmani migration adimlari
- Eski
sendMessagemutasyonunu kaldirmayin; oncesendTextMessageV2vesendPhotoMessageV2ekleyin. - Composer UI'yi yeni mutasyonlara baglayin:
- text send ->
sendTextMessageV2 - image send ->
sendPhotoMessageV2
- text send ->
- Production rollout'ta gecici fallback birakin:
- v2 hata alınirsa text için bir kere
v1/messagesretry (opsiyonel)
- v2 hata alınirsa text için bir kere
- Stabil olduktan sonra v1 send mutasyonunu kaldirin.
4.3 UI migration checklist
- Composer'a image picker aksiyonu ekle
- Gonderim oncesi local validation:
- mime type kontrolu
- 10MB boyut kontrolu
mediamesaj render'i:message.type === 'media' && message.media?.type === 'image'contentvarsa caption olarak göster
4.4 Hata kodlari ve beklenen davranis
400 Bad Request- content bos (text mesajda)
- media için file yok
- chatId ve targetUserId ikisi de yok
- gecersiz mime type / 10MB ustu dosya
403 Forbidden- follow/block kurali nedeniyle gönderim engeli
404 Not Found- chat bulunamadi
UI tarafında 400 hatalarini kullanıcıya dogrudan okunur mesajla gösterin, 5xx hatalarda generic retry/snackbar kullanın.
5) E2E Test Senaryolari (Mobil)
- Var olan chatte text mesaj gönder (
/api/v2/messages) -> basarili, listede gorunur, socketnewMessagegelir. - Yeni chat acarak text gönder (
targetUserIdile) -> basarili, chat olusur. - Var olan chatte fotograf + caption gönder -> basarili,
type=media,media.urldolu. - 10MB ustu dosya gönder -> 400 döner.
image/gifgönder -> 400 döner.type='text'+ file gönder -> 400 döner.chatIdvetargetUserIdolmadan gönder -> 400 döner.