Приложение

This commit is contained in:
Халимов Рустам
2026-04-14 01:15:54 +03:00
parent 8399d32490
commit 1fb1be47dd
126 changed files with 6145 additions and 0 deletions

View 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
)

View 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>
)

View 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()
}

View 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()
}
}

View 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
}
)