Отправка гиф
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)
|
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 logout()
|
||||||
suspend fun fetchConfig(): Result<Unit>
|
suspend fun fetchConfig(): Result<Unit>
|
||||||
fun isAuthenticated(): Boolean
|
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.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class AuthState(
|
data class AuthState(
|
||||||
@@ -35,6 +36,7 @@ class AuthViewModel @Inject constructor(
|
|||||||
repository.login(userName, password)
|
repository.login(userName, password)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
_state.update { it.copy(isLoading = false, isAuthenticated = true) }
|
_state.update { it.copy(isLoading = false, isAuthenticated = true) }
|
||||||
|
updatePushToken()
|
||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
_state.update { it.copy(isLoading = false, error = e.message) }
|
_state.update { it.copy(isLoading = false, error = e.message) }
|
||||||
@@ -48,10 +50,22 @@ class AuthViewModel @Inject constructor(
|
|||||||
repository.register(userName, password)
|
repository.register(userName, password)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
_state.update { it.copy(isLoading = false, isAuthenticated = true) }
|
_state.update { it.copy(isLoading = false, isAuthenticated = true) }
|
||||||
|
updatePushToken()
|
||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
_state.update { it.copy(isLoading = false, error = e.message) }
|
_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,25 +7,37 @@ import com.microsoft.signalr.HubConnectionBuilder
|
|||||||
import com.microsoft.signalr.HubConnectionState
|
import com.microsoft.signalr.HubConnectionState
|
||||||
import chats.data.remote.dto.ChatDto
|
import chats.data.remote.dto.ChatDto
|
||||||
import chats.data.remote.dto.MessageDto
|
import chats.data.remote.dto.MessageDto
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
enum class ConnectionStatus { CONNECTED, CONNECTING, DISCONNECTED }
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ChatHubClient @Inject constructor() {
|
class ChatHubClient @Inject constructor() {
|
||||||
private var hubConnection: HubConnection? = null
|
private var hubConnection: HubConnection? = null
|
||||||
private val _events = MutableSharedFlow<ChatEvent>(extraBufferCapacity = 64)
|
private val _events = MutableSharedFlow<ChatEvent>(extraBufferCapacity = 64)
|
||||||
val events: SharedFlow<ChatEvent> = _events.asSharedFlow()
|
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 val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
private var lastBaseUrl: String? = null
|
||||||
|
private var lastToken: String? = null
|
||||||
|
|
||||||
fun connect(baseUrl: String, accessToken: String) {
|
fun connect(baseUrl: String, accessToken: String) {
|
||||||
if (hubConnection?.connectionState == HubConnectionState.CONNECTED) return
|
if (hubConnection?.connectionState == HubConnectionState.CONNECTED) return
|
||||||
|
|
||||||
|
lastBaseUrl = baseUrl
|
||||||
|
lastToken = accessToken
|
||||||
|
_status.value = ConnectionStatus.CONNECTING
|
||||||
|
|
||||||
hubConnection = HubConnectionBuilder.create("${baseUrl}/chatHub")
|
hubConnection = HubConnectionBuilder.create("${baseUrl}/chatHub")
|
||||||
.withAccessTokenProvider(Single.just(accessToken))
|
.withAccessTokenProvider(Single.just(accessToken))
|
||||||
.build()
|
.build()
|
||||||
@@ -33,16 +45,22 @@ class ChatHubClient @Inject constructor() {
|
|||||||
setupHandlers()
|
setupHandlers()
|
||||||
|
|
||||||
hubConnection?.onClosed { exception ->
|
hubConnection?.onClosed { exception ->
|
||||||
Log.e("ChatHubClient", "Connection closed", exception)
|
Log.e("ChatHubClient", "Connection closed. Reconnecting...", exception)
|
||||||
// Optional: Reconnect logic
|
_status.value = ConnectionStatus.DISCONNECTED
|
||||||
|
scope.launch {
|
||||||
|
delay(5000)
|
||||||
|
connect(baseUrl, accessToken)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
hubConnection?.start()?.blockingAwait()
|
hubConnection?.start()?.blockingAwait()
|
||||||
|
_status.value = ConnectionStatus.CONNECTED
|
||||||
Log.d("ChatHubClient", "SignalR Connected")
|
Log.d("ChatHubClient", "SignalR Connected")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ChatHubClient", "SignalR Connection Error", e)
|
Log.e("ChatHubClient", "SignalR Connection Error", e)
|
||||||
|
_status.value = ConnectionStatus.DISCONNECTED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,5 +112,6 @@ class ChatHubClient @Inject constructor() {
|
|||||||
|
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
hubConnection?.stop()
|
hubConnection?.stop()
|
||||||
|
_status.value = ConnectionStatus.DISCONNECTED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,17 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
return api.getMessages(chatId).map { it.toDomain(baseUrl) }
|
return api.getMessages(chatId).map { it.toDomain(baseUrl) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendMessage(chatId: String, content: String): Message {
|
override suspend fun sendMessage(
|
||||||
val request = SendMessageRequest(content = content, type = "text")
|
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/")
|
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
||||||
return api.sendMessage(chatId, request).toDomain(baseUrl)
|
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 {
|
fun MessageDto.toDomain(baseUrl: String): Message {
|
||||||
val domainMediaType = when (type) {
|
val domainMediaType = when (type) {
|
||||||
|
"gif" -> MediaType.GIF
|
||||||
"image", "photo" -> MediaType.IMAGE
|
"image", "photo" -> MediaType.IMAGE
|
||||||
"video" -> MediaType.VIDEO
|
"video" -> MediaType.VIDEO
|
||||||
"audio", "voice" -> MediaType.AUDIO
|
"audio", "voice" -> MediaType.AUDIO
|
||||||
|
|||||||
@@ -26,5 +26,5 @@ data class Media(
|
|||||||
)
|
)
|
||||||
|
|
||||||
enum class MediaType {
|
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 {
|
interface ChatRepository {
|
||||||
suspend fun getChats(): List<Chat>
|
suspend fun getChats(): List<Chat>
|
||||||
suspend fun getMessages(chatId: String): List<Message>
|
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 addReaction(messageId: String, emoji: String)
|
||||||
suspend fun sendTypingStatus(chatId: String)
|
suspend fun sendTypingStatus(chatId: String)
|
||||||
suspend fun markMessagesAsRead(chatId: String, lastMessageId: String)
|
suspend fun markMessagesAsRead(chatId: String, lastMessageId: String)
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ class ChatDetailViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun setChatId(chatId: String) {
|
fun setChatId(chatId: String) {
|
||||||
currentChatId = chatId
|
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)
|
loadChatInfo(chatId)
|
||||||
loadMessages(chatId)
|
loadMessages(chatId)
|
||||||
observeSignalREvents()
|
observeSignalREvents()
|
||||||
@@ -155,9 +163,17 @@ class ChatDetailViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun sendMessage(text: String) {
|
fun sendMessage(text: String) {
|
||||||
val chatId = currentChatId ?: return
|
val chatId = currentChatId ?: return
|
||||||
|
if (text.isBlank()) return
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
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) {
|
} catch (e: Exception) {
|
||||||
_state.update { it.copy(error = e.localizedMessage) }
|
_state.update { it.copy(error = e.localizedMessage) }
|
||||||
}
|
}
|
||||||
@@ -239,7 +255,20 @@ class ChatDetailViewModel @Inject constructor(
|
|||||||
val chatId = currentChatId ?: return
|
val chatId = currentChatId ?: return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
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
|
// Add to recent
|
||||||
val allGifs = _state.value.trendingGifs + _state.value.searchedGifs + _state.value.recentGifs
|
val allGifs = _state.value.trendingGifs + _state.value.searchedGifs + _state.value.recentGifs
|
||||||
|
|||||||
@@ -142,13 +142,29 @@ fun MessageBubble(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media Content
|
// GIF Content (Klipy)
|
||||||
if (message.media.isNotEmpty()) {
|
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
|
val mediaCount = message.media.size
|
||||||
if (mediaCount == 1) {
|
if (mediaCount == 1) {
|
||||||
val mediaItem = message.media[0]
|
val mediaItem = message.media[0]
|
||||||
when {
|
when {
|
||||||
mediaItem.type.startsWith("image") || message.mediaType == MediaType.IMAGE || mediaItem.type.startsWith("image") -> {
|
mediaItem.type.startsWith("image") || message.mediaType == MediaType.IMAGE -> {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = mediaItem.url,
|
model = mediaItem.url,
|
||||||
imageLoader = core.utils.CoilUtils.getGifImageLoader(context),
|
imageLoader = core.utils.CoilUtils.getGifImageLoader(context),
|
||||||
@@ -218,7 +234,7 @@ fun MessageBubble(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Text Content
|
// Text Content
|
||||||
if (!message.content.isNullOrBlank() && !isVoiceMessage) {
|
if (!message.content.isNullOrBlank() && !isVoiceMessage && message.mediaType != MediaType.GIF) {
|
||||||
Text(
|
Text(
|
||||||
text = message.content,
|
text = message.content,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
|||||||
Reference in New Issue
Block a user