Skip to content

Commit bb1aaf9

Browse files
authored
Merge pull request #6024 from anakin78z/share-suggestions
Add support for Direct Share targets.
2 parents 49344bd + afaf9cd commit bb1aaf9

15 files changed

Lines changed: 2079 additions & 4 deletions

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ dependencies {
314314
}
315315

316316
implementation("androidx.core:core-ktx:1.18.0")
317+
implementation("androidx.sharetarget:sharetarget:1.2.0")
317318
implementation("androidx.activity:activity-ktx:1.13.0")
318319
implementation("com.github.nextcloud.android-common:ui:0.33.2")
319320
implementation("com.github.nextcloud.android-common:core:0.33.2")

app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@
123123
<data android:scheme="content" />
124124
<data android:scheme="file" />
125125
</intent-filter>
126+
127+
<meta-data
128+
android:name="android.app.shortcuts"
129+
android:resource="@xml/shortcuts" />
126130
</activity>
127131

128132
<activity
@@ -156,6 +160,10 @@
156160
android:exported="true"
157161
android:theme="@style/AppTheme">
158162

163+
<meta-data
164+
android:name="android.service.chooser.chooser_target_service"
165+
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
166+
159167
<intent-filter>
160168
<action android:name="android.intent.action.SEND" />
161169
<category android:name="android.intent.category.DEFAULT" />

