Приложение
This commit is contained in:
73
client-mobile/chats/data/remote/api/ChatApi.kt
Normal file
73
client-mobile/chats/data/remote/api/ChatApi.kt
Normal file
@@ -0,0 +1,73 @@
|
||||
package chats.data.remote.api
|
||||
|
||||
import chats.data.remote.dto.ChatDto
|
||||
import chats.data.remote.dto.MessageDto
|
||||
import retrofit2.http.*
|
||||
|
||||
interface ChatApi {
|
||||
@GET("chats")
|
||||
suspend fun getChats(): List<ChatDto>
|
||||
|
||||
@GET("messages/chat/{chatId}")
|
||||
suspend fun getMessages(
|
||||
@Path("chatId") chatId: String,
|
||||
@Query("cursor") cursor: String? = null,
|
||||
@Query("limit") limit: Int? = 50
|
||||
): List<MessageDto>
|
||||
|
||||
@POST("messages/chat/{chatId}")
|
||||
suspend fun sendMessage(@Path("chatId") chatId: String, @Body content: String): MessageDto
|
||||
|
||||
@Multipart
|
||||
@POST("messages/upload")
|
||||
suspend fun uploadFile(@Part file: okhttp3.MultipartBody.Part): FileUploadResponse
|
||||
|
||||
// Klipy GIF API
|
||||
@GET("klipy/trending")
|
||||
suspend fun getTrendingGifs(@Query("page") page: Int): List<KlipyGifDto>
|
||||
|
||||
@GET("klipy/search")
|
||||
suspend fun searchGifs(@Query("q") query: String, @Query("page") page: Int): List<KlipyGifDto>
|
||||
|
||||
@GET("klipy/categories")
|
||||
suspend fun getGifCategories(): List<GifCategoryDto>
|
||||
|
||||
@POST("klipy/shared/{id}")
|
||||
suspend fun markGifShared(@Path("id") id: String, @Body query: String)
|
||||
|
||||
@POST("messages/{messageId}/reactions")
|
||||
suspend fun addReaction(@Path("messageId") messageId: String, @Body emoji: String)
|
||||
|
||||
@POST("chats/{chatId}/typing")
|
||||
suspend fun sendTypingStatus(@Path("chatId") chatId: String)
|
||||
|
||||
@POST("chats/{chatId}/read")
|
||||
suspend fun markMessagesAsRead(@Path("chatId") chatId: String, @Body lastMessageId: String)
|
||||
}
|
||||
|
||||
data class KlipyGifDto(
|
||||
val id: String,
|
||||
val images: GifImagesDto?,
|
||||
val title: String?
|
||||
)
|
||||
|
||||
data class GifImagesDto(
|
||||
val fixed_height: GifImageSourceDto?,
|
||||
val original: GifImageSourceDto?
|
||||
)
|
||||
|
||||
data class GifImageSourceDto(
|
||||
val url: String
|
||||
)
|
||||
|
||||
data class GifCategoryDto(
|
||||
val category: String,
|
||||
val preview_url: String,
|
||||
val query: String
|
||||
)
|
||||
|
||||
data class FileUploadResponse(
|
||||
val url: String,
|
||||
val filename: String,
|
||||
val size: Long
|
||||
)
|
||||
36
client-mobile/chats/data/remote/dto/ChatDtos.kt
Normal file
36
client-mobile/chats/data/remote/dto/ChatDtos.kt
Normal file
@@ -0,0 +1,36 @@
|
||||
package chats.data.remote.dto
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class UserBasicDto(
|
||||
@SerializedName("id") val id: String,
|
||||
@SerializedName("userName") val userName: String,
|
||||
@SerializedName("displayName") val displayName: String,
|
||||
@SerializedName("avatarUrl") val avatarUrl: String?
|
||||
)
|
||||
|
||||
data class MessageDto(
|
||||
@SerializedName("id") val id: String,
|
||||
@SerializedName("chatId") val chatId: String,
|
||||
@SerializedName("senderId") val senderId: String,
|
||||
@SerializedName("content") val content: String?,
|
||||
@SerializedName("type") val type: String,
|
||||
@SerializedName("sequenceId") val sequenceId: Int,
|
||||
@SerializedName("createdAt") val createdAt: String,
|
||||
@SerializedName("sender") val sender: UserBasicDto,
|
||||
@SerializedName("media") val media: List<MediaItemDto> = emptyList()
|
||||
)
|
||||
|
||||
data class MediaItemDto(
|
||||
@SerializedName("id") val id: String,
|
||||
@SerializedName("type") val type: String,
|
||||
@SerializedName("url") val url: String
|
||||
)
|
||||
|
||||
data class ChatDto(
|
||||
@SerializedName("id") val id: String,
|
||||
@SerializedName("type") val type: String,
|
||||
@SerializedName("name") val name: String?,
|
||||
@SerializedName("unreadCount") val unreadCount: Int,
|
||||
@SerializedName("messages") val messages: List<MessageDto>
|
||||
)
|
||||
31
client-mobile/chats/data/remote/signalr/ChatEvent.kt
Normal file
31
client-mobile/chats/data/remote/signalr/ChatEvent.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
package chats.data.remote.signalr
|
||||
|
||||
import chats.data.remote.dto.ChatDto
|
||||
import chats.data.remote.dto.MessageDto
|
||||
|
||||
sealed class ChatEvent {
|
||||
data class NewMessage(val message: MessageDto) : ChatEvent()
|
||||
data class MessageEdited(val messageId: String, val chatId: String, val content: String) : ChatEvent()
|
||||
data class MessageDeleted(val messageId: String, val chatId: String) : ChatEvent()
|
||||
data class MessagesRead(val chatId: String, val userId: String, val lastReadSequenceId: Int) : ChatEvent()
|
||||
data class UserTyping(val chatId: String, val userId: String) : ChatEvent()
|
||||
data class UserStoppedTyping(val chatId: String, val userId: String) : ChatEvent()
|
||||
data class UserOnline(val userId: String) : ChatEvent()
|
||||
data class UserOffline(val userId: String, val lastSeen: String?) : ChatEvent()
|
||||
data class NewChat(val chat: ChatDto) : ChatEvent()
|
||||
data class ReactionUpdated(val messageId: String, val chatId: String, val userId: String, val emoji: String) : ChatEvent()
|
||||
|
||||
// Call Events (WebRTC Signaling)
|
||||
data class CallIncoming(val chatId: String, val from: String, val offer: String, val callType: String) : ChatEvent()
|
||||
data class CallAnswered(val chatId: String, val answer: String) : ChatEvent()
|
||||
data class IceCandidateReceived(val chatId: String, val candidate: String) : ChatEvent()
|
||||
data class CallEnded(val chatId: String) : ChatEvent()
|
||||
|
||||
// Group Call Events
|
||||
data class GroupCallIncoming(val chatId: String, val from: String, val callerInfo: Any) : ChatEvent()
|
||||
data class GroupCallParticipants(val chatId: String, val participants: List<String>) : ChatEvent()
|
||||
data class GroupCallUserJoined(val chatId: String, val userId: String) : ChatEvent()
|
||||
data class GroupCallUserLeft(val chatId: String, val userId: String) : ChatEvent()
|
||||
data class GroupCallOffer(val chatId: String, val from: String, val offer: String) : ChatEvent()
|
||||
data class GroupCallAnswer(val chatId: String, val from: String, val answer: String) : ChatEvent()
|
||||
}
|
||||
86
client-mobile/chats/data/remote/signalr/ChatHubClient.kt
Normal file
86
client-mobile/chats/data/remote/signalr/ChatHubClient.kt
Normal file
@@ -0,0 +1,86 @@
|
||||
package chats.data.remote.signalr
|
||||
|
||||
import android.util.Log
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import com.microsoft.signalr.HubConnection
|
||||
import com.microsoft.signalr.HubConnectionBuilder
|
||||
import com.microsoft.signalr.HubConnectionState
|
||||
import chats.data.remote.dto.ChatDto
|
||||
import chats.data.remote.dto.MessageDto
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ChatHubClient @Inject constructor() {
|
||||
private var hubConnection: HubConnection? = null
|
||||
private val _events = MutableSharedFlow<ChatEvent>(extraBufferCapacity = 64)
|
||||
val events: SharedFlow<ChatEvent> = _events.asSharedFlow()
|
||||
|
||||
fun connect(baseUrl: String, accessToken: String) {
|
||||
if (hubConnection?.connectionState == HubConnectionState.CONNECTED) return
|
||||
|
||||
hubConnection = HubConnectionBuilder.create("${baseUrl}/chatHub")
|
||||
.withAccessTokenProvider(Single.just(accessToken))
|
||||
.build()
|
||||
|
||||
setupHandlers()
|
||||
|
||||
hubConnection?.onClosed { exception ->
|
||||
Log.e("ChatHubClient", "Connection closed", exception)
|
||||
}
|
||||
|
||||
hubConnection?.start()?.blockingAwait()
|
||||
}
|
||||
|
||||
private fun setupHandlers() {
|
||||
hubConnection?.let { conn ->
|
||||
conn.on("new_message", { message: MessageDto ->
|
||||
_events.tryEmit(ChatEvent.NewMessage(message))
|
||||
}, MessageDto::class.java)
|
||||
|
||||
conn.on("messages_read", { chatId: String, userId: String, lastReadSequenceId: Int ->
|
||||
_events.tryEmit(ChatEvent.MessagesRead(chatId, userId, lastReadSequenceId))
|
||||
}, String::class.java, String::class.java, Int::class.java)
|
||||
|
||||
conn.on("user_typing", { chatId: String, userId: String ->
|
||||
_events.tryEmit(ChatEvent.UserTyping(chatId, userId))
|
||||
}, String::class.java, String::class.java)
|
||||
|
||||
conn.on("user_online", { userId: String ->
|
||||
_events.tryEmit(ChatEvent.UserOnline(userId))
|
||||
}, String::class.java)
|
||||
|
||||
conn.on("new_chat", { chat: ChatDto ->
|
||||
_events.tryEmit(ChatEvent.NewChat(chat))
|
||||
}, ChatDto::class.java)
|
||||
|
||||
conn.on("reaction_updated", { messageId: String, chatId: String, userId: String, emoji: String ->
|
||||
_events.tryEmit(ChatEvent.ReactionUpdated(messageId, chatId, userId, emoji))
|
||||
}, String::class.java, String::class.java, String::class.java, String::class.java)
|
||||
|
||||
// WebRTC Signaling Handlers
|
||||
conn.on("call_incoming", { chatId: String, from: String, offer: String, callType: String ->
|
||||
_events.tryEmit(ChatEvent.CallIncoming(chatId, from, offer, callType))
|
||||
}, String::class.java, String::class.java, String::class.java, String::class.java)
|
||||
|
||||
conn.on("call_answered", { chatId: String, answer: String ->
|
||||
_events.tryEmit(ChatEvent.CallAnswered(chatId, answer))
|
||||
}, String::class.java, String::class.java)
|
||||
|
||||
conn.on("ice_candidate", { chatId: String, candidate: String ->
|
||||
_events.tryEmit(ChatEvent.IceCandidateReceived(chatId, candidate))
|
||||
}, String::class.java, String::class.java)
|
||||
|
||||
conn.on("call_ended", { chatId: String ->
|
||||
_events.tryEmit(ChatEvent.CallEnded(chatId))
|
||||
}, String::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
hubConnection?.stop()
|
||||
}
|
||||
}
|
||||
68
client-mobile/chats/data/repository/ChatRepositoryImpl.kt
Normal file
68
client-mobile/chats/data/repository/ChatRepositoryImpl.kt
Normal file
@@ -0,0 +1,68 @@
|
||||
package chats.data.repository
|
||||
|
||||
import chats.data.remote.api.ChatApi
|
||||
import chats.data.remote.dto.ChatDto
|
||||
import chats.data.remote.dto.MessageDto
|
||||
import chats.domain.model.Chat
|
||||
import chats.domain.model.Message
|
||||
import chats.domain.model.MediaType
|
||||
import chats.domain.repository.ChatRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class ChatRepositoryImpl @Inject constructor(
|
||||
private val api: ChatApi
|
||||
) : ChatRepository {
|
||||
|
||||
override suspend fun getChats(): List<Chat> {
|
||||
return api.getChats().map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun getMessages(chatId: String): List<Message> {
|
||||
return api.getMessages(chatId).map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(chatId: String, content: String): Message {
|
||||
return api.sendMessage(chatId, content).toDomain()
|
||||
}
|
||||
|
||||
override suspend fun addReaction(messageId: String, emoji: String) {
|
||||
api.addReaction(messageId, emoji)
|
||||
}
|
||||
|
||||
override suspend fun sendTypingStatus(chatId: String) {
|
||||
api.sendTypingStatus(chatId)
|
||||
}
|
||||
|
||||
override suspend fun markMessagesAsRead(chatId: String, lastMessageId: String) {
|
||||
api.markMessagesAsRead(chatId, lastMessageId)
|
||||
}
|
||||
}
|
||||
|
||||
// Mappers
|
||||
fun ChatDto.toDomain(): Chat = Chat(
|
||||
id = id,
|
||||
type = type,
|
||||
name = name ?: "Unknown Chat",
|
||||
avatar = null, // Update if avatar is added to ChatDto
|
||||
unreadCount = unreadCount,
|
||||
lastMessage = messages.firstOrNull()?.toDomain()
|
||||
)
|
||||
|
||||
fun MessageDto.toDomain(): Message = Message(
|
||||
id = id,
|
||||
chatId = chatId,
|
||||
senderId = senderId,
|
||||
senderName = sender.displayName,
|
||||
content = content,
|
||||
sequenceId = sequenceId,
|
||||
createdAt = createdAt,
|
||||
media = media.map { it.url },
|
||||
mediaUrl = media.firstOrNull()?.url,
|
||||
mediaType = when (media.firstOrNull()?.type) {
|
||||
"image" -> MediaType.IMAGE
|
||||
"video" -> MediaType.VIDEO
|
||||
"audio" -> MediaType.AUDIO
|
||||
"file" -> MediaType.FILE
|
||||
else -> MediaType.TEXT
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user