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 <julius.linus@nextcloud.com>
This commit is contained in:
Julius Linus 2024-01-22 11:08:36 -06:00 committed by Marcel Hibbe
parent 5b85731097
commit d45277beaf
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
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 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"]

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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