app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.nextcloud.talk.data.user.model.User
2323
import com.nextcloud.talk.databinding.ActivitySwitchAccountBinding
2424
import com.nextcloud.talk.models.ImportAccount
2525
import com.nextcloud.talk.models.json.participants.Participant
26+
import com.nextcloud.talk.conversationlist.DirectShareHelper
2627
import com.nextcloud.talk.users.UserManager
2728
import com.nextcloud.talk.utils.AccountUtils.findAvailableAccountsOnDevice
2829
import com.nextcloud.talk.utils.AccountUtils.getInformationFromAccount
@@ -93,6 +94,7 @@ class SwitchAccountActivity : BaseActivity() {
9394
reauthorizeFromImport(item.account)
9495
} else {
9596
if (userManager.setUserAsActive(item.user!!).blockingGet()) {
97+
DirectShareHelper.removeAllShareTargetShortcuts(this@SwitchAccountActivity)
9698
cookieManager.cookieStore.removeAll()
9799
finish()
98100
}

app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,7 @@ class MessageInputFragment : Fragment() {
10371037
replaceMentionChipSpans(editable)
10381038
binding.fragmentMessageInputView.inputEditText?.setText("")
10391039
sendStopTypingMessage()
1040+
reportOutgoingDirectShare()
10401041
sendMessage(
10411042
editable.toString(),
10421043
sendWithoutNotification
@@ -1046,6 +1047,23 @@ class MessageInputFragment : Fragment() {
10461047
}
10471048
}
10481049

1050+
private fun reportOutgoingDirectShare() {
1051+
val user = chatActivity.conversationUser ?: return
1052+
val isOneToOne = chatActivity.currentConversation?.type ==
1053+
com.nextcloud.talk.models.json.conversations.ConversationEnums
1054+
.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
1055+
val displayName = chatActivity.currentConversation?.displayName ?: chatActivity.roomToken
1056+
lifecycleScope.launch {
1057+
com.nextcloud.talk.conversationlist.DirectShareHelper.reportOutgoingMessage(
1058+
requireContext(),
1059+
user,
1060+
chatActivity.roomToken,
1061+
displayName,
1062+
isOneToOne
1063+
)
1064+
}
1065+
}
1066+
10491067
private fun sendMessage(message: String, sendWithoutNotification: Boolean) {
10501068
chatActivity.chatViewModel.onMessageSent()
10511069

app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ interface ChatMessageRepository : LifecycleAwareManager {
4242

4343
val roomRefreshFlow: Flow<Unit>
4444

45+
val incomingMessageFlow: Flow<Unit>
46+
4547
// /**
4648
// * Used for informing the user of the underlying processing behind offline support, [String] is the key
4749
// * which is handled in a switch statement in ChatActivity.

app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ import kotlinx.coroutines.flow.take
5050
import retrofit2.HttpException
5151
import java.io.IOException
5252
import javax.inject.Inject
53-
import kotlin.collections.map
5453

5554
@Suppress("LargeClass", "TooManyFunctions")
5655
class OfflineFirstChatRepository @Inject constructor(
@@ -104,6 +103,11 @@ class OfflineFirstChatRepository @Inject constructor(
104103

105104
private val _roomRefreshFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
106105

106+
override val incomingMessageFlow: Flow<Unit>
107+
get() = _incomingMessageFlow
108+
109+
private val _incomingMessageFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
110+
107111
private var newXChatLastCommonRead: Int? = null
108112
private var itIsPaused = false
109113

@@ -493,7 +497,8 @@ class OfflineFirstChatRepository @Inject constructor(
493497
lookIntoFuture: Boolean,
494498
hasHistory: Boolean
495499
) {
496-
val chatMessageEntities = persistChatMessagesAndHandleSystemMessages(chatMessagesJson)
500+
val chatMessageEntities =
501+
persistChatMessagesAndHandleSystemMessages(chatMessagesJson, emitOnIncoming = lookIntoFuture)
497502

498503
val oldestIdFromSync = chatMessageEntities.minByOrNull { it.id }!!.id
499504
val newestIdFromSync = chatMessageEntities.maxByOrNull { it.id }!!.id
@@ -950,7 +955,7 @@ class OfflineFirstChatRepository @Inject constructor(
950955
}
951956

952957
override suspend fun onSignalingChatMessageReceived(chatMessages: List<ChatMessageJson>) {
953-
persistChatMessagesAndHandleSystemMessages(chatMessages)
958+
persistChatMessagesAndHandleSystemMessages(chatMessages, emitOnIncoming = true)
954959

955960
// we assume that the signaling messages are on top of the latest chatblock and include them inside it.
956961
// 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(
963968
}
964969

965970
suspend fun persistChatMessagesAndHandleSystemMessages(
966-
chatMessages: List<ChatMessageJson>
971+
chatMessages: List<ChatMessageJson>,
972+
emitOnIncoming: Boolean = false
967973
): List<ChatMessageEntity> {
968974
handleSystemMessagesThatAffectDatabase(chatMessages)
969975

@@ -973,6 +979,16 @@ class OfflineFirstChatRepository @Inject constructor(
973979

974980
chatDao.upsertChatMessagesAndDeleteTemp(internalConversationId, chatMessageEntities)
975981

982+
if (emitOnIncoming) {
983+
val hasIncomingFromOther = chatMessages.any { msg ->
984+
msg.systemMessageType == ChatMessage.SystemMessageType.DUMMY &&
985+
msg.actorId != currentUser.userId
986+
}
987+
if (hasIncomingFromOther) {
988+
_incomingMessageFlow.emit(Unit)
989+
}
990+
}
991+
976992
return chatMessageEntities
977993
}
978994

app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
2828
import com.nextcloud.talk.chat.ui.model.ChatMessageUi
2929
import com.nextcloud.talk.chat.ui.model.MessageTypeContent
3030
import com.nextcloud.talk.chat.ui.model.toUiModel
31+
import com.nextcloud.talk.application.NextcloudTalkApplication
32+
import com.nextcloud.talk.conversationlist.DirectShareHelper
3133
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
3234
import com.nextcloud.talk.conversationlist.data.network.OfflineFirstConversationsRepository
3335
import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel.Companion.FOLLOWED_THREADS_EXIST
@@ -79,6 +81,7 @@ import kotlinx.coroutines.flow.debounce
7981
import kotlinx.coroutines.flow.distinctUntilChanged
8082
import kotlinx.coroutines.flow.distinctUntilChangedBy
8183
import kotlinx.coroutines.flow.filterNotNull
84+
import kotlinx.coroutines.flow.first
8285
import kotlinx.coroutines.flow.flatMapLatest
8386
import kotlinx.coroutines.flow.launchIn
8487
import kotlinx.coroutines.flow.map
@@ -435,6 +438,7 @@ class ChatViewModel @AssistedInject constructor(
435438
observeMediaPlayerProgressForCompose()
436439
observePinnedMessage()
437440
observeRoomRefresh()
441+
observeIncomingMessages()
438442
}
439443

440444
private fun observeMediaPlayerProgressForCompose() {
@@ -573,6 +577,23 @@ class ChatViewModel @AssistedInject constructor(
573577
.launchIn(viewModelScope)
574578
}
575579

580+
private fun observeIncomingMessages() {
581+
chatRepository.incomingMessageFlow
582+
.onEach {
583+
val (conversation, user) = conversationAndUserFlow.first()
584+
val context = NextcloudTalkApplication.sharedApplication!!
585+
val isOneToOne = conversation.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
586+
DirectShareHelper.reportIncomingMessage(
587+
context,
588+
user,
589+
conversation.token,
590+
conversation.displayName ?: conversation.token,
591+
isOneToOne
592+
)
593+
}
594+
.launchIn(viewModelScope)
595+
}
596+
576597
// val lastCommonReadMessageId = getLastCommonReadFlow.first()
577598

578599
// ------------------------------

app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ class ConversationsListActivity : BaseActivity() {
162162
private var selectedConversation: ConversationModel? = null
163163
private var textToPaste: String? = ""
164164
private var selectedMessageId: String? = null
165+
private var pendingDirectShareToken: String? = null
165166

166167
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
167168
override fun handleOnBackPressed() {
@@ -320,6 +321,21 @@ class ConversationsListActivity : BaseActivity() {
320321
showNotificationWarningState.value = shouldShowNotificationWarning()
321322
showShareToScreenState.value = hasActivityActionSendIntent()
322323

324+
// Home screen shortcut tap: shortcut uses ACTION_VIEW with KEY_ROOM_TOKEN to open the
325+
// conversation directly without showing the share picker.
326+
if (Intent.ACTION_VIEW == intent.action && intent.hasExtra(KEY_ROOM_TOKEN)) {
327+
pendingDirectShareToken = intent.getStringExtra(KEY_ROOM_TOKEN)
328+
}
329+
330+
// Share sheet shortcut tap: Android delivers ACTION_SEND with EXTRA_SHORTCUT_ID.
331+
// Extract the room token from the shortcut ID so we can pre-select the conversation.
332+
if (hasActivityActionSendIntent()) {
333+
val shortcutId = intent.getStringExtra(Intent.EXTRA_SHORTCUT_ID)
334+
if (shortcutId != null) {
335+
pendingDirectShareToken = DirectShareHelper.extractTokenFromShortcutId(shortcutId)
336+
}
337+
}
338+
323339
if (!eventBus.isRegistered(this)) {
324340
eventBus.register(this)
325341
}
@@ -377,11 +393,31 @@ class ConversationsListActivity : BaseActivity() {
377393
lifecycleScope.launch {
378394
conversationsListViewModel.getRoomsFlow
379395
.onEach { list ->
396+
// Add app shortcut for note to self
380397
val noteToSelf = list
381398
.firstOrNull { ConversationUtils.isNoteToSelfConversation(it) }
382399
val isNoteToSelfAvailable = noteToSelf != null
383400
handleNoteToSelfShortcut(isNoteToSelfAvailable, noteToSelf?.token ?: "")
384401

402+
// Update Direct Share targets
403+
if (currentUser != null) {
404+
lifecycleScope.launch {
405+
DirectShareHelper.publishShareTargetShortcuts(
406+
context,
407+
currentUser!!,
408+
list
409+
)
410+
}
411+
}
412+
413+
// check for Direct Share
414+
val token = pendingDirectShareToken
415+
if (token != null) {
416+
pendingDirectShareToken = null
417+
val conversation = list.firstOrNull { it.token == token }
418+
if (conversation != null) handleConversation(conversation)
419+
}
420+
385421
if (!scrollPositionRestored) {
386422
scrollPositionRestored = true
387423
val pair = appPreferences.conversationListPositionAndOffset

0 commit comments

Comments
 (0)