Browse Source

Merge pull request #3586 from nextcloud/issue-3584-note-to-self-action

Implementing Add to Notes in Message Actions
Marcel Hibbe 1 year ago
parent
commit
8e40ff0155

+ 81 - 2
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<String?, String?>?
+        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"]

+ 15 - 0
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<Reminder>
     fun getReminder(user: User, roomToken: String, messageId: String): Observable<Reminder>
     fun deleteReminder(user: User, roomToken: String, messageId: String): Observable<GenericOverall>
+    fun shareToNotes(
+        credentials: String,
+        url: String,
+        message: String,
+        displayName: String
+    ): Observable<GenericOverall> // last two fields are false
+    fun checkForNoteToSelf(credentials: String, url: String, includeStatus: Boolean): Observable<RoomsOverall>
+    fun shareLocationToNotes(
+        credentials: String,
+        url: String,
+        objectType: String,
+        objectId: String,
+        metadata: String
+    ): Observable<GenericOverall>
 }

+ 37 - 0
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<GenericOverall> {
+        return ncApi.sendChatMessage(
+            credentials,
+            url,
+            message,
+            displayName,
+            null,
+            false
+        ).map {
+            it
+        }
+    }
+
+    override fun checkForNoteToSelf(
+        credentials: String,
+        url: String,
+        includeStatus: Boolean
+    ): Observable<RoomsOverall> {
+        return ncApi.getRooms(credentials, url, includeStatus).map { it }
+    }
+
+    override fun shareLocationToNotes(
+        credentials: String,
+        url: String,
+        objectType: String,
+        objectId: String,
+        metadata: String
+    ): Observable<GenericOverall> {
+        return ncApi.sendLocation(credentials, url, objectType, objectId, metadata).map { it }
+    }
 }

+ 91 - 0
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<ViewState>
         get() = _getReminderExistState
 
+    object NoteToSelfNotAvaliableState : ViewState
+    open class NoteToSelfAvaliableState(val roomToken: String) : ViewState
+
+    private val _getNoteToSelfAvaliability: MutableLiveData<ViewState> = MutableLiveData(NoteToSelfNotAvaliableState)
+    val getNoteToSelfAvaliability: LiveData<ViewState>
+        get() = _getNoteToSelfAvaliability
+
     open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState
 
     private val _getRoomViewState: MutableLiveData<ViewState> = 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<GenericOverall> {
+                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<GenericOverall> {
+                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<ConversationModel> {
         override fun onSubscribe(d: Disposable) {
             // unused atm
@@ -192,6 +253,36 @@ class ChatViewModel @Inject constructor(private val repository: ChatRepository)
         }
     }
 
+    inner class CheckForNoteToSelfObserver : Observer<RoomsOverall> {
+        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

+ 1 - 1
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,

+ 38 - 0
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

+ 21 - 0
app/src/main/res/drawable/ic_edit_note_24.xml

@@ -0,0 +1,21 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2021 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M3,10h11v2H3V10zM3,8h11V6H3V8zM3,16h7v-2H3V16zM18.01,12.87l0.71,-0.71c0.39,-0.39 1.02,-0.39 1.41,0l0.71,0.71c0.39,0.39 0.39,1.02 0,1.41l-0.71,0.71L18.01,12.87zM17.3,13.58l-5.3,5.3V21h2.12l5.3,-5.3L17.3,13.58z"/>
+    
+</vector>

+ 33 - 0
app/src/main/res/layout/dialog_message_actions.xml

@@ -358,6 +358,39 @@
 
         </LinearLayout>
 
+        <LinearLayout
+            android:id="@+id/menu_share_to_note"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/bottom_sheet_item_height"
+            android:background="?android:attr/selectableItemBackground"
+            android:gravity="center_vertical"
+            android:orientation="horizontal"
+            tools:ignore="UseCompoundDrawables">
+
+            <ImageView
+                android:id="@+id/menu_icon_share_to_note"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@null"
+                android:paddingStart="@dimen/standard_padding"
+                android:paddingEnd="@dimen/zero"
+                android:src="@drawable/ic_edit_note_24"
+                app:tint="@color/high_emphasis_menu_icon" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/menu_text_share_to_note"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start|center_vertical"
+                android:paddingStart="@dimen/standard_double_padding"
+                android:paddingEnd="@dimen/standard_padding"
+                android:text="@string/add_to_notes"
+                android:textAlignment="viewStart"
+                android:textColor="@color/high_emphasis_text"
+                android:textSize="@dimen/bottom_sheet_text_size" />
+        </LinearLayout>
+
+
         <LinearLayout
             android:id="@+id/menu_share"
             android:layout_width="match_parent"

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -786,4 +786,5 @@ How to translate with transifex:
     <string name="nc_caption">Caption</string>
     <string name="languages_error_title">Retrieval failed</string>
     <string name="languages_error_message">Languages could not be retrieved</string>
+    <string name="add_to_notes">Add to Notes</string>
 </resources>