Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/src/main/java/com/nextcloud/talk/api/NcApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,8 @@ Observable<GenericOverall> createRemoteShare(@Nullable @Header("Authorization")
@Field("path") String remotePath,
@Field("shareWith") String roomToken,
@Field("shareType") String shareType,
@Field("talkMetaData") String talkMetaData);
@Field("talkMetaData") String talkMetaData,
@Field("referenceId") String referenceId);

@FormUrlEncoded
@PUT
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ import com.nextcloud.talk.ui.StatusDrawable
import com.nextcloud.talk.ui.chat.ChatView
import com.nextcloud.talk.ui.chat.ChatViewCallbacks
import com.nextcloud.talk.ui.chat.ChatViewState
import com.nextcloud.talk.ui.chat.LocalUploadProgressProvider
import com.nextcloud.talk.ui.dialog.DateTimeCompose
import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
import com.nextcloud.talk.ui.dialog.GetPinnedOptionsDialog
Expand Down Expand Up @@ -605,10 +606,13 @@ class ChatActivity :
val listState = rememberLazyListState()
SideEffect { chatListState = listState }

val uploadProgressMap by chatViewModel.uploadProgressMap.collectAsStateWithLifecycle()

CompositionLocalProvider(
LocalViewThemeUtils provides viewThemeUtils,
LocalMessageUtils provides messageUtils,
LocalOpenGraphFetcher provides { url -> chatViewModel.fetchOpenGraph(url) }
LocalOpenGraphFetcher provides { url -> chatViewModel.fetchOpenGraph(url) },
LocalUploadProgressProvider provides { refId -> uploadProgressMap[refId] }
) {
val isOneToOneConversation = uiState.isOneToOneConversation
Log.d(TAG, "isOneToOneConversation=" + isOneToOneConversation)
Expand All @@ -635,7 +639,8 @@ class ChatActivity :
onReactionClick = { messageId, emoji -> handleReactionClick(messageId, emoji) },
onReactionLongClick = { messageId -> openReactionsDialog(messageId) },
onOpenThreadClick = { messageId -> openThread(messageId.toLong()) },
onLoadQuotedMessageClick = { messageId -> onLoadQuotedMessage(messageId) }
onLoadQuotedMessageClick = { messageId -> onLoadQuotedMessage(messageId) },
onCancelUpload = { referenceId -> chatViewModel.cancelUpload(referenceId) }
),
listState = listState
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ interface ChatMessageRepository : LifecycleAwareManager {
referenceId: String
): Flow<Result<ChatMessage?>>

@Suppress("LongParameterList")
suspend fun addUploadPlaceholderMessage(
localFileUri: String,
caption: String,
mimeType: String?,
fileSize: Long,
referenceId: String
): Flow<Result<ChatMessage?>>

suspend fun deleteTempMessageByReferenceId(referenceId: String)

suspend fun editChatMessage(credentials: String, url: String, text: String): Flow<Result<ChatOverallSingleMessage>>

suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow<Boolean>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,77 @@ class OfflineFirstChatRepository @Inject constructor(
}
}

@Suppress("Detekt.TooGenericExceptionCaught", "LongMethod")
override suspend fun addUploadPlaceholderMessage(
localFileUri: String,
caption: String,
mimeType: String?,
fileSize: Long,
referenceId: String
): Flow<Result<ChatMessage?>> =
flow {
try {
val currentTimeMillis = System.currentTimeMillis()

// Use the first 15 hex chars so the value always fits in a signed Long.
// Use referenceId.hashCode() as the placeholder id so that:
// 1. It is unique per file even when multiple files are selected simultaneously
// 2. It fits in an Int, so it survives the Long→Int cast in ChatMessageUi.id without
// truncation, keeping DB lookups consistent when the message is tapped.
// 3. It is always positive, because getMessagesEqualOrNewerThan expects it to be larger
// than oldestMessageId
@Suppress("MagicNumber")
val placeholderId = (referenceId.hashCode().toLong() and 0x7FFF_FFFFL)

Log.d(TAG, "addUploadPlaceholderMessage: referenceId=$referenceId " +
"placeholderId=$placeholderId caption=$caption")

val fileParams = hashMapOf<String?, String?>(
"type" to "file",
"name" to caption,
"mimetype" to (mimeType ?: ""),
"size" to fileSize.toString(),
"path" to localFileUri
)
val messageParameters = hashMapOf<String?, HashMap<String?, String?>>(
"file" to fileParams
)

val entity = ChatMessageEntity(
internalId = "$internalConversationId@_temp_$referenceId",
internalConversationId = internalConversationId,
id = placeholderId,
threadId = threadId,
message = "{file}",
deleted = false,
token = conversationModel.token,
actorId = currentUser.userId!!,
actorType = EnumActorTypeConverter().convertToString(Participant.ActorType.USERS),
accountId = currentUser.id!!,
messageParameters = messageParameters,
messageType = "comment",
parentMessageId = null,
systemMessageType = ChatMessage.SystemMessageType.DUMMY,
replyable = false,
timestamp = currentTimeMillis / MILLIES,
expirationTimestamp = 0,
actorDisplayName = currentUser.displayName!!,
referenceId = referenceId,
isTemporary = true,
sendStatus = SendStatus.PENDING,
silent = false
)
chatDao.upsertChatMessage(entity)
} catch (e: Exception) {
Log.e(TAG, "addUploadPlaceholderMessage failed for referenceId=$referenceId", e)
emit(Result.failure(e))
}
}

