diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt index 351115c60..876e66e00 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -258,4 +258,7 @@ interface NcApiCoroutines { @GET suspend fun getProfile(@Header("Authorization") authorization: String, @Url url: String): ProfileOverall + + @DELETE + suspend fun unbindRoom(@Header("Authorization") authorization: String, @Url url: String): GenericOverall } 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 9b242a8a8..c92ca2e4a 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -86,6 +86,7 @@ import coil.request.CachePolicy import coil.request.ImageRequest import coil.target.Target import coil.transform.CircleCropTransformation +import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.color.ColorUtil @@ -168,6 +169,10 @@ import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.AudioUtils import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfEventRooms +import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfInstantMeetingRoom +import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfSIPRoom import com.nextcloud.talk.utils.ContactUtils import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.DateConstants @@ -664,6 +669,59 @@ class ChatActivity : } } + if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT && + hasSpreedFeatureCapability( + conversationUser?.capabilities!!.spreedCapability!!, + SpreedFeatures.UNBIND_CONVERSATION + ) + ) { + val eventEndTimeStamp = + currentConversation?.objectId + ?.split("#") + ?.getOrNull(1) + ?.toLongOrNull() + val currentTimeStamp = (System.currentTimeMillis() / 1000).toLong() + val retentionPeriod = retentionOfEventRooms(spreedCapabilities) + val isPastEvent = eventEndTimeStamp?.let { it < currentTimeStamp } + if (isPastEvent == true && retentionPeriod != 0) { + showConversationDeletionWarning(retentionPeriod) + } + } + + if (currentConversation?.objectType == ConversationEnums.ObjectType.PHONE_TEMPORARY && + hasSpreedFeatureCapability( + conversationUser?.capabilities!!.spreedCapability!!, + SpreedFeatures.UNBIND_CONVERSATION + ) + ) { + val retentionPeriod = retentionOfSIPRoom(spreedCapabilities) + val systemMessage = currentConversation?.lastMessage?.systemMessageType + if (retentionPeriod != 0 && ( + systemMessage == ChatMessage.SystemMessageType.CALL_ENDED || + systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE + ) + ) { + showConversationDeletionWarning(retentionPeriod) + } + } + + if (currentConversation?.objectType == ConversationEnums.ObjectType.INSTANT_MEETING && + hasSpreedFeatureCapability( + conversationUser?.capabilities!!.spreedCapability!!, + SpreedFeatures.UNBIND_CONVERSATION + ) + ) { + val retentionPeriod = retentionOfInstantMeetingRoom(spreedCapabilities) + val systemMessage = currentConversation?.lastMessage?.systemMessageType + if (retentionPeriod != 0 && ( + systemMessage == ChatMessage.SystemMessageType.CALL_ENDED || + systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE + ) + ) { + showConversationDeletionWarning(retentionPeriod) + } + } + updateRoomTimerHandler(MILLIS_250) val urlForChatting = @@ -1035,6 +1093,27 @@ class ChatActivity : binding.voiceRecordingLock.y -= y } + chatViewModel.unbindRoomResult.observe(this) { uiState -> + when (uiState) { + is ChatViewModel.UnbindRoomUiState.Success -> { + binding.conversationDeleteNotice.visibility = View.GONE + Snackbar.make( + binding.root, + context.getString(R.string.nc_room_retention), + Snackbar.LENGTH_LONG + ).show() + } + is ChatViewModel.UnbindRoomUiState.Error -> { + Snackbar.make( + binding.root, + context.getString(R.string.nc_common_error_sorry), + Snackbar.LENGTH_LONG + ).show() + } + else -> { } + } + } + chatViewModel.outOfOfficeViewState.observe(this) { uiState -> when (uiState) { is ChatViewModel.OutOfOfficeUIState.Error -> { @@ -1150,6 +1229,63 @@ class ChatActivity : } } + fun showConversationDeletionWarning(retentionPeriod: Int) { + binding.conversationDeleteNotice.visibility = View.VISIBLE + binding.conversationDeleteNotice.apply { + isClickable = false + isFocusable = false + bringToFront() + } + val deleteNoticeText = binding.conversationDeleteNotice.findViewById(R.id.deletion_message) + + deleteNoticeText.text = String.format( + resources.getString(R.string.nc_conversation_auto_delete_notice), + retentionPeriod + ) + + if (ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!)) { + binding.conversationDeleteNotice.findViewById(R.id.delete_now_button).visibility = + View.VISIBLE + binding.conversationDeleteNotice.findViewById(R.id.keep_button).visibility = View.VISIBLE + } else { + binding.conversationDeleteNotice.findViewById(R.id.delete_now_button).visibility = + View.GONE + binding.conversationDeleteNotice.findViewById(R.id.keep_button).visibility = View.GONE + } + binding.conversationDeleteNotice.findViewById(R.id.delete_now_button).setOnClickListener { + deleteConversationDialog(it.context) + } + + binding.conversationDeleteNotice.findViewById(R.id.keep_button).setOnClickListener { + chatViewModel.unbindRoom(credentials!!, conversationUser?.baseUrl!!, currentConversation?.token!!) + } + } + + fun deleteConversationDialog(context: Context) { + val dialogBuilder = MaterialAlertDialogBuilder(context) + .setIcon( + viewThemeUtils.dialog + .colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp) + ) + .setTitle(R.string.nc_delete_call) + .setMessage(R.string.nc_delete_conversation_more) + .setPositiveButton(R.string.nc_delete) { _, _ -> + currentConversation?.let { conversation -> + deleteConversation(conversation) + } + } + .setNegativeButton(R.string.nc_cancel) { _, _ -> + } + + viewThemeUtils.dialog + .colorMaterialAlertDialogBackground(context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + @Suppress("Detekt.TooGenericExceptionCaught") override fun onResume() { super.onResume() @@ -3041,28 +3177,7 @@ class ChatActivity : deleteConversation.visibility = View.VISIBLE deleteConversation.setOnClickListener { - val dialogBuilder = MaterialAlertDialogBuilder(it.context) - .setIcon( - viewThemeUtils.dialog - .colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp) - ) - .setTitle(R.string.nc_delete_call) - .setMessage(R.string.nc_delete_conversation_more) - .setPositiveButton(R.string.nc_delete) { _, _ -> - currentConversation?.let { conversation -> - deleteConversation(conversation) - } - } - .setNegativeButton(R.string.nc_cancel) { _, _ -> - } - - viewThemeUtils.dialog - .colorMaterialAlertDialogBackground(it.context, dialogBuilder) - val dialog = dialogBuilder.show() - viewThemeUtils.platform.colorTextButtons( - dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE) - ) + deleteConversationDialog(it.context) popupWindow.dismiss() } } else { diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt index a533221dc..7276f4489 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt @@ -76,4 +76,5 @@ interface ChatNetworkDataSource { limit: Int ): List suspend fun getOpenGraph(credentials: String, baseUrl: String, extractedLinkToPreview: String): Reference? + suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt index 07989ffdb..bf36274f6 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt @@ -217,4 +217,9 @@ class RetrofitChatNetwork( extractedLinkToPreview ).blockingFirst().ocs?.data?.references?.entries?.iterator()?.next()?.value } + + override suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall { + val url = ApiUtils.getUrlForUnbindingRoom(baseUrl, roomToken) + return ncApiCoroutines.unbindRoom(credentials, url) + } } 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 617e949cd..124d81cdb 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 @@ -146,6 +146,10 @@ class ChatViewModel @Inject constructor( val outOfOfficeViewState: LiveData get() = _outOfOfficeViewState + private val _unbindRoomResult = MutableLiveData(UnbindRoomUiState.None) + val unbindRoomResult: LiveData + get() = _unbindRoomResult + private val _voiceMessagePlaybackSpeedPreferences: MutableLiveData> = MutableLiveData() val voiceMessagePlaybackSpeedPreferences: LiveData> get() = _voiceMessagePlaybackSpeedPreferences @@ -804,6 +808,18 @@ class ChatViewModel @Inject constructor( } } + @Suppress("Detekt.TooGenericExceptionCaught") + fun unbindRoom(credentials: String, baseUrl: String, roomToken: String) { + viewModelScope.launch { + try { + val response = chatNetworkDataSource.unbindRoom(credentials, baseUrl, roomToken) + _unbindRoomResult.value = UnbindRoomUiState.Success(response.ocs?.meta?.statusCode!!) + } catch (exception: Exception) { + _unbindRoomResult.value = UnbindRoomUiState.Error(exception.message.toString()) + } + } + } + fun resendMessage(credentials: String, urlForChat: String, message: ChatMessage) { viewModelScope.launch { chatRepository.resendChatMessage( @@ -855,4 +871,10 @@ class ChatViewModel @Inject constructor( data class Success(val userAbsence: UserAbsenceData) : OutOfOfficeUIState() data class Error(val exception: Exception) : OutOfOfficeUIState() } + + sealed class UnbindRoomUiState { + data object None : UnbindRoomUiState() + data class Success(val statusCode: Int) : UnbindRoomUiState() + data class Error(val message: String) : UnbindRoomUiState() + } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index a7bd96431..f5fed2217 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -523,7 +523,7 @@ class ConversationsListActivity : nearFutureEventConversationItems.clear() for (conversation in list) { - if (!futureEvent(conversation)) { + if (!isFutureEvent(conversation)) { addToNearFutureEventConversationItems(conversation) } addToConversationItems(conversation) @@ -564,13 +564,14 @@ class ConversationsListActivity : return false } - private fun futureEvent(conversation: ConversationModel): Boolean { + private fun isFutureEvent(conversation: ConversationModel): Boolean { if (!conversation.objectId.contains("#")) { return false } - return conversation.objectType == ConversationEnums.ObjectType.EVENT && - (conversation.objectId.split("#")[0].toLong() - (System.currentTimeMillis() / LONG_1000)) > - AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS + val eventTimeStart = conversation.objectId.split("#")[0].toLong() + val currentTimeStampInSeconds = System.currentTimeMillis() / LONG_1000 + val sixteenHoursAfterTimeStamp = (eventTimeStart - currentTimeStampInSeconds) > SIXTEEN_HOURS_IN_SECONDS + return conversation.objectType == ConversationEnums.ObjectType.EVENT && sixteenHoursAfterTimeStamp } fun showOnlyNearFutureEvents() { @@ -2156,7 +2157,7 @@ class ConversationsListActivity : const val NOTIFICATION_WARNING_DATE_NOT_SET = 0L const val OFFSET_HEIGHT_DIVIDER: Int = 3 const val ROOM_TYPE_ONE_ONE = "1" - private const val AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS: Long = 57600 + private const val SIXTEEN_HOURS_IN_SECONDS: Long = 57600 const val LONG_1000: Long = 1000 } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt index 44dd302b5..56a8d9660 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt @@ -44,6 +44,9 @@ class ConversationEnums { SHARE_PASSWORD, FILE, ROOM, - EVENT + EVENT, + PHONE_TEMPORARY, + PHONE_PERSIST, + INSTANT_MEETING } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt index 817f1b089..3171bf223 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt @@ -16,6 +16,9 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter ConversationEnums.ObjectType.ROOM "file" -> ConversationEnums.ObjectType.FILE "event" -> ConversationEnums.ObjectType.EVENT + "phone_persist" -> ConversationEnums.ObjectType.PHONE_PERSIST + "phone_temporary" -> ConversationEnums.ObjectType.PHONE_TEMPORARY + "instant_meeting" -> ConversationEnums.ObjectType.INSTANT_MEETING else -> ConversationEnums.ObjectType.DEFAULT } } @@ -30,6 +33,9 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter "room" ConversationEnums.ObjectType.FILE -> "file" ConversationEnums.ObjectType.EVENT -> "event" + ConversationEnums.ObjectType.PHONE_PERSIST -> "phone_persist" + ConversationEnums.ObjectType.PHONE_TEMPORARY -> "phone_temporary" + ConversationEnums.ObjectType.INSTANT_MEETING -> "instant_meeting" else -> "" } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt index 4cd1e6d68..d27608b56 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt @@ -449,6 +449,10 @@ object ApiUtils { return "$baseUrl$OCS_API_VERSION/cloud/users/search/by-phone" } + fun getUrlForUnbindingRoom(baseUrl: String, roomToken: String): String { + return "$baseUrl/ocs/v2.php/apps/spreed/api/v4/room/$roomToken/object" + } + fun getUrlForFileUpload(baseUrl: String, user: String, remotePath: String): String { return "$baseUrl/remote.php/dav/files/$user$remotePath" } diff --git a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt index 8ed61b5c7..ef0f85f1e 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt @@ -57,7 +57,8 @@ enum class SpreedFeatures(val value: String) { BAN_V1("ban-v1"), EDIT_MESSAGES_NOTE_TO_SELF("edit-messages-note-to-self"), ARCHIVE_CONVERSATIONS("archived-conversations-v2"), - CONVERSATION_CREATION_ALL("conversation-creation-all") + CONVERSATION_CREATION_ALL("conversation-creation-all"), + UNBIND_CONVERSATION("unbind-conversation") } @Suppress("TooManyFunctions") @@ -140,6 +141,36 @@ object CapabilitiesUtil { return false } + fun retentionOfEventRooms(spreedCapabilities: SpreedCapability): Int { + if (spreedCapabilities.config?.containsKey("conversations") == true) { + val map = spreedCapabilities.config!!["conversations"] + if (map?.containsKey("retention-event") == true) { + return map["retention-event"].toString().toInt() + } + } + return 0 + } + + fun retentionOfSIPRoom(spreedCapabilities: SpreedCapability): Int { + if (spreedCapabilities.config?.containsKey("conversations") == true) { + val map = spreedCapabilities.config!!["conversations"] + if (map?.containsKey("retention-phone") == true) { + return map["retention-phone"].toString().toInt() + } + } + return 0 + } + + fun retentionOfInstantMeetingRoom(spreedCapabilities: SpreedCapability): Int { + if (spreedCapabilities.config?.containsKey("conversations") == true) { + val map = spreedCapabilities.config!!["conversations"] + if (map?.containsKey("retention-instant-meetings") == true) { + return map["retention-instant-meetings"].toString().toInt() + } + } + return 0 + } + @JvmStatic fun isCallRecordingAvailable(spreedCapabilities: SpreedCapability): Boolean { if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RECORDING_V1) && diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 9652c2947..0a77d7a5e 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -139,6 +139,19 @@ + + + + + + + android:background="@color/grey_600"> @@ -26,7 +27,7 @@ android:id="@+id/meetingTime" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="?android:attr/textColorSecondary" + android:textColor="@color/black" android:textSize="16sp" tools:text="Meeting at 8:00 pm"/> @@ -45,7 +46,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/archive_conversation" + android:textColor="@color/black" android:visibility = "gone" + android:textSize="18sp" android:paddingTop="24dp"/> @@ -54,6 +57,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/unarchive_conversation" + android:textColor="@color/black" android:visibility = "gone" android:textSize="18sp" android:paddingTop="24dp"/> diff --git a/app/src/main/res/layout/remainder_to_delete_conversation.xml b/app/src/main/res/layout/remainder_to_delete_conversation.xml new file mode 100644 index 000000000..30fe63629 --- /dev/null +++ b/app/src/main/res/layout/remainder_to_delete_conversation.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index df8bcb4d6..05d9f105e 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -12,7 +12,7 @@ #006AA3 @color/colorPrimary #ff6F6F6F - #FF37474F + #1E1E1E diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 829a9ed53..661c4299d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -13,7 +13,7 @@ #ff888888 #ffffff #B3FFFFFF - #FF607D8B + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b8f31db2e..6b23dff0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -513,9 +513,13 @@ How to translate with transifex: Reply Reply privately Delete + This conversation will be automatically deleted for everyone in %1$d days of no activity + Delete now + Keep Message deleted successfully, but it might have been leaked to other services You are not allowed to start a call You need to promote a new moderator before you can leave the conversation + Room is retained successfully Share Send to