From d45277beaf092c1365a61a2e52749081fec77632 Mon Sep 17 00:00:00 2001 From: Julius Linus Date: Mon, 22 Jan 2024 11:08:36 -0600 Subject: [PATCH] Impl "Add to Notes Action" - Note to self option should only appear if conversation is available - Added ic_edit_note_24_xml - Implemented MVVM functions for cleaner data flow - Added the option to the XML - Works for Voice Messages - Works for Files(and Gifs) + captions - Works for GeoLocation Messages - Added SnackBar Signed-off-by: Julius Linus --- .../com/nextcloud/talk/chat/ChatActivity.kt | 83 ++++++++++++++++- .../talk/chat/data/ChatRepository.kt | 15 +++ .../talk/chat/data/ChatRepositoryImpl.kt | 37 ++++++++ .../talk/chat/viewmodels/ChatViewModel.kt | 91 +++++++++++++++++++ .../talk/jobs/UploadAndShareFilesWorker.kt | 2 +- .../talk/ui/dialog/MessageActionsDialog.kt | 38 ++++++++ app/src/main/res/drawable/ic_edit_note_24.xml | 21 +++++ .../res/layout/dialog_message_actions.xml | 33 +++++++ app/src/main/res/values/strings.xml | 1 + 9 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/drawable/ic_edit_note_24.xml diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 3ace986ef8..3cd1013b57 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -2682,8 +2682,9 @@ class ChatActivity : } } - private fun uploadFile(fileUri: String, isVoiceMessage: Boolean, caption: String = "") { + private fun uploadFile(fileUri: String, isVoiceMessage: Boolean, caption: String = "", token: String = "") { var metaData = "" + var room = "" if (!participantPermissions.hasChatPermission()) { Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions") @@ -2698,11 +2699,13 @@ class ChatActivity : metaData = "{\"caption\":\"$caption\"}" } + if (token == "") room = roomToken else room = token + try { require(fileUri.isNotEmpty()) UploadAndShareFilesWorker.upload( fileUri, - roomToken, + room, currentConversation?.displayName!!, metaData ) @@ -4171,6 +4174,82 @@ class ChatActivity : } } + fun shareToNotes(message: ChatMessage, roomToken: String) { + val apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1)) + val type = message.getCalculateMessageType() + var shareUri: Uri? = null + var data: HashMap? + var metaData: String = "" + var objectId: String = "" + if (message.hasFileAttachment()) { + val filename = message.selectedIndividualHashMap!!["name"] + path = applicationContext.cacheDir.absolutePath + "/" + filename + shareUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID, + File(path) + ) + + this.grantUriPermission( + applicationContext.packageName, + shareUri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } else if (message.hasGeoLocation()) { + data = message.messageParameters?.get("object") + objectId = data?.get("id")!! + val name = data.get("name")!! + val lat = data.get("latitude")!! + val lon = data.get("longitude")!! + metaData = + "{\"type\":\"geo-location\",\"id\":\"geo:$lat,$lon\",\"latitude\":\"$lat\"," + + "\"longitude\":\"$lon\",\"name\":\"$name\"}" + } + + when (type) { + ChatMessage.MessageType.VOICE_MESSAGE -> { + uploadFile(shareUri.toString(), true, token = roomToken) + Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show() + } + ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> { + val caption = if (message.message != "{file}") message.message else "" + if (null != shareUri) { + try { + context.contentResolver.openInputStream(shareUri)?.close() + uploadFile(shareUri.toString(), false, caption!!, roomToken) + Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show() + } catch (e: java.lang.Exception) { + Log.w(TAG, "File corresponding to the uri does not exist " + shareUri.toString()) + downloadFileToCache(message, false) { + uploadFile(shareUri.toString(), false, caption!!, roomToken) + Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show() + } + } + } + } + ChatMessage.MessageType.SINGLE_NC_GEOLOCATION_MESSAGE -> { + chatViewModel.shareLocationToNotes( + credentials!!, + ApiUtils.getUrlToSendLocation(apiVersion, conversationUser!!.baseUrl, roomToken), + "geo-location", + objectId, + metaData + ) + Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show() + } + ChatMessage.MessageType.REGULAR_TEXT_MESSAGE -> { + chatViewModel.shareToNotes( + credentials!!, + ApiUtils.getUrlForChat(apiVersion, conversationUser!!.baseUrl, roomToken), + message.message!!, + conversationUser!!.displayName!! + ) + Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show() + } + else -> {} + } + } + fun openInFilesApp(message: ChatMessage) { val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID] val link = message.selectedIndividualHashMap!!["link"] diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt index 7c85caa158..1cd1860d79 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt @@ -22,6 +22,7 @@ package com.nextcloud.talk.chat.data import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.reminder.Reminder import io.reactivex.Observable @@ -32,4 +33,18 @@ interface ChatRepository { fun setReminder(user: User, roomToken: String, messageId: String, timeStamp: Int): Observable fun getReminder(user: User, roomToken: String, messageId: String): Observable fun deleteReminder(user: User, roomToken: String, messageId: String): Observable + fun shareToNotes( + credentials: String, + url: String, + message: String, + displayName: String + ): Observable // last two fields are false + fun checkForNoteToSelf(credentials: String, url: String, includeStatus: Boolean): Observable + fun shareLocationToNotes( + credentials: String, + url: String, + objectType: String, + objectId: String, + metadata: String + ): Observable } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt index 488aaece3b..5a2d22485b 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt @@ -23,6 +23,7 @@ package com.nextcloud.talk.chat.data import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.utils.ApiUtils @@ -83,4 +84,40 @@ class ChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository { it } } + + override fun shareToNotes( + credentials: String, + url: String, + message: String, + displayName: String + ): Observable { + return ncApi.sendChatMessage( + credentials, + url, + message, + displayName, + null, + false + ).map { + it + } + } + + override fun checkForNoteToSelf( + credentials: String, + url: String, + includeStatus: Boolean + ): Observable { + return ncApi.getRooms(credentials, url, includeStatus).map { it } + } + + override fun shareLocationToNotes( + credentials: String, + url: String, + objectType: String, + objectId: String, + metadata: String + ): Observable { + return ncApi.sendLocation(credentials, url, objectType, objectId, metadata).map { it } + } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index a6d9db80fe..d285f112af 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -27,8 +27,10 @@ import androidx.lifecycle.ViewModel import com.nextcloud.talk.chat.data.ChatRepository import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.reminder.Reminder +import com.nextcloud.talk.utils.ConversationUtils import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable @@ -49,6 +51,13 @@ class ChatViewModel @Inject constructor(private val repository: ChatRepository) val getReminderExistState: LiveData get() = _getReminderExistState + object NoteToSelfNotAvaliableState : ViewState + open class NoteToSelfAvaliableState(val roomToken: String) : ViewState + + private val _getNoteToSelfAvaliability: MutableLiveData = MutableLiveData(NoteToSelfNotAvaliableState) + val getNoteToSelfAvaliability: LiveData + get() = _getNoteToSelfAvaliability + open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState private val _getRoomViewState: MutableLiveData = MutableLiveData(GetRoomStartState) @@ -117,6 +126,58 @@ class ChatViewModel @Inject constructor(private val repository: ChatRepository) }) } + fun shareToNotes(credentials: String, url: String, message: String, displayName: String) { + repository.shareToNotes(credentials, url, message, displayName) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + // unused atm + } + + override fun onError(e: Throwable) { + Log.d(TAG, "Error when sharing to notes $e") + } + + override fun onComplete() { + // unused atm + } + }) + } + + fun checkForNoteToSelf(credentials: String, baseUrl: String, includeStatus: Boolean) { + repository.checkForNoteToSelf(credentials, baseUrl, includeStatus).subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(CheckForNoteToSelfObserver()) + } + + fun shareLocationToNotes(credentials: String, url: String, objectType: String, objectId: String, metadata: String) { + repository.shareLocationToNotes(credentials, url, objectType, objectId, metadata) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + // unused atm + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when sharing location to notes $e") + } + + override fun onComplete() { + // unused atm + } + }) + } + inner class GetRoomObserver : Observer { override fun onSubscribe(d: Disposable) { // unused atm @@ -192,6 +253,36 @@ class ChatViewModel @Inject constructor(private val repository: ChatRepository) } } + inner class CheckForNoteToSelfObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(roomsOverall: RoomsOverall) { + val rooms = roomsOverall.ocs?.data + rooms?.let { + try { + val noteToSelf = rooms.first { + val model = ConversationModel.mapToConversationModel(it) + ConversationUtils.isNoteToSelfConversation(model) + } + _getNoteToSelfAvaliability.value = NoteToSelfAvaliableState(noteToSelf.token!!) + } catch (e: NoSuchElementException) { + _getNoteToSelfAvaliability.value = NoteToSelfNotAvaliableState + Log.e(TAG, "Note to self not found $e") + } + } + } + + override fun onError(e: Throwable) { + Log.d(TAG, "Error when getting rooms for Note to Self Observer $e") + } + + override fun onComplete() { + // unused atm + } + } + companion object { private val TAG = ChatViewModel::class.simpleName const val JOIN_ROOM_RETRY_COUNT: Long = 3 diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt index ee24bd0992..404352225f 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt @@ -152,7 +152,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa remotePath ) } else { - Log.d(TAG, "starting normal upload (not chunked)") + Log.d(TAG, "starting normal upload (not chunked) of $fileName") uploadSuccess = FileUploader( context, diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt index 7414a36aed..16023ca156 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt @@ -38,6 +38,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.viewmodels.ChatViewModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.DialogMessageActionsBinding import com.nextcloud.talk.models.domain.ConversationModel @@ -48,6 +49,8 @@ import com.nextcloud.talk.models.domain.ReactionDeletedModel import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew import com.vanniktech.emoji.EmojiPopup import com.vanniktech.emoji.EmojiTextView @@ -96,6 +99,31 @@ class MessageActionsDialog( viewThemeUtils.platform.themeDialog(dialogMessageActionsBinding.root) initEmojiBar(hasChatPermission) initMenuItemCopy(!message.isDeleted) + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv3, 1)) + chatActivity.chatViewModel.checkForNoteToSelf( + ApiUtils.getCredentials(user!!.username, user.token), + ApiUtils.getUrlForRooms( + apiVersion, + user.baseUrl + ), + false + ) + chatActivity.chatViewModel.getNoteToSelfAvaliability.observe(this) { state -> + when (state) { + is ChatViewModel.NoteToSelfAvaliableState -> { + initMenuAddToNote( + !message.isDeleted && !ConversationUtils.isNoteToSelfConversation(currentConversation), + state.roomToken + ) + } + else -> { + initMenuAddToNote( + false + ) + } + } + } + initMenuItemTranslate( !message.isDeleted && ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() && @@ -374,6 +402,16 @@ class MessageActionsDialog( dialogMessageActionsBinding.menuSaveMessage.visibility = getVisibility(visible) } + private fun initMenuAddToNote(visible: Boolean, roomToken: String = "") { + if (visible) { + dialogMessageActionsBinding.menuShareToNote.setOnClickListener { + chatActivity.shareToNotes(message, roomToken) + dismiss() + } + } + dialogMessageActionsBinding.menuShareToNote.visibility = getVisibility(visible) + } + private fun getVisibility(visible: Boolean): Int { return if (visible) { View.VISIBLE diff --git a/app/src/main/res/drawable/ic_edit_note_24.xml b/app/src/main/res/drawable/ic_edit_note_24.xml new file mode 100644 index 0000000000..aa2553c27a --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_note_24.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/main/res/layout/dialog_message_actions.xml b/app/src/main/res/layout/dialog_message_actions.xml index 7b201b247c..34b286d4da 100644 --- a/app/src/main/res/layout/dialog_message_actions.xml +++ b/app/src/main/res/layout/dialog_message_actions.xml @@ -358,6 +358,39 @@ + + + + + + + + Caption Retrieval failed Languages could not be retrieved + Add to Notes