diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 81be3132e15..6737d7e9b82 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -314,6 +314,7 @@ dependencies { } implementation("androidx.core:core-ktx:1.18.0") + implementation("androidx.sharetarget:sharetarget:1.2.0") implementation("androidx.activity:activity-ktx:1.13.0") implementation("com.github.nextcloud.android-common:ui:0.33.2") implementation("com.github.nextcloud.android-common:core:0.33.2") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5da4d1fc8da..e315e7a9242 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -123,6 +123,10 @@ + + + + diff --git a/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt index 55be48319ff..1912aa231af 100644 --- a/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt @@ -23,6 +23,7 @@ import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivitySwitchAccountBinding import com.nextcloud.talk.models.ImportAccount import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.conversationlist.DirectShareHelper import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.AccountUtils.findAvailableAccountsOnDevice import com.nextcloud.talk.utils.AccountUtils.getInformationFromAccount @@ -93,6 +94,7 @@ class SwitchAccountActivity : BaseActivity() { reauthorizeFromImport(item.account) } else { if (userManager.setUserAsActive(item.user!!).blockingGet()) { + DirectShareHelper.removeAllShareTargetShortcuts(this@SwitchAccountActivity) cookieManager.cookieStore.removeAll() finish() } diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt index 4ba2288b1a0..33245acc3ea 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -1037,6 +1037,7 @@ class MessageInputFragment : Fragment() { replaceMentionChipSpans(editable) binding.fragmentMessageInputView.inputEditText?.setText("") sendStopTypingMessage() + reportOutgoingDirectShare() sendMessage( editable.toString(), sendWithoutNotification @@ -1046,6 +1047,23 @@ class MessageInputFragment : Fragment() { } } + private fun reportOutgoingDirectShare() { + val user = chatActivity.conversationUser ?: return + val isOneToOne = chatActivity.currentConversation?.type == + com.nextcloud.talk.models.json.conversations.ConversationEnums + .ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + val displayName = chatActivity.currentConversation?.displayName ?: chatActivity.roomToken + lifecycleScope.launch { + com.nextcloud.talk.conversationlist.DirectShareHelper.reportOutgoingMessage( + requireContext(), + user, + chatActivity.roomToken, + displayName, + isOneToOne + ) + } + } + private fun sendMessage(message: String, sendWithoutNotification: Boolean) { chatActivity.chatViewModel.onMessageSent() diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt index 12c035c5c1a..af47fa9ffba 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt @@ -42,6 +42,8 @@ interface ChatMessageRepository : LifecycleAwareManager { val roomRefreshFlow: Flow + val incomingMessageFlow: Flow + // /** // * Used for informing the user of the underlying processing behind offline support, [String] is the key // * which is handled in a switch statement in ChatActivity. diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt index 445363b103d..a1d92acda48 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt @@ -50,7 +50,6 @@ import kotlinx.coroutines.flow.take import retrofit2.HttpException import java.io.IOException import javax.inject.Inject -import kotlin.collections.map @Suppress("LargeClass", "TooManyFunctions") class OfflineFirstChatRepository @Inject constructor( @@ -104,6 +103,11 @@ class OfflineFirstChatRepository @Inject constructor( private val _roomRefreshFlow: MutableSharedFlow = MutableSharedFlow() + override val incomingMessageFlow: Flow + get() = _incomingMessageFlow + + private val _incomingMessageFlow: MutableSharedFlow = MutableSharedFlow() + private var newXChatLastCommonRead: Int? = null private var itIsPaused = false @@ -493,7 +497,8 @@ class OfflineFirstChatRepository @Inject constructor( lookIntoFuture: Boolean, hasHistory: Boolean ) { - val chatMessageEntities = persistChatMessagesAndHandleSystemMessages(chatMessagesJson) + val chatMessageEntities = + persistChatMessagesAndHandleSystemMessages(chatMessagesJson, emitOnIncoming = lookIntoFuture) val oldestIdFromSync = chatMessageEntities.minByOrNull { it.id }!!.id val newestIdFromSync = chatMessageEntities.maxByOrNull { it.id }!!.id @@ -950,7 +955,7 @@ class OfflineFirstChatRepository @Inject constructor( } override suspend fun onSignalingChatMessageReceived(chatMessages: List) { - persistChatMessagesAndHandleSystemMessages(chatMessages) + persistChatMessagesAndHandleSystemMessages(chatMessages, emitOnIncoming = true) // we assume that the signaling messages are on top of the latest chatblock and include them inside it. // If for whatever reason the assume was not correct and there would be messages in between, the @@ -963,7 +968,8 @@ class OfflineFirstChatRepository @Inject constructor( } suspend fun persistChatMessagesAndHandleSystemMessages( - chatMessages: List + chatMessages: List, + emitOnIncoming: Boolean = false ): List { handleSystemMessagesThatAffectDatabase(chatMessages) @@ -973,6 +979,16 @@ class OfflineFirstChatRepository @Inject constructor( chatDao.upsertChatMessagesAndDeleteTemp(internalConversationId, chatMessageEntities) + if (emitOnIncoming) { + val hasIncomingFromOther = chatMessages.any { msg -> + msg.systemMessageType == ChatMessage.SystemMessageType.DUMMY && + msg.actorId != currentUser.userId + } + if (hasIncomingFromOther) { + _incomingMessageFlow.emit(Unit) + } + } + return chatMessageEntities } 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 42775b17b62..4d934fb845f 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 @@ -28,6 +28,8 @@ import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource import com.nextcloud.talk.chat.ui.model.ChatMessageUi import com.nextcloud.talk.chat.ui.model.MessageTypeContent import com.nextcloud.talk.chat.ui.model.toUiModel +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.conversationlist.DirectShareHelper import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository import com.nextcloud.talk.conversationlist.data.network.OfflineFirstConversationsRepository import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel.Companion.FOLLOWED_THREADS_EXIST @@ -79,6 +81,7 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -435,6 +438,7 @@ class ChatViewModel @AssistedInject constructor( observeMediaPlayerProgressForCompose() observePinnedMessage() observeRoomRefresh() + observeIncomingMessages() } private fun observeMediaPlayerProgressForCompose() { @@ -573,6 +577,23 @@ class ChatViewModel @AssistedInject constructor( .launchIn(viewModelScope) } + private fun observeIncomingMessages() { + chatRepository.incomingMessageFlow + .onEach { + val (conversation, user) = conversationAndUserFlow.first() + val context = NextcloudTalkApplication.sharedApplication!! + val isOneToOne = conversation.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + DirectShareHelper.reportIncomingMessage( + context, + user, + conversation.token, + conversation.displayName ?: conversation.token, + isOneToOne + ) + } + .launchIn(viewModelScope) + } + // val lastCommonReadMessageId = getLastCommonReadFlow.first() // ------------------------------ 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 f0b21622d9f..1572924cdcb 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -162,6 +162,7 @@ class ConversationsListActivity : BaseActivity() { private var selectedConversation: ConversationModel? = null private var textToPaste: String? = "" private var selectedMessageId: String? = null + private var pendingDirectShareToken: String? = null private val onBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { @@ -320,6 +321,21 @@ class ConversationsListActivity : BaseActivity() { showNotificationWarningState.value = shouldShowNotificationWarning() showShareToScreenState.value = hasActivityActionSendIntent() + // Home screen shortcut tap: shortcut uses ACTION_VIEW with KEY_ROOM_TOKEN to open the + // conversation directly without showing the share picker. + if (Intent.ACTION_VIEW == intent.action && intent.hasExtra(KEY_ROOM_TOKEN)) { + pendingDirectShareToken = intent.getStringExtra(KEY_ROOM_TOKEN) + } + + // Share sheet shortcut tap: Android delivers ACTION_SEND with EXTRA_SHORTCUT_ID. + // Extract the room token from the shortcut ID so we can pre-select the conversation. + if (hasActivityActionSendIntent()) { + val shortcutId = intent.getStringExtra(Intent.EXTRA_SHORTCUT_ID) + if (shortcutId != null) { + pendingDirectShareToken = DirectShareHelper.extractTokenFromShortcutId(shortcutId) + } + } + if (!eventBus.isRegistered(this)) { eventBus.register(this) } @@ -377,11 +393,31 @@ class ConversationsListActivity : BaseActivity() { lifecycleScope.launch { conversationsListViewModel.getRoomsFlow .onEach { list -> + // Add app shortcut for note to self val noteToSelf = list .firstOrNull { ConversationUtils.isNoteToSelfConversation(it) } val isNoteToSelfAvailable = noteToSelf != null handleNoteToSelfShortcut(isNoteToSelfAvailable, noteToSelf?.token ?: "") + // Update Direct Share targets + if (currentUser != null) { + lifecycleScope.launch { + DirectShareHelper.publishShareTargetShortcuts( + context, + currentUser!!, + list + ) + } + } + + // check for Direct Share + val token = pendingDirectShareToken + if (token != null) { + pendingDirectShareToken = null + val conversation = list.firstOrNull { it.token == token } + if (conversation != null) handleConversation(conversation) + } + if (!scrollPositionRestored) { scrollPositionRestored = true val pair = appPreferences.conversationListPositionAndOffset diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/DirectShareHelper.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/DirectShareHelper.kt new file mode 100644 index 00000000000..088de5cf23b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/DirectShareHelper.kt @@ -0,0 +1,256 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2026 Jens Zalzala + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationlist + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.BitmapDrawable +import android.util.Log +import androidx.core.app.Person +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import coil.imageLoader +import coil.request.ImageRequest +import coil.transform.CircleCropTransformation +import com.nextcloud.talk.R +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.bundle.BundleKeys + +object DirectShareHelper { + + private val TAG = DirectShareHelper::class.java.simpleName + private const val SHARE_TARGET_CATEGORY = "com.nextcloud.talk.sharing.SHARE_TARGET_CATEGORY" + private const val SHORTCUT_ID_PREFIX = "direct_share_" + private const val SHORTCUT_ID_MIN_PARTS = 4 + private const val AVATAR_SIZE_PX = 256 + + private enum class MessageDirection { NONE, SEND, RECEIVE } + + suspend fun publishShareTargetShortcuts(context: Context, user: User, conversations: List) { + val maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context) + + // Preserve shortcuts not managed by DirectShareHelper (e.g. Note to Self). + val existingOtherShortcuts = ShortcutManagerCompat.getDynamicShortcuts(context) + .filter { !it.id.startsWith(SHORTCUT_ID_PREFIX) } + val limit = (maxShortcuts - existingOtherShortcuts.size).coerceAtLeast(0) + + @Suppress("MagicNumber") + val thirtyDaysAgoSeconds = System.currentTimeMillis() / 1000 - 30 * 24 * 60 * 60 + val afterFilter = conversations + .filter { !ConversationUtils.isNoteToSelfConversation(it) } + .filter { it.lastActivity >= thirtyDaysAgoSeconds } + val candidates = afterFilter + .sortedByDescending { it.lastActivity } + .take(limit) + + val credentials = ApiUtils.getCredentials(user.username, user.token) + // Build shortcuts most-recent-first; setDynamicShortcuts uses index order (0 = most important). + val shareShortcuts = candidates.map { conversation -> + val displayName = conversation.displayName + val icon = loadAvatarIcon(context, user, conversation.token, displayName, credentials) + prepShortcutBuilder(context, user, conversation.token, displayName, icon).build() + } + + ShortcutManagerCompat.setDynamicShortcuts(context, shareShortcuts + existingOtherShortcuts) + } + + /** + * Reports an incoming message for the given conversation, improving its share sheet ranking. + * Per Android docs, communication apps should republish the shortcut via pushDynamicShortcut + * when a message is received. + */ + suspend fun reportIncomingMessage( + context: Context, + user: User, + token: String, + displayName: String, + isOneToOne: Boolean + ) { + val credentials = ApiUtils.getCredentials(user.username, user.token) + val icon = loadAvatarIcon(context, user, token, displayName, credentials) + val shortcut = prepShortcutBuilder(context, user, token, displayName, icon) + .also { applyCapabilityBinding(it, MessageDirection.RECEIVE, isOneToOne) } + .build() + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + } + + /** + * Reports an outgoing message for the given conversation. Per Android docs, outgoing messages + * should republish the shortcut via pushDynamicShortcut with SEND_MESSAGE capability binding. + */ + suspend fun reportOutgoingMessage( + context: Context, + user: User, + token: String, + displayName: String, + isOneToOne: Boolean + ) { + val credentials = ApiUtils.getCredentials(user.username, user.token) + val icon = loadAvatarIcon(context, user, token, displayName, credentials) + val shortcut = prepShortcutBuilder(context, user, token, displayName, icon) + .also { applyCapabilityBinding(it, MessageDirection.SEND, isOneToOne) } + .build() + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + } + + /** + * Removes all direct-share shortcuts belonging to the user. + * Called on logout/Remove account + */ + fun removeShortcutsForUser(context: Context, userId: Long) { + val prefix = "${SHORTCUT_ID_PREFIX}${userId}_" + val ids = ShortcutManagerCompat.getDynamicShortcuts(context) + .map { it.id } + .filter { it.startsWith(prefix) } + if (ids.isNotEmpty()) { + ShortcutManagerCompat.removeLongLivedShortcuts(context, ids) + } + } + + /** + * Removes the shortcut for a conversation. + * Called when a conversation is deleted or left. + */ + fun removeShortcutForConversation(context: Context, userId: Long, token: String) { + val id = "${SHORTCUT_ID_PREFIX}${userId}_$token" + ShortcutManagerCompat.removeLongLivedShortcuts(context, listOf(id)) + } + + /** + * Removes all direct-share shortcuts. + * Called on account switch. + */ + fun removeAllShareTargetShortcuts(context: Context) { + val ids = ShortcutManagerCompat.getDynamicShortcuts(context) + .map { it.id } + .filter { it.startsWith(SHORTCUT_ID_PREFIX) } + if (ids.isNotEmpty()) { + ShortcutManagerCompat.removeLongLivedShortcuts(context, ids) + } + } + + /** + * Parses the room token from a shortcut ID of the form "direct_share_{userId}_{token}". + * Returns null if the ID does not belong to this app's direct share shortcuts. + */ + fun extractTokenFromShortcutId(shortcutId: String): String? { + if (!shortcutId.startsWith(SHORTCUT_ID_PREFIX)) return null + val parts = shortcutId.split("_") + return if (parts.size < SHORTCUT_ID_MIN_PARTS) { + null + } else { + parts.drop(SHORTCUT_ID_MIN_PARTS - 1).joinToString("_") + } + } + + private fun shortcutId(user: User, token: String): String = "$SHORTCUT_ID_PREFIX${user.id}_$token" + + private suspend fun loadAvatarIcon( + context: Context, + user: User, + token: String, + displayName: String, + credentials: String? + ): IconCompat = + try { + val avatarUrl = ApiUtils.getUrlForConversationAvatar( + version = 1, + baseUrl = user.baseUrl, + token = token + ) + val requestBuilder = ImageRequest.Builder(context) + .data(avatarUrl) + .size(AVATAR_SIZE_PX) + .allowHardware(false) + .transformations(CircleCropTransformation()) + if (credentials != null) { + requestBuilder.addHeader("Authorization", credentials) + } + val result = context.imageLoader.execute(requestBuilder.build()) + val bitmap = (result.drawable as? BitmapDrawable)?.bitmap + if (bitmap != null) { + IconCompat.createWithBitmap(bitmap) + } else { + defaultIcon(context) + } + } catch (e: java.io.IOException) { + Log.w(TAG, "Failed to load avatar for shortcut: $displayName", e) + defaultIcon(context) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Invalid avatar request for shortcut: $displayName", e) + defaultIcon(context) + } + + private fun defaultIcon(context: Context): IconCompat = + IconCompat.createWithResource(context, R.drawable.ic_circular_group) + + private fun prepShortcutBuilder( + context: Context, + user: User, + token: String, + displayName: String, + icon: IconCompat + ): ShortcutInfoCompat.Builder { + // ACTION_VIEW is used so that tapping this shortcut on the home screen opens the + // conversation directly. When used from the share sheet, Android delivers ACTION_SEND + // with the shared content and EXTRA_SHORTCUT_ID, not this intent. + val intent = Intent(Intent.ACTION_VIEW, null, context, ConversationsListActivity::class.java).apply { + putExtra(BundleKeys.KEY_ROOM_TOKEN, token) + putExtra(BundleKeys.KEY_INTERNAL_USER_ID, user.id) + } + + val person = Person.Builder() + .setName(displayName) + .setIcon(icon) + .setKey(shortcutId(user, token)) + .build() + + return ShortcutInfoCompat.Builder(context, shortcutId(user, token)) + .setShortLabel(displayName) + .setLongLabel(displayName) + .setIcon(icon) + .setPerson(person) + .setCategories(setOf(SHARE_TARGET_CATEGORY)) + .setIntent(intent) + .setIsConversation() + .setLongLived(true) + } + + private fun applyCapabilityBinding( + builder: ShortcutInfoCompat.Builder, + direction: MessageDirection, + isOneToOne: Boolean + ) { + when (direction) { + MessageDirection.SEND -> if (isOneToOne) { + builder.addCapabilityBinding("actions.intent.SEND_MESSAGE") + } else { + builder.addCapabilityBinding( + "actions.intent.SEND_MESSAGE", + "message.recipient.@type", + listOf("Audience") + ) + } + MessageDirection.RECEIVE -> if (isOneToOne) { + builder.addCapabilityBinding("actions.intent.RECEIVE_MESSAGE") + } else { + builder.addCapabilityBinding( + "actions.intent.RECEIVE_MESSAGE", + "message.sender.@type", + listOf("Audience") + ) + } + MessageDirection.NONE -> Unit + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java index 77706c15a62..bbb4f4440ba 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java +++ b/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java @@ -22,6 +22,7 @@ import com.nextcloud.talk.models.json.generic.GenericMeta; import com.nextcloud.talk.models.json.generic.GenericOverall; import com.nextcloud.talk.models.json.push.PushConfigurationState; +import com.nextcloud.talk.conversationlist.DirectShareHelper; import com.nextcloud.talk.users.UserManager; import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.preferences.AppPreferences; @@ -179,6 +180,7 @@ public void onComplete() { private void initiateUserDeletion(User user) { if (user.getId() != null) { long id = user.getId(); + DirectShareHelper.INSTANCE.removeShortcutsForUser(getApplicationContext(), id); WebSocketConnectionHelper.deleteExternalSignalingInstanceForUserEntity(id); try { diff --git a/app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java index e664a2fb9e0..d1bc97c4e97 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java +++ b/app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java @@ -14,6 +14,7 @@ import com.nextcloud.talk.data.user.model.User; import com.nextcloud.talk.events.EventStatus; import com.nextcloud.talk.models.json.generic.GenericOverall; +import com.nextcloud.talk.conversationlist.DirectShareHelper; import com.nextcloud.talk.users.UserManager; import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.UserIdUtils; @@ -94,6 +95,10 @@ public void onSubscribe(Disposable d) { @Override public void onNext(GenericOverall genericOverall) { eventBus.postSticky(eventStatus); + if (conversationToken != null) { + DirectShareHelper.INSTANCE.removeShortcutForConversation( + getApplicationContext(), operationUserId, conversationToken); + } } @Override diff --git a/app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt index 57acc97c98d..da17ac617fd 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt @@ -20,6 +20,7 @@ import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.conversationlist.DirectShareHelper import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils.getConversationApiVersion import com.nextcloud.talk.utils.ApiUtils.getCredentials @@ -89,6 +90,13 @@ class LeaveConversationWorker(context: Context, workerParams: WorkerParameters) } override fun onComplete() { + if (currentUser.id != null) { + DirectShareHelper.removeShortcutForConversation( + applicationContext, + currentUser.id!!, + conversationToken + ) + } result.set(Result.success()) } }) diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index b705a8f8186..f5fda7e5984 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -532,6 +532,23 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor else -> Log.e(TAG, "unknown pushMessage.type") } + if (pushMessage.type == TYPE_CHAT || pushMessage.type == TYPE_ROOM) { + val token = pushMessage.id + val user = signatureVerification.user + val displayName = pushMessage.subject + if (token != null && user != null && displayName.isNotEmpty()) { + kotlinx.coroutines.runBlocking { + com.nextcloud.talk.conversationlist.DirectShareHelper.reportIncomingMessage( + context!!, + user, + token, + displayName, + isOneToOne = "one2one" == conversationType + ) + } + } + } + val pendingIntent = createUniquePendingIntent(intent) val uri = signatureVerification.user!!.baseUrl!!.toUri() val baseUrl = uri.host diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml new file mode 100644 index 00000000000..b752fcf8c31 --- /dev/null +++ b/app/src/main/res/xml/shortcuts.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 5fa3cff5e1f..3f6e500096c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -521,6 +521,14 @@ + + + + + + + + @@ -605,6 +613,14 @@ + + + + + + + + @@ -661,6 +677,14 @@ + + + + + + + + @@ -1035,6 +1059,22 @@ + + + + + + + + + + + + + + + + @@ -1075,6 +1115,14 @@ + + + + + + + + @@ -1115,6 +1163,14 @@ + + + + + + + + @@ -1155,6 +1211,14 @@ + + + + + + + + @@ -1195,6 +1259,14 @@ + + + + + + + + @@ -1219,6 +1291,22 @@ + + + + + + + + + + + + + + + + @@ -1508,6 +1596,11 @@ + + + + + @@ -1531,6 +1624,11 @@ + + + + + @@ -1608,6 +1706,14 @@ + + + + + + + + @@ -1676,6 +1782,11 @@ + + + + + @@ -1692,6 +1803,11 @@ + + + + + @@ -1753,6 +1869,14 @@ + + + + + + + + @@ -1821,6 +1945,11 @@ + + + + + @@ -1901,6 +2030,14 @@ + + + + + + + + @@ -1972,6 +2109,11 @@ + + + + + @@ -2052,6 +2194,14 @@ + + + + + + + + @@ -2120,6 +2270,11 @@ + + + + + @@ -2177,6 +2332,14 @@ + + + + + + + + @@ -2265,6 +2428,14 @@ + + + + + + + + @@ -2278,6 +2449,14 @@ + + + + + + + + @@ -2298,6 +2477,11 @@ + + + + + @@ -2370,6 +2554,14 @@ + + + + + + + + @@ -2486,6 +2678,11 @@ + + + + + @@ -2566,6 +2763,14 @@ + + + + + + + + @@ -2637,6 +2842,11 @@ + + + + + @@ -2685,6 +2895,14 @@ + + + + + + + + @@ -2724,6 +2942,11 @@ + + + + + @@ -2756,6 +2979,14 @@ + + + + + + + + @@ -2784,6 +3015,16 @@ + + + + + + + + + + @@ -2869,6 +3110,14 @@ + + + + + + + + @@ -2948,6 +3197,11 @@ + + + + + @@ -3033,6 +3287,14 @@ + + + + + + + + @@ -3109,6 +3371,11 @@ + + + + + @@ -3189,6 +3456,14 @@ + + + + + + + + @@ -3260,6 +3535,11 @@ + + + + + @@ -3345,6 +3625,14 @@ + + + + + + + + @@ -3510,6 +3798,9 @@ + + + @@ -3671,6 +3962,9 @@ + + + @@ -3752,6 +4046,14 @@ + + + + + + + + @@ -3828,6 +4130,11 @@ + + + + + @@ -3913,6 +4220,14 @@ + + + + + + + + @@ -3986,6 +4301,11 @@ + + + + + @@ -4053,6 +4373,14 @@ + + + + + + + + @@ -4126,6 +4454,11 @@ + + + + + @@ -4193,6 +4526,14 @@ + + + + + + + + @@ -4269,6 +4610,11 @@ + + + + + @@ -4354,6 +4700,14 @@ + + + + + + + + @@ -4430,6 +4784,11 @@ + + + + + @@ -4510,6 +4869,14 @@ + + + + + + + + @@ -4581,6 +4948,11 @@ + + + + + @@ -4661,6 +5033,14 @@ + + + + + + + + @@ -4883,6 +5263,9 @@ + + + @@ -4921,6 +5304,14 @@ + + + + + + + + @@ -4950,6 +5341,14 @@ + + + + + + + + @@ -5131,6 +5530,14 @@ + + + + + + + + @@ -5259,6 +5666,14 @@ + + + + + + + + @@ -5387,6 +5802,14 @@ + + + + + + + + @@ -5443,6 +5866,11 @@ + + + + + @@ -5491,6 +5919,14 @@ + + + + + + + + @@ -5547,6 +5983,11 @@ + + + + + @@ -5595,6 +6036,14 @@ + + + + + + + + @@ -5643,6 +6092,11 @@ + + + + + @@ -5691,6 +6145,14 @@ + + + + + + + + @@ -5747,6 +6209,11 @@ + + + + + @@ -5795,6 +6262,14 @@ + + + + + + + + @@ -5851,6 +6326,11 @@ + + + + + @@ -5875,6 +6355,14 @@ + + + + + + + + @@ -5947,6 +6435,14 @@ + + + + + + + + @@ -5995,6 +6491,14 @@ + + + + + + + + @@ -6232,6 +6736,14 @@ + + + + + + + + @@ -7436,6 +7948,14 @@ + + + + + + + + @@ -7516,6 +8036,14 @@ + + + + + + + + @@ -7596,6 +8124,14 @@ + + + + + + + + @@ -7676,6 +8212,14 @@ + + + + + + + + @@ -7756,6 +8300,14 @@ + + + + + + + + @@ -7836,6 +8388,14 @@ + + + + + + + + @@ -7916,6 +8476,14 @@ + + + + + + + + @@ -7996,11 +8564,27 @@ + + + + + + + + + + + + + + + + @@ -8089,6 +8673,11 @@ + + + + + @@ -8099,6 +8688,14 @@ + + + + + + + + @@ -8115,6 +8712,11 @@ + + + + + @@ -8125,6 +8727,14 @@ + + + + + + + + @@ -8960,6 +9570,14 @@ + + + + + + + + @@ -9596,6 +10214,19 @@ + + + + + + + + + + + + + @@ -9604,6 +10235,14 @@ + + + + + + + + @@ -9774,6 +10413,14 @@ + + + + + + + + @@ -9814,6 +10461,14 @@ + + + + + + + + @@ -9855,6 +10510,9 @@ + + + @@ -9867,6 +10525,19 @@ + + + + + + + + + + + + + @@ -9909,6 +10580,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -10101,6 +10814,14 @@ + + + + + + + + @@ -10229,6 +10950,14 @@ + + + + + + + + @@ -10357,6 +11086,19 @@ + + + + + + + + + + + + + @@ -10581,6 +11323,14 @@ + + + + + + + + @@ -10709,6 +11459,14 @@ + + + + + + + + @@ -10837,6 +11595,14 @@ + + + + + + + + @@ -11005,6 +11771,14 @@ + + + + + + + + @@ -11133,6 +11907,14 @@ + + + + + + + + @@ -11261,6 +12043,14 @@ + + + + + + + + @@ -11389,6 +12179,14 @@ + + + + + + + + @@ -11517,6 +12315,14 @@ + + + + + + + + @@ -11645,6 +12451,14 @@ + + + + + + + + @@ -11773,6 +12587,14 @@ + + + + + + + + @@ -12005,6 +12827,14 @@ + + + + + + + + @@ -12133,6 +12963,14 @@ + + + + + + + + @@ -12261,6 +13099,14 @@ + + + + + + + + @@ -12389,6 +13235,14 @@ + + + + + + + + @@ -12517,6 +13371,14 @@ + + + + + + + + @@ -12645,6 +13507,14 @@ + + + + + + + + @@ -12773,6 +13643,14 @@ + + + + + + + + @@ -12981,6 +13859,14 @@ + + + + + + + + @@ -13125,6 +14011,14 @@ + + + + + + + + @@ -13141,6 +14035,22 @@ + + + + + + + + + + + + + + + + @@ -13285,6 +14195,14 @@ + + + + + + + + @@ -13429,6 +14347,14 @@ + + + + + + + + @@ -13581,6 +14507,14 @@ + + + + + + + + @@ -13709,6 +14643,14 @@ + + + + + + + + @@ -13957,6 +14899,14 @@ + + + + + + + + @@ -14245,6 +15195,14 @@ + + + + + + + + @@ -14373,6 +15331,14 @@ + + + + + + + + @@ -14501,6 +15467,14 @@ + + + + + + + + @@ -14629,6 +15603,14 @@ + + + + + + + + @@ -15173,6 +16155,14 @@ + + + + + + + + @@ -15301,6 +16291,14 @@ + + + + + + + + @@ -15429,6 +16427,14 @@ + + + + + + + + @@ -15557,6 +16563,14 @@ + + + + + + + + @@ -15685,6 +16699,14 @@ + + + + + + + + @@ -15813,6 +16835,14 @@ + + + + + + + + @@ -15941,6 +16971,14 @@ + + + + + + + + @@ -16069,6 +17107,14 @@ + + + + + + + + @@ -16197,6 +17243,14 @@ + + + + + + + + @@ -16325,6 +17379,14 @@ + + + + + + + + @@ -16453,6 +17515,14 @@ + + + + + + + + @@ -16581,6 +17651,14 @@ + + + + + + + + @@ -16853,6 +17931,14 @@ + + + + + + + + @@ -16981,6 +18067,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -17109,6 +18219,14 @@ + + + + + + + + @@ -19573,6 +20691,11 @@ + + + + + @@ -19679,6 +20802,14 @@ + + + + + + + + @@ -19704,6 +20835,11 @@ + + + + + @@ -19774,6 +20910,11 @@ + + + + + @@ -20118,6 +21259,11 @@ + + + + + @@ -20211,6 +21357,11 @@ + + + + + @@ -20351,6 +21502,16 @@ + + + + + + + + + + @@ -20380,6 +21541,19 @@ + + + + + + + + + + + + + @@ -20404,6 +21578,14 @@ + + + + + + + + @@ -20420,6 +21602,14 @@ + + + + + + + + @@ -20458,6 +21648,16 @@ + + + + + + + + + + @@ -21533,6 +22733,9 @@ + + + @@ -21671,6 +22874,19 @@ + + + + + + + + + + + + + @@ -22919,6 +24135,14 @@ + + + + + + + + @@ -22927,6 +24151,19 @@ + + + + + + + + + + + + + @@ -23135,6 +24372,9 @@ + + + @@ -23259,6 +24499,9 @@ + + + @@ -23272,6 +24515,9 @@ + + + @@ -23794,6 +25040,11 @@ + + + + + @@ -24035,6 +25286,14 @@ + + + + + + + + @@ -24043,6 +25302,14 @@ + + + + + + + + @@ -24064,6 +25331,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -24096,6 +25400,14 @@ + + + + + + + + @@ -24136,6 +25448,11 @@ + + + + + @@ -24174,6 +25491,16 @@ + + + + + + + + + + @@ -24189,11 +25516,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -24304,6 +25657,14 @@ + + + + + + + + @@ -24360,6 +25721,14 @@ + + + + + + + + @@ -24376,6 +25745,14 @@ + + + + + + + + @@ -24424,6 +25801,14 @@ + + + + + + + + @@ -24528,6 +25913,14 @@ + + + + + + + + @@ -24618,6 +26011,14 @@ + + + + + + + + @@ -24728,6 +26129,9 @@ + + + @@ -24748,6 +26152,22 @@ + + + + + + + + + + + + + + + + @@ -24812,6 +26232,14 @@ + + + + + + + + @@ -24925,6 +26353,14 @@ + + + + + + + + @@ -25035,6 +26471,9 @@ + + + @@ -25103,6 +26542,14 @@ + + + + + + + + @@ -25213,6 +26660,9 @@ + + + @@ -25330,6 +26780,14 @@ + + + + + + + + @@ -25394,6 +26852,14 @@ + + + + + + + + @@ -25458,6 +26924,14 @@ + + + + + + + + @@ -25540,6 +27014,14 @@ + + + + + + + + @@ -25604,6 +27086,14 @@ + + + + + + + + @@ -25668,6 +27158,14 @@ + + + + + + + + @@ -25811,6 +27309,9 @@ + + + @@ -25895,6 +27396,14 @@ + + + + + + + + @@ -26007,6 +27516,14 @@ + + + + + + + + @@ -26071,6 +27588,14 @@ + + + + + + + + @@ -26135,6 +27660,14 @@ + + + + + + + + @@ -26199,6 +27732,14 @@ + + + + + + + + @@ -26277,6 +27818,9 @@ + + + @@ -26369,6 +27913,14 @@ + + + + + + + + @@ -26625,6 +28177,14 @@ + + + + + + + + @@ -26689,6 +28249,14 @@ + + + + + + + + @@ -26889,6 +28457,14 @@ + + + + + + + + @@ -26981,6 +28557,11 @@ + + + + + @@ -26996,6 +28577,11 @@ + + + + + @@ -27101,6 +28687,9 @@ + + + @@ -27204,6 +28793,9 @@ + + + @@ -27273,6 +28865,9 @@ + + + @@ -27341,6 +28936,14 @@ + + + + + + + + @@ -27405,6 +29008,14 @@ + + + + + + + + @@ -27469,6 +29080,14 @@ + + + + + + + + @@ -27591,6 +29210,11 @@ + + + + + @@ -27599,6 +29223,14 @@ + + + + + + + + @@ -28340,6 +29972,9 @@ + + + @@ -28947,6 +30582,14 @@ + + + + + + + + @@ -29008,6 +30651,22 @@ + + + + + + + + + + + + + + + + @@ -29048,6 +30707,14 @@ + + + + + + + + @@ -29188,6 +30855,11 @@ + + + + +