Отправка гиф

This commit is contained in:
Халимов Рустам
2026-04-14 13:55:01 +03:00
parent 8ce4bc714f
commit 8165b74e43
16 changed files with 117 additions and 15 deletions

View File

@@ -75,4 +75,12 @@ class AuthRepositoryImpl @Inject constructor(
Result.failure(e)
}
}
override suspend fun updatePushToken(token: String) {
try {
api.updatePushToken(token)
} catch (e: Exception) {
// Silent fail
}
}
}

View File

@@ -8,4 +8,5 @@ interface AuthRepository {
suspend fun logout()
suspend fun fetchConfig(): Result<Unit>
fun isAuthenticated(): Boolean
suspend fun updatePushToken(token: String)
}

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import com.google.firebase.messaging.FirebaseMessaging
import javax.inject.Inject
data class AuthState(
@@ -35,6 +36,7 @@ class AuthViewModel @Inject constructor(
repository.login(userName, password)
.onSuccess {
_state.update { it.copy(isLoading = false, isAuthenticated = true) }
updatePushToken()
}
.onFailure { e ->
_state.update { it.copy(isLoading = false, error = e.message) }
@@ -48,10 +50,22 @@ class AuthViewModel @Inject constructor(
repository.register(userName, password)
.onSuccess {
_state.update { it.copy(isLoading = false, isAuthenticated = true) }
updatePushToken()
}
.onFailure { e ->
_state.update { it.copy(isLoading = false, error = e.message) }
}
}
}
private fun updatePushToken() {
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
val token = task.result
viewModelScope.launch {
repository.updatePushToken(token)
}
}
}
}
}

View File

