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

Implementing Add to Notes in Message Actions
This commit is contained in:
Marcel Hibbe 2024-02-01 13:23:03 +01:00 committed by GitHub
commit 8e40ff0155
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 318 additions and 3 deletions

View File

@ -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 metaData = ""
var room = ""
if (!participantPermissions.hasChatPermission()) { if (!participantPermissions.hasChatPermission()) {
Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions") Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions")
@ -2698,11 +2699,13 @@ class ChatActivity :
metaData = "{\"caption\":\"$caption\"}" metaData = "{\"caption\":\"$caption\"}"
} }
if (token == "") room = roomToken else room = token
try { try {
require(fileUri.isNotEmpty()) require(fileUri.isNotEmpty())
UploadAndShareFilesWorker.upload( UploadAndShareFilesWorker.upload(
fileUri, fileUri,
roomToken, room,
currentConversation?.displayName!!, currentConversation?.displayName!!,
metaData 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) { fun openInFilesApp(message: ChatMessage) {
val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID] val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]
val link = message.selectedIndividualHashMap!!["link"] val link = message.selectedIndividualHashMap!!["link"]

View File

@ -22,6 +22,7 @@ package com.nextcloud.talk.chat.data
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel 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.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.models.json.reminder.Reminder
import io.reactivex.Observable import io.reactivex.Observable
@ -32,4 +33,18 @@ interface ChatRepository {
fun setReminder(user: User, roomToken: String, messageId: String, timeStamp: Int): Observable<Reminder> fun setReminder(user: User, roomToken: String, messageId: String, timeStamp: Int): Observable<Reminder>
fun getReminder(user: User, roomToken: String, messageId: String): Observable<Reminder> fun getReminder(user: User, roomToken: String, messageId: String): Observable<Reminder>
fun deleteReminder(user: User, roomToken: String, messageId: String): Observable<GenericOverall> 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>
} }

View File

@ -23,6 +23,7 @@ package com.nextcloud.talk.chat.data
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel 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.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.models.json.reminder.Reminder
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
@ -83,4 +84,40 @@ class ChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository {
it 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 }
}
} }

View File

@ -27,8 +27,10 @@ import androidx.lifecycle.ViewModel
import com.nextcloud.talk.chat.data.ChatRepository import com.nextcloud.talk.chat.data.ChatRepository
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel 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.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.models.json.reminder.Reminder
import com.nextcloud.talk.utils.ConversationUtils
import io.reactivex.Observer import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
@ -49,6 +51,13 @@ class ChatViewModel @Inject constructor(private val repository: ChatRepository)
val getReminderExistState: LiveData<ViewState> val getReminderExistState: LiveData<ViewState>
get() = _getReminderExistState 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 open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState
private val _getRoomViewState: MutableLiveData<ViewState> = MutableLiveData(GetRoomStartState) 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> { inner class GetRoomObserver : Observer<ConversationModel> {
override fun onSubscribe(d: Disposable) { override fun onSubscribe(d: Disposable) {
// unused atm // 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 { companion object {
private val TAG = ChatViewModel::class.simpleName private val TAG = ChatViewModel::class.simpleName
const val JOIN_ROOM_RETRY_COUNT: Long = 3 const val JOIN_ROOM_RETRY_COUNT: Long = 3

View File

@ -152,7 +152,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
remotePath remotePath
) )
} else { } else {
Log.d(TAG, "starting normal upload (not chunked)") Log.d(TAG, "starting normal upload (not chunked) of $fileName")
uploadSuccess = FileUploader( uploadSuccess = FileUploader(
context, context,

View File

@ -38,6 +38,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity 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.data.user.model.User
import com.nextcloud.talk.databinding.DialogMessageActionsBinding import com.nextcloud.talk.databinding.DialogMessageActionsBinding
import com.nextcloud.talk.models.domain.ConversationModel 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.models.json.chat.ChatMessage
import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.ui.theme.ViewThemeUtils 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.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
import com.vanniktech.emoji.EmojiPopup import com.vanniktech.emoji.EmojiPopup
import com.vanniktech.emoji.EmojiTextView import com.vanniktech.emoji.EmojiTextView
@ -96,6 +99,31 @@ class MessageActionsDialog(
viewThemeUtils.platform.themeDialog(dialogMessageActionsBinding.root) viewThemeUtils.platform.themeDialog(dialogMessageActionsBinding.root)
initEmojiBar(hasChatPermission) initEmojiBar(hasChatPermission)
initMenuItemCopy(!message.isDeleted) 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( initMenuItemTranslate(
!message.isDeleted && !message.isDeleted &&
ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() && ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
@ -374,6 +402,16 @@ class MessageActionsDialog(
dialogMessageActionsBinding.menuSaveMessage.visibility = getVisibility(visible) 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 { private fun getVisibility(visible: Boolean): Int {
return if (visible) { return if (visible) {
View.VISIBLE View.VISIBLE

View File

@ -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>

View File

@ -358,6 +358,39 @@
</LinearLayout> </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 <LinearLayout
android:id="@+id/menu_share" android:id="@+id/menu_share"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

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