override suspend fun deleteTempMessageByReferenceId(referenceId: String) {
chatDao.deleteTempChatMessages(internalConversationId, listOf(referenceId))
}

@Suppress("Detekt.TooGenericExceptionCaught")
override suspend fun editChatMessage(
credentials: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ data class ChatMessageUi(
val reactions: List<MessageReactionUi> = emptyList(),
val isEdited: Boolean = false,
val parentMessage: ChatMessageUi? = null,
val replyable: Boolean = false
val replyable: Boolean = false,
val referenceId: String? = null
)

data class MessageReactionUi(val emoji: String, val amount: Int, val isSelfReaction: Boolean)
Expand All @@ -56,6 +57,13 @@ sealed interface MessageTypeContent {

data class Media(val previewUrl: String?, val drawableResourceId: Int) : MessageTypeContent

data class UploadingMedia(
val localFileUri: String,
val caption: String,
val mimeType: String?,
val drawableResourceId: Int
) : MessageTypeContent

data class Geolocation(val id: String, val name: String, val lat: Double, val lon: Double) : MessageTypeContent

data class Poll(val pollId: String, val pollName: String) : MessageTypeContent
Expand Down Expand Up @@ -125,7 +133,8 @@ fun ChatMessage.toUiModel(
lastCommonReadMessageId = 0,
parentMessage = null
),
replyable = replyable
replyable = replyable,
referenceId = referenceId
)

private fun ChatMessage.normalizeMessageParameters(): Map<String, Map<String, String>> =
Expand Down Expand Up @@ -173,6 +182,8 @@ fun resolveStatusIcon(
): MessageStatusIcon {
val status = if (sendStatus == SendStatus.FAILED) {
MessageStatusIcon.FAILED
} else if (isTemporary && sendStatus == SendStatus.SENT_PENDING_ACK) {
MessageStatusIcon.SENT
} else if (isTemporary) {
MessageStatusIcon.SENDING
} else if (jsonMessageId <= lastCommonReadMessageId) {
Expand All @@ -188,6 +199,8 @@ fun getMessageTypeContent(user: User, message: ChatMessage): MessageTypeContent?
MessageTypeContent.SystemMessage
} else if (message.isVoiceMessage) {
getVoiceContent(message)
} else if (message.hasFileAttachment && message.isTemporary) {
getUploadingMediaContent(message)
} else if (message.hasFileAttachment) {
getMediaContent(user, message)
} else if (message.hasGeoLocation) {
Expand All @@ -202,6 +215,17 @@ fun getMessageTypeContent(user: User, message: ChatMessage): MessageTypeContent?
?: MessageTypeContent.RegularText
}

fun getUploadingMediaContent(message: ChatMessage): MessageTypeContent.UploadingMedia {
val mimetype = message.fileParameters.mimetype
val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(mimetype)
return MessageTypeContent.UploadingMedia(
localFileUri = message.fileParameters.path.orEmpty(),
caption = message.fileParameters.name.orEmpty(),
mimeType = mimetype.takeIf { !it.isNullOrEmpty() },
drawableResourceId = drawableResourceId
)
}

fun getMediaContent(user: User, message: ChatMessage): MessageTypeContent.Media {
val previewUrl = getPreviewImageUrl(user, message)
val mimetype = message.fileParameters.mimetype
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package com.nextcloud.talk.chat.viewmodels
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.util.Log
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
Expand All @@ -35,6 +36,9 @@ import com.nextcloud.talk.data.database.mappers.toDomainModel
import com.nextcloud.talk.data.database.model.ChatMessageEntity
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.extensions.toIntOrZero
import androidx.lifecycle.asFlow
import androidx.work.WorkManager
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
import com.nextcloud.talk.models.MessageDraft
import com.nextcloud.talk.models.domain.ConversationModel
Expand Down Expand Up @@ -100,7 +104,9 @@ import retrofit2.HttpException
import java.io.File
import java.io.IOException
import java.time.LocalDate
import java.util.UUID
import javax.inject.Inject
import androidx.core.net.toUri

@Suppress("TooManyFunctions", "LongParameterList")
class ChatViewModel @AssistedInject constructor(
Expand Down Expand Up @@ -145,6 +151,21 @@ class ChatViewModel @AssistedInject constructor(
var hiddenUpcomingEvent: String? = null
lateinit var participantPermissions: ParticipantPermissions

private val _uploadProgressMap = MutableStateFlow<Map<String, Int>>(emptyMap())
val uploadProgressMap: StateFlow<Map<String, Int>> = _uploadProgressMap

// Maps referenceId -> fileUri for cancellation support
private val uploadReferenceToUri = mutableMapOf<String, String>()

fun cancelUpload(referenceId: String) {
val fileUri = uploadReferenceToUri.remove(referenceId) ?: return
WorkManager.getInstance(NextcloudTalkApplication.sharedApplication!!).cancelUniqueWork(fileUri)
viewModelScope.launch {
chatRepository.deleteTempMessageByReferenceId(referenceId)
}
_uploadProgressMap.update { it - referenceId }
}

fun getChatRepository(): ChatMessageRepository = chatRepository

override fun onResume(owner: LifecycleOwner) {
Expand Down Expand Up @@ -1392,23 +1413,82 @@ class ChatViewModel @AssistedInject constructor(
metaDataMap["caption"] = caption
}

val referenceId = UUID.randomUUID().toString().replace("-", "")
metaDataMap["referenceId"] = referenceId

val metaData = Gson().toJson(metaDataMap)

room = if (roomToken == "") chatRoomToken else roomToken

try {
require(fileUri.isNotEmpty())
UploadAndShareFilesWorker.upload(

if (!isVoiceMessage) {
val (fileName, mimeType, fileSize) = resolveFileInfo(fileUri)
viewModelScope.launch {
chatRepository.addUploadPlaceholderMessage(
localFileUri = fileUri,
caption = caption.ifEmpty { fileName },
mimeType = mimeType,
fileSize = fileSize,
referenceId = referenceId
).collect {}
}
}

val internalConversationId = "${currentUser.id}@$chatRoomToken"
val workerId = UploadAndShareFilesWorker.upload(
fileUri,
room,
displayName,
metaData
metaData,
referenceId,
internalConversationId
)

if (!isVoiceMessage) {
uploadReferenceToUri[referenceId] = fileUri
observeUploadProgress(workerId, referenceId)
}
} catch (e: IllegalArgumentException) {
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
}
}

private fun resolveFileInfo(fileUri: String): Triple<String, String?, Long> {
val uri = fileUri.toUri()
val mimeType = NextcloudTalkApplication.sharedApplication!!.contentResolver.getType(uri)
val cursor = NextcloudTalkApplication.sharedApplication!!.contentResolver.query(uri, null, null, null, null)
cursor?.use {
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE)
if (it.moveToFirst()) {
val name = if (nameIndex >= 0) it.getString(nameIndex).orEmpty() else uri.lastPathSegment.orEmpty()
val size = if (sizeIndex >= 0) it.getLong(sizeIndex) else 0L
return Triple(name, mimeType, size)
}
}
return Triple(uri.lastPathSegment.orEmpty(), mimeType, 0L)
}

private fun observeUploadProgress(workerId: UUID, referenceId: String) {
WorkManager.getInstance(NextcloudTalkApplication.sharedApplication!!)
.getWorkInfoByIdLiveData(workerId)
.asFlow()
.onEach { workInfo ->
if (workInfo == null) return@onEach
val progress = workInfo.progress.getInt(UploadAndShareFilesWorker.PROGRESS_KEY, -1)
if (progress >= 0) {
_uploadProgressMap.update { it + (referenceId to progress) }
}
if (workInfo.state.isFinished) {
_uploadProgressMap.update { it - referenceId }
uploadReferenceToUri.remove(referenceId)
}
}
.launchIn(viewModelScope)
}

fun postToRecordTouchObserver(float: Float) {
_recordTouchObserver.postValue(float)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ class ShareOperationWorker(context: Context, workerParams: WorkerParameters) : W
filePath,
roomToken,
"10",
metaData
metaData,
"" // no reference id
)
.subscribeOn(Schedulers.io())
.blockingSubscribe(
Expand Down
Loading
Loading