Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@
<data android:scheme="content" />
<data android:scheme="file" />
</intent-filter>

<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>

<activity
Expand Down Expand Up @@ -156,6 +160,10 @@
android:exported="true"
android:theme="@style/AppTheme">

<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />

<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,7 @@ class MessageInputFragment : Fragment() {
replaceMentionChipSpans(editable)
binding.fragmentMessageInputView.inputEditText?.setText("")
sendStopTypingMessage()
reportOutgoingDirectShare()
sendMessage(
editable.toString(),
sendWithoutNotification
Expand All @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ interface ChatMessageRepository : LifecycleAwareManager {

val roomRefreshFlow: Flow<Unit>

val incomingMessageFlow: Flow<Unit>

// /**
// * 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -104,6 +103,11 @@ class OfflineFirstChatRepository @Inject constructor(

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

override val incomingMessageFlow: Flow<Unit>
get() = _incomingMessageFlow

private val _incomingMessageFlow: MutableSharedFlow<Unit> = MutableSharedFlow()

private var newXChatLastCommonRead: Int? = null
private var itIsPaused = false

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -950,7 +955,7 @@ class OfflineFirstChatRepository @Inject constructor(
}

override suspend fun onSignalingChatMessageReceived(chatMessages: List<ChatMessageJson>) {
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
Expand All @@ -963,7 +968,8 @@ class OfflineFirstChatRepository @Inject constructor(
}

suspend fun persistChatMessagesAndHandleSystemMessages(
chatMessages: List<ChatMessageJson>
chatMessages: List<ChatMessageJson>,
emitOnIncoming: Boolean = false
): List<ChatMessageEntity> {
handleSystemMessagesThatAffectDatabase(chatMessages)

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -435,6 +438,7 @@ class ChatViewModel @AssistedInject constructor(
observeMediaPlayerProgressForCompose()
observePinnedMessage()
observeRoomRefresh()
observeIncomingMessages()
}

private fun observeMediaPlayerProgressForCompose() {
Expand Down Expand Up @@ -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()

// ------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading