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