Отправка гиф
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ interface AuthRepository {
|
||||
suspend fun logout()
|
||||
suspend fun fetchConfig(): Result<Unit>
|
||||
fun isAuthenticated(): Boolean
|
||||
suspend fun updatePushToken(token: String)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user