@@ -7,24 +7,36 @@ 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 kotlinx.coroutines.flow.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.delay
enum class ConnectionStatus { CONNECTED, CONNECTING, DISCONNECTED }
@Singleton
class ChatHubClient @Inject constructor() {
private var hubConnection: HubConnection? = null
private val _events = MutableSharedFlow<ChatEvent>(extraBufferCapacity = 64)
val events: SharedFlow<ChatEvent> = _events.asSharedFlow()
private val _status = MutableStateFlow(ConnectionStatus.DISCONNECTED)
val status: StateFlow<ConnectionStatus> = _status.asStateFlow()
private val scope = CoroutineScope(Dispatchers.IO)
private var lastBaseUrl: String? = null
private var lastToken: String? = null
fun connect(baseUrl: String, accessToken: String) {
if (hubConnection?.connectionState == HubConnectionState.CONNECTED) return
lastBaseUrl = baseUrl
lastToken = accessToken
_status.value = ConnectionStatus.CONNECTING
hubConnection = HubConnectionBuilder.create("${baseUrl}/chatHub")
.withAccessTokenProvider(Single.just(accessToken))
@@ -33,16 +45,22 @@ class ChatHubClient @Inject constructor() {
setupHandlers()
hubConnection?.onClosed { exception ->
Log.e("ChatHubClient", "Connection closed", exception)
// Optional: Reconnect logic
Log.e("ChatHubClient", "Connection closed. Reconnecting...", exception)
_status.value = ConnectionStatus.DISCONNECTED
scope.launch {
delay(5000)
connect(baseUrl, accessToken)
}
}
scope.launch {
try {
hubConnection?.start()?.blockingAwait()
_status.value = ConnectionStatus.CONNECTED
Log.d("ChatHubClient", "SignalR Connected")
} catch (e: Exception) {
Log.e("ChatHubClient", "SignalR Connection Error", e)
_status.value = ConnectionStatus.DISCONNECTED
}
}
}
@@ -94,5 +112,6 @@ class ChatHubClient @Inject constructor() {
fun disconnect() {
hubConnection?.stop()
_status.value = ConnectionStatus.DISCONNECTED
}
}

View File

@@ -32,8 +32,17 @@ class ChatRepositoryImpl @Inject constructor(
return api.getMessages(chatId).map { it.toDomain(baseUrl) }
}
override suspend fun sendMessage(chatId: String, content: String): Message {
val request = SendMessageRequest(content = content, type = "text")
override suspend fun sendMessage(
chatId: String,
content: String?,
type: String,
attachments: List<chats.data.remote.api.AttachmentRequest>?
): Message {
val request = SendMessageRequest(
content = content,
type = type,
attachments = attachments
)
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
return api.sendMessage(chatId, request).toDomain(baseUrl)
}
@@ -91,6 +100,7 @@ fun ChatDto.toDomain(currentUserId: String, baseUrl: String): Chat {
fun MessageDto.toDomain(baseUrl: String): Message {
val domainMediaType = when (type) {
"gif" -> MediaType.GIF
"image", "photo" -> MediaType.IMAGE
"video" -> MediaType.VIDEO
"audio", "voice" -> MediaType.AUDIO

View File

@@ -26,5 +26,5 @@ data class Media(
)
enum class MediaType {
TEXT, IMAGE, VIDEO, AUDIO, FILE, STORY_REPLY
TEXT, IMAGE, VIDEO, AUDIO, FILE, STORY_REPLY, GIF
}

View File

@@ -6,7 +6,12 @@ import chats.domain.model.Message
interface ChatRepository {
suspend fun getChats(): List<Chat>
suspend fun getMessages(chatId: String): List<Message>
suspend fun sendMessage(chatId: String, content: String): Message
suspend fun sendMessage(
chatId: String,
content: String?,
type: String = "text",
attachments: List<chats.data.remote.api.AttachmentRequest>? = null
): Message
suspend fun addReaction(messageId: String, emoji: String)
suspend fun sendTypingStatus(chatId: String)
suspend fun markMessagesAsRead(chatId: String, lastMessageId: String)

View File

@@ -65,6 +65,14 @@ class ChatDetailViewModel @Inject constructor(
fun setChatId(chatId: String) {
currentChatId = chatId
// Ensure SignalR is connected
val token = tokenManager.getToken()
if (token != null) {
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
signalrClient.connect(baseUrl, token)
}
loadChatInfo(chatId)
loadMessages(chatId)
observeSignalREvents()
@@ -155,9 +163,17 @@ class ChatDetailViewModel @Inject constructor(
fun sendMessage(text: String) {
val chatId = currentChatId ?: return
if (text.isBlank()) return
viewModelScope.launch {
try {
repository.sendMessage(chatId, text)
val sentMessage = repository.sendMessage(chatId, text)
// Optimistic update if not already there
_state.update { s ->
if (s.messages.none { it.id == sentMessage.id }) {
s.copy(messages = s.messages + sentMessage)
} else s
}
} catch (e: Exception) {
_state.update { it.copy(error = e.localizedMessage) }
}
@@ -239,8 +255,21 @@ class ChatDetailViewModel @Inject constructor(
val chatId = currentChatId ?: return
viewModelScope.launch {
try {
repository.sendMessage(chatId, url)
val attachment = chats.data.remote.api.AttachmentRequest(
type = "image",
url = url,
fileName = "gif.gif",
fileSize = 0
)
val sentMessage = repository.sendMessage(chatId, null, "image", listOf(attachment))
// Optimistic update
_state.update { s ->
if (s.messages.none { it.id == sentMessage.id }) {
s.copy(messages = s.messages + sentMessage)
} else s
}
// Add to recent
val allGifs = _state.value.trendingGifs + _state.value.searchedGifs + _state.value.recentGifs
val selectedGif = allGifs.find { gif ->

View File

@@ -142,13 +142,29 @@ fun MessageBubble(
}
}
// Media Content
if (message.media.isNotEmpty()) {
// GIF Content (Klipy)
if (message.mediaType == MediaType.GIF) {
AsyncImage(
model = message.content,
imageLoader = core.utils.CoilUtils.getGifImageLoader(context),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 300.dp)
.clip(RoundedCornerShape(8.dp))
.clickable { /* Handle click */ },
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(4.dp))
}
// Media Content (Attachments)
if (message.media.isNotEmpty() && message.mediaType != MediaType.GIF) {
val mediaCount = message.media.size
if (mediaCount == 1) {
val mediaItem = message.media[0]
when {
mediaItem.type.startsWith("image") || message.mediaType == MediaType.IMAGE || mediaItem.type.startsWith("image") -> {
mediaItem.type.startsWith("image") || message.mediaType == MediaType.IMAGE -> {
AsyncImage(
model = mediaItem.url,
imageLoader = core.utils.CoilUtils.getGifImageLoader(context),
@@ -218,7 +234,7 @@ fun MessageBubble(
}
// Text Content
if (!message.content.isNullOrBlank() && !isVoiceMessage) {
if (!message.content.isNullOrBlank() && !isVoiceMessage && message.mediaType != MediaType.GIF) {
Text(
text = message.content,
style = MaterialTheme.typography.bodyMedium,