Merge pull request #4300 from nextcloud/feature/4299/improveOfflineSupport

Feature/4299/improve offline support
This commit is contained in:
Marcel Hibbe 2024-10-21 16:06:29 +02:00 committed by GitHub
commit a51875098b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 258 additions and 169 deletions

View file

@ -183,6 +183,7 @@ import io.reactivex.Observer
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -416,7 +417,12 @@ class ChatActivity :
messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java] messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java]
binding.progressBar.visibility = View.VISIBLE this.lifecycleScope.launch {
delay(DELAY_TO_SHOW_PROGRESS_BAR)
if (adapter?.isEmpty == true) {
binding.progressBar.visibility = View.VISIBLE
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback) onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
@ -1244,9 +1250,7 @@ class ChatActivity :
@Suppress("MagicNumber", "LongMethod") @Suppress("MagicNumber", "LongMethod")
private fun updateTypingIndicator() { private fun updateTypingIndicator() {
fun ellipsize(text: String): String { fun ellipsize(text: String): String = DisplayUtils.ellipsize(text, TYPING_INDICATOR_MAX_NAME_LENGTH)
return DisplayUtils.ellipsize(text, TYPING_INDICATOR_MAX_NAME_LENGTH)
}
val participantNames = ArrayList<String>() val participantNames = ArrayList<String>()
@ -1320,10 +1324,9 @@ class ChatActivity :
} }
} }
private fun isTypingStatusEnabled(): Boolean { private fun isTypingStatusEnabled(): Boolean =
return webSocketInstance != null && webSocketInstance != null &&
!CapabilitiesUtil.isTypingStatusPrivate(conversationUser!!) !CapabilitiesUtil.isTypingStatusPrivate(conversationUser!!)
}
private fun setupSwipeToReply() { private fun setupSwipeToReply() {
if (this::participantPermissions.isInitialized && if (this::participantPermissions.isInitialized &&
@ -1422,15 +1425,18 @@ class ChatActivity :
} }
fun isOneToOneConversation() = fun isOneToOneConversation() =
currentConversation != null && currentConversation?.type != null && currentConversation != null &&
currentConversation?.type != null &&
currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
private fun isGroupConversation() = private fun isGroupConversation() =
currentConversation != null && currentConversation?.type != null && currentConversation != null &&
currentConversation?.type != null &&
currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL
private fun isPublicConversation() = private fun isPublicConversation() =
currentConversation != null && currentConversation?.type != null && currentConversation != null &&
currentConversation?.type != null &&
currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL
private fun updateRoomTimerHandler() { private fun updateRoomTimerHandler() {
@ -1668,11 +1674,10 @@ class ChatActivity :
adapter?.notifyDataSetChanged() adapter?.notifyDataSetChanged()
} }
private fun isChildOfExpandableSystemMessage(chatMessage: ChatMessage): Boolean { private fun isChildOfExpandableSystemMessage(chatMessage: ChatMessage): Boolean =
return isSystemMessage(chatMessage) && isSystemMessage(chatMessage) &&
!chatMessage.expandableParent && !chatMessage.expandableParent &&
chatMessage.lastItemOfExpandableGroup != 0 chatMessage.lastItemOfExpandableGroup != 0
}
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
override fun expandSystemMessage(chatMessageToExpand: ChatMessage) { override fun expandSystemMessage(chatMessageToExpand: ChatMessage) {
@ -1758,12 +1763,11 @@ class ChatActivity :
} }
} }
fun isRecordAudioPermissionGranted(): Boolean { fun isRecordAudioPermissionGranted(): Boolean =
return PermissionChecker.checkSelfPermission( PermissionChecker.checkSelfPermission(
context, context,
Manifest.permission.RECORD_AUDIO Manifest.permission.RECORD_AUDIO
) == PERMISSION_GRANTED ) == PERMISSION_GRANTED
}
fun requestRecordAudioPermissions() { fun requestRecordAudioPermissions() {
requestPermissions( requestPermissions(
@ -1870,11 +1874,10 @@ class ChatActivity :
} }
} }
private fun isReadOnlyConversation(): Boolean { private fun isReadOnlyConversation(): Boolean =
return currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState != null &&
currentConversation?.conversationReadOnlyState == currentConversation?.conversationReadOnlyState ==
ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY
}
private fun checkLobbyState() { private fun checkLobbyState() {
if (currentConversation != null && if (currentConversation != null &&
@ -1890,7 +1893,8 @@ class ChatActivity :
sb.append(resources!!.getText(R.string.nc_lobby_waiting)) sb.append(resources!!.getText(R.string.nc_lobby_waiting))
.append("\n\n") .append("\n\n")
if (currentConversation?.lobbyTimer != null && currentConversation?.lobbyTimer != if (currentConversation?.lobbyTimer != null &&
currentConversation?.lobbyTimer !=
0L 0L
) { ) {
val timestampMS = (currentConversation?.lobbyTimer ?: 0) * DateConstants.SECOND_DIVIDER val timestampMS = (currentConversation?.lobbyTimer ?: 0) * DateConstants.SECOND_DIVIDER
@ -2089,7 +2093,7 @@ class ChatActivity :
if (position != null && position >= 0) { if (position != null && position >= 0) {
binding.messagesListView.scrollToPosition(position) binding.messagesListView.scrollToPosition(position)
} else { } else {
// TODO show error that we don't have that message? Log.d(TAG, "message $messageId that should be scrolled to was not found (scrollToMessageWithId)")
} }
} }
@ -2101,6 +2105,12 @@ class ChatActivity :
position, position,
binding.messagesListView.height / 2 binding.messagesListView.height / 2
) )
} else {
Log.d(
TAG,
"message $messageId that should be scrolled to was not found " +
"(scrollToAndCenterMessageWithId)"
)
} }
} }
} }
@ -2264,11 +2274,10 @@ class ChatActivity :
startActivity(intent) startActivity(intent)
} }
private fun validSessionId(): Boolean { private fun validSessionId(): Boolean =
return currentConversation != null && currentConversation != null &&
sessionIdAfterRoomJoined?.isNotEmpty() == true && sessionIdAfterRoomJoined?.isNotEmpty() == true &&
sessionIdAfterRoomJoined != "0" sessionIdAfterRoomJoined != "0"
}
@Suppress("Detekt.TooGenericExceptionCaught") @Suppress("Detekt.TooGenericExceptionCaught")
private fun cancelNotificationsForCurrentConversation() { private fun cancelNotificationsForCurrentConversation() {
@ -2321,14 +2330,11 @@ class ChatActivity :
} }
} }
private fun isActivityNotChangingConfigurations(): Boolean { private fun isActivityNotChangingConfigurations(): Boolean = !isChangingConfigurations
return !isChangingConfigurations
}
private fun isNotInCall(): Boolean { private fun isNotInCall(): Boolean =
return !ApplicationWideCurrentRoomHolder.getInstance().isInCall && !ApplicationWideCurrentRoomHolder.getInstance().isInCall &&
!ApplicationWideCurrentRoomHolder.getInstance().isDialing !ApplicationWideCurrentRoomHolder.getInstance().isDialing
}
private fun setActionBarTitle() { private fun setActionBarTitle() {
val title = binding.chatToolbar.findViewById<TextView>(R.id.chat_toolbar_title) val title = binding.chatToolbar.findViewById<TextView>(R.id.chat_toolbar_title)
@ -2769,11 +2775,10 @@ class ChatActivity :
} }
} }
private fun isSameDayNonSystemMessages(messageLeft: ChatMessage, messageRight: ChatMessage): Boolean { private fun isSameDayNonSystemMessages(messageLeft: ChatMessage, messageRight: ChatMessage): Boolean =
return TextUtils.isEmpty(messageLeft.systemMessage) && TextUtils.isEmpty(messageLeft.systemMessage) &&
TextUtils.isEmpty(messageRight.systemMessage) && TextUtils.isEmpty(messageRight.systemMessage) &&
DateFormatter.isSameDay(messageLeft.createdAt, messageRight.createdAt) DateFormatter.isSameDay(messageLeft.createdAt, messageRight.createdAt)
}
override fun onLoadMore(page: Int, totalItemsCount: Int) { override fun onLoadMore(page: Int, totalItemsCount: Int) {
val id = ( val id = (
@ -2793,15 +2798,14 @@ class ChatActivity :
) )
} }
override fun format(date: Date): String { override fun format(date: Date): String =
return if (DateFormatter.isToday(date)) { if (DateFormatter.isToday(date)) {
resources!!.getString(R.string.nc_date_header_today) resources!!.getString(R.string.nc_date_header_today)
} else if (DateFormatter.isYesterday(date)) { } else if (DateFormatter.isYesterday(date)) {
resources!!.getString(R.string.nc_date_header_yesterday) resources!!.getString(R.string.nc_date_header_yesterday)
} else { } else {
DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR) DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR)
} }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
@ -2863,8 +2867,8 @@ class ChatActivity :
return true return true
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean =
return when (item.itemId) { when (item.itemId) {
R.id.conversation_video_call -> { R.id.conversation_video_call -> {
startACall(false, false) startACall(false, false)
true true
@ -2892,7 +2896,6 @@ class ChatActivity :
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
}
private fun showSharedItems() { private fun showSharedItems() {
val intent = Intent(this, SharedItemsActivity::class.java) val intent = Intent(this, SharedItemsActivity::class.java)
@ -2954,25 +2957,23 @@ class ChatActivity :
return chatMessageMap.values.toList() return chatMessageMap.values.toList()
} }
private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean { private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean =
return currentMessage.value.parentMessageId != null && currentMessage.value.systemMessageType == ChatMessage currentMessage.value.parentMessageId != null &&
.SystemMessageType.MESSAGE_DELETED currentMessage.value.systemMessageType == ChatMessage
} .SystemMessageType.MESSAGE_DELETED
private fun isReactionsMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean { private fun isReactionsMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean =
return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION || currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION ||
currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED || currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED ||
currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED
}
private fun isEditMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean { private fun isEditMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean =
return currentMessage.value.parentMessageId != null && currentMessage.value.systemMessageType == ChatMessage currentMessage.value.parentMessageId != null &&
.SystemMessageType.MESSAGE_EDITED currentMessage.value.systemMessageType == ChatMessage
} .SystemMessageType.MESSAGE_EDITED
private fun isPollVotedMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean { private fun isPollVotedMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean =
return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.POLL_VOTED currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.POLL_VOTED
}
private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) { private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
currentConversation?.let { currentConversation?.let {
@ -3076,9 +3077,8 @@ class ChatActivity :
} }
} }
private fun isSystemMessage(message: ChatMessage): Boolean { private fun isSystemMessage(message: ChatMessage): Boolean =
return ChatMessage.MessageType.SYSTEM_MESSAGE == message.getCalculateMessageType() ChatMessage.MessageType.SYSTEM_MESSAGE == message.getCalculateMessageType()
}
fun deleteMessage(message: IMessage) { fun deleteMessage(message: IMessage) {
if (!participantPermissions.hasChatPermission()) { if (!participantPermissions.hasChatPermission()) {
@ -3321,20 +3321,26 @@ class ChatActivity :
fileViewerUtils.openFileInFilesApp(link!!, keyID!!) fileViewerUtils.openFileInFilesApp(link!!, keyID!!)
} }
private fun hasVisibleItems(message: ChatMessage): Boolean { private fun hasVisibleItems(message: ChatMessage): Boolean =
return !message.isDeleted || // copy message !message.isDeleted ||
message.replyable || // reply to // copy message
message.replyable && // reply privately message.replyable ||
conversationUser?.userId?.isNotEmpty() == true && conversationUser!!.userId != "?" && // reply to
message.replyable &&
// reply privately
conversationUser?.userId?.isNotEmpty() == true &&
conversationUser!!.userId != "?" &&
message.user.id.startsWith("users/") && message.user.id.startsWith("users/") &&
message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId && message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
isShowMessageDeletionButton(message) || // delete isShowMessageDeletionButton(message) ||
ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() || // forward // delete
message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && // mark as unread ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() ||
// forward
message.previousMessageId > NO_PREVIOUS_MESSAGE_ID &&
// mark as unread
ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() && ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() &&
BuildConfig.DEBUG BuildConfig.DEBUG
}
private fun setMessageAsDeleted(message: IMessage?) { private fun setMessageAsDeleted(message: IMessage?) {
val messageTemp = message as ChatMessage val messageTemp = message as ChatMessage
@ -3452,8 +3458,8 @@ class ChatActivity :
return isUserAllowedByPrivileges return isUserAllowedByPrivileges
} }
override fun hasContentFor(message: ChatMessage, type: Byte): Boolean { override fun hasContentFor(message: ChatMessage, type: Byte): Boolean =
return when (type) { when (type) {
CONTENT_TYPE_LOCATION -> message.hasGeoLocation() CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage
CONTENT_TYPE_POLL -> message.isPoll() CONTENT_TYPE_POLL -> message.isPoll()
@ -3464,7 +3470,6 @@ class ChatActivity :
else -> false else -> false
} }
}
private fun processMostRecentMessage(recent: ChatMessage, chatMessageList: List<ChatMessage>) { private fun processMostRecentMessage(recent: ChatMessage, chatMessageList: List<ChatMessage>) {
when (recent.systemMessageType) { when (recent.systemMessageType) {
@ -3712,5 +3717,6 @@ class ChatActivity :
private const val CURRENT_AUDIO_POSITION_KEY = "CURRENT_AUDIO_POSITION" private const val CURRENT_AUDIO_POSITION_KEY = "CURRENT_AUDIO_POSITION"
private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING" private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING"
private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG" private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG"
private const val DELAY_TO_SHOW_PROGRESS_BAR = 1000L
} }
} }

View file

@ -56,10 +56,8 @@ interface ChatMessageRepository : LifecycleAwareManager {
* Long polls the server for any updates to the chat, if found, it synchronizes * Long polls the server for any updates to the chat, if found, it synchronizes
* the database with the server and emits the new messages to [messageFlow], * the database with the server and emits the new messages to [messageFlow],
* else it simply retries after timeout. * else it simply retries after timeout.
*
* [withNetworkParams] credentials and url.
*/ */
fun initMessagePolling(): Job fun initMessagePolling(initialMessageId: Long): Job
/** /**
* Gets a individual message. * Gets a individual message.

View file

@ -108,38 +108,101 @@ class OfflineFirstChatRepository @Inject constructor(
override fun loadInitialMessages(withNetworkParams: Bundle): Job = override fun loadInitialMessages(withNetworkParams: Bundle): Job =
scope.launch { scope.launch {
Log.d(TAG, "---- loadInitialMessages ------------") Log.d(TAG, "---- loadInitialMessages ------------")
newXChatLastCommonRead = conversationModel.lastCommonReadMessage newXChatLastCommonRead = conversationModel.lastCommonReadMessage
val fieldMap = getFieldMap( Log.d(TAG, "conversationModel.internalId: " + conversationModel.internalId)
lookIntoFuture = false, Log.d(TAG, "conversationModel.lastReadMessage:" + conversationModel.lastReadMessage)
includeLastKnown = true,
setReadMarker = true,
lastKnown = null
)
withNetworkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap)
withNetworkParams.putString(BundleKeys.KEY_ROOM_TOKEN, conversationModel.token)
sync(withNetworkParams) var newestMessageIdFromDb = chatDao.getNewestMessageId(internalConversationId)
Log.d(TAG, "newestMessageIdFromDb: $newestMessageIdFromDb")
val newestMessageId = chatDao.getNewestMessageId(internalConversationId) val weAlreadyHaveSomeOfflineMessages = newestMessageIdFromDb > 0
Log.d(TAG, "newestMessageId after sync: $newestMessageId") val weHaveAtLeastTheLastReadMessage = newestMessageIdFromDb >= conversationModel.lastReadMessage.toLong()
Log.d(TAG, "weAlreadyHaveSomeOfflineMessages:$weAlreadyHaveSomeOfflineMessages")
Log.d(TAG, "weHaveAtLeastTheLastReadMessage:$weHaveAtLeastTheLastReadMessage")
showLast100MessagesBeforeAndEqual( if (weAlreadyHaveSomeOfflineMessages && weHaveAtLeastTheLastReadMessage) {
internalConversationId, Log.d(
chatDao.getNewestMessageId(internalConversationId) TAG,
) "Initial online request is skipped because offline messages are up to date" +
" until lastReadMessage"
)
Log.d(TAG, "For messages newer than lastRead, lookIntoFuture will load them.")
} else {
if (!weAlreadyHaveSomeOfflineMessages) {
Log.d(TAG, "An online request for newest 100 messages is made because offline chat is empty")
} else {
Log.d(
TAG,
"An online request for newest 100 messages is made because we don't have the lastReadMessage " +
"(gaps could be closed by scrolling up to merge the chatblocks)"
)
}
// delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing // set up field map to load the newest messages
// with them (otherwise there is a race condition). val fieldMap = getFieldMap(
delay(DELAY_TO_ENSURE_MESSAGES_ARE_ADDED) lookIntoFuture = false,
includeLastKnown = true,
setReadMarker = true,
lastKnown = null
)
withNetworkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap)
withNetworkParams.putString(BundleKeys.KEY_ROOM_TOKEN, conversationModel.token)
updateUiForLastCommonRead() Log.d(TAG, "Starting online request for initial loading")
updateUiForLastReadMessage(newestMessageId) val chatMessageEntities = sync(withNetworkParams)
if (chatMessageEntities == null) {
Log.e(TAG, "initial loading of messages failed")
}
initMessagePolling() newestMessageIdFromDb = chatDao.getNewestMessageId(internalConversationId)
Log.d(TAG, "newestMessageIdFromDb after sync: $newestMessageIdFromDb")
}
if (newestMessageIdFromDb.toInt() != 0) {
val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb)
showMessagesBeforeAndEqual(
internalConversationId,
newestMessageIdFromDb,
limit
)
// delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing
// with them (otherwise there is a race condition).
delay(DELAY_TO_ENSURE_MESSAGES_ARE_ADDED)
updateUiForLastCommonRead()
updateUiForLastReadMessage(newestMessageIdFromDb)
}
initMessagePolling(newestMessageIdFromDb)
} }
private suspend fun getCappedMessagesAmountOfChatBlock(messageId: Long): Int {
val chatBlock = getBlockOfMessage(messageId.toInt())
if (chatBlock != null) {
val amountBetween = chatDao.getCountBetweenMessageIds(
internalConversationId,
messageId,
chatBlock.oldestMessageId
)
Log.d(TAG, "amount of messages between newestMessageId and oldest message of same ChatBlock:$amountBetween")
val limit = if (amountBetween > DEFAULT_MESSAGES_LIMIT) {
DEFAULT_MESSAGES_LIMIT
} else {
amountBetween
}
Log.d(TAG, "limit of messages to load for UI (max 100 to ensure performance is okay):$limit")
return limit
} else {
Log.e(TAG, "No chat block found. Returning 0 as limit.")
return 0
}
}
private suspend fun updateUiForLastReadMessage(newestMessageId: Long) { private suspend fun updateUiForLastReadMessage(newestMessageId: Long) {
val scrollToLastRead = conversationModel.lastReadMessage.toLong() < newestMessageId val scrollToLastRead = conversationModel.lastReadMessage.toLong() < newestMessageId
if (scrollToLastRead) { if (scrollToLastRead) {
@ -175,25 +238,25 @@ class OfflineFirstChatRepository @Inject constructor(
val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId) val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId)
if (loadFromServer) { if (loadFromServer) {
Log.d(TAG, "Starting online request for loadMoreMessages")
sync(withNetworkParams) sync(withNetworkParams)
} }
showLast100MessagesBefore(internalConversationId, beforeMessageId) showMessagesBefore(internalConversationId, beforeMessageId, DEFAULT_MESSAGES_LIMIT)
updateUiForLastCommonRead() updateUiForLastCommonRead()
} }
override fun initMessagePolling(): Job = override fun initMessagePolling(initialMessageId: Long): Job =
scope.launch { scope.launch {
Log.d(TAG, "---- initMessagePolling ------------") Log.d(TAG, "---- initMessagePolling ------------")
val initialMessageId = chatDao.getNewestMessageId(internalConversationId).toInt()
Log.d(TAG, "newestMessage: $initialMessageId") Log.d(TAG, "newestMessage: $initialMessageId")
var fieldMap = getFieldMap( var fieldMap = getFieldMap(
lookIntoFuture = true, lookIntoFuture = true,
includeLastKnown = false, includeLastKnown = false,
setReadMarker = true, setReadMarker = true,
lastKnown = initialMessageId lastKnown = initialMessageId.toInt()
) )
val networkParams = Bundle() val networkParams = Bundle()
@ -205,6 +268,7 @@ class OfflineFirstChatRepository @Inject constructor(
// sync database with server (This is a long blocking call because long polling (lookIntoFuture) is set) // sync database with server (This is a long blocking call because long polling (lookIntoFuture) is set)
networkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap) networkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap)
Log.d(TAG, "Starting online request for long polling")
val resultsFromSync = sync(networkParams) val resultsFromSync = sync(networkParams)
if (!resultsFromSync.isNullOrEmpty()) { if (!resultsFromSync.isNullOrEmpty()) {
val chatMessages = resultsFromSync.map(ChatMessageEntity::asModel) val chatMessages = resultsFromSync.map(ChatMessageEntity::asModel)
@ -240,15 +304,15 @@ class OfflineFirstChatRepository @Inject constructor(
loadFromServer = false loadFromServer = false
} else { } else {
// we know that beforeMessageId and blockForMessage.oldestMessageId are in the same block. // we know that beforeMessageId and blockForMessage.oldestMessageId are in the same block.
// As we want the last 100 entries before beforeMessageId, we calculate if these messages are 100 // As we want the last DEFAULT_MESSAGES_LIMIT entries before beforeMessageId, we calculate if these
// entries apart from each other // messages are DEFAULT_MESSAGES_LIMIT entries apart from each other
val amountBetween = chatDao.getCountBetweenMessageIds( val amountBetween = chatDao.getCountBetweenMessageIds(
internalConversationId, internalConversationId,
beforeMessageId, beforeMessageId,
blockForMessage.oldestMessageId blockForMessage.oldestMessageId
) )
loadFromServer = amountBetween < 100 loadFromServer = amountBetween < DEFAULT_MESSAGES_LIMIT
Log.d( Log.d(
TAG, TAG,
@ -263,7 +327,8 @@ class OfflineFirstChatRepository @Inject constructor(
lookIntoFuture: Boolean, lookIntoFuture: Boolean,
includeLastKnown: Boolean, includeLastKnown: Boolean,
setReadMarker: Boolean, setReadMarker: Boolean,
lastKnown: Int? lastKnown: Int?,
limit: Int = DEFAULT_MESSAGES_LIMIT
): HashMap<String, Int> { ): HashMap<String, Int> {
val fieldMap = HashMap<String, Int>() val fieldMap = HashMap<String, Int>()
@ -278,7 +343,7 @@ class OfflineFirstChatRepository @Inject constructor(
} }
fieldMap["timeout"] = if (lookIntoFuture) 30 else 0 fieldMap["timeout"] = if (lookIntoFuture) 30 else 0
fieldMap["limit"] = 100 fieldMap["limit"] = limit
fieldMap["lookIntoFuture"] = if (lookIntoFuture) 1 else 0 fieldMap["lookIntoFuture"] = if (lookIntoFuture) 1 else 0
fieldMap["setReadMarker"] = if (setReadMarker) 1 else 0 fieldMap["setReadMarker"] = if (setReadMarker) 1 else 0
@ -294,73 +359,84 @@ class OfflineFirstChatRepository @Inject constructor(
lookIntoFuture = false, lookIntoFuture = false,
includeLastKnown = true, includeLastKnown = true,
setReadMarker = false, setReadMarker = false,
lastKnown = messageId.toInt() lastKnown = messageId.toInt(),
limit = 1
) )
bundle.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap) bundle.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap)
// Although only the single message will be returned, a server request will load 100 messages. Log.d(TAG, "Starting online request for single message (e.g. a reply)")
// If this turns out to be confusion for debugging we could load set the limit to 1 for this request.
sync(bundle) sync(bundle)
} }
return chatDao.getChatMessageForConversation(internalConversationId, messageId) return chatDao.getChatMessageForConversation(internalConversationId, messageId)
.map(ChatMessageEntity::asModel) .map(ChatMessageEntity::asModel)
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST", "MagicNumber")
private fun getMessagesFromServer(bundle: Bundle): Pair<Int, List<ChatMessageJson>>? { private fun getMessagesFromServer(bundle: Bundle): Pair<Int, List<ChatMessageJson>>? {
Log.d(TAG, "An online request is made!!!!!!!!!!!!!!!!!!!!")
val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap<String, Int> val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap<String, Int>
try { var attempts = 1
val result = network.pullChatMessages(credentials, urlForChatting, fieldMap) while (attempts < 5) {
.subscribeOn(Schedulers.io()) Log.d(TAG, "message limit: " + fieldMap["limit"])
.observeOn(AndroidSchedulers.mainThread()) try {
// .timeout(3, TimeUnit.SECONDS) val result = network.pullChatMessages(credentials, urlForChatting, fieldMap)
.map { it -> .subscribeOn(Schedulers.io())
when (it.code()) { .observeOn(AndroidSchedulers.mainThread())
HTTP_CODE_OK -> { .map { it ->
Log.d(TAG, "getMessagesFromServer HTTP_CODE_OK") when (it.code()) {
newXChatLastCommonRead = it.headers()["X-Chat-Last-Common-Read"]?.let { HTTP_CODE_OK -> {
Integer.parseInt(it) Log.d(TAG, "getMessagesFromServer HTTP_CODE_OK")
newXChatLastCommonRead = it.headers()["X-Chat-Last-Common-Read"]?.let {
Integer.parseInt(it)
}
return@map Pair(
HTTP_CODE_OK,
(it.body() as ChatOverall).ocs!!.data!!
)
} }
return@map Pair( HTTP_CODE_NOT_MODIFIED -> {
HTTP_CODE_OK, Log.d(TAG, "getMessagesFromServer HTTP_CODE_NOT_MODIFIED")
(it.body() as ChatOverall).ocs!!.data!!
)
}
HTTP_CODE_NOT_MODIFIED -> { return@map Pair(
Log.d(TAG, "getMessagesFromServer HTTP_CODE_NOT_MODIFIED") HTTP_CODE_NOT_MODIFIED,
listOf<ChatMessageJson>()
)
}
return@map Pair( HTTP_CODE_PRECONDITION_FAILED -> {
HTTP_CODE_NOT_MODIFIED, Log.d(TAG, "getMessagesFromServer HTTP_CODE_PRECONDITION_FAILED")
listOf<ChatMessageJson>()
)
}
HTTP_CODE_PRECONDITION_FAILED -> { return@map Pair(
Log.d(TAG, "getMessagesFromServer HTTP_CODE_PRECONDITION_FAILED") HTTP_CODE_PRECONDITION_FAILED,
listOf<ChatMessageJson>()
)
}
return@map Pair( else -> {
HTTP_CODE_PRECONDITION_FAILED, return@map Pair(
listOf<ChatMessageJson>() HTTP_CODE_PRECONDITION_FAILED,
) listOf<ChatMessageJson>()
} )
}
else -> {
return@map Pair(
HTTP_CODE_PRECONDITION_FAILED,
listOf<ChatMessageJson>()
)
} }
} }
.blockingSingle()
return result
} catch (e: Exception) {
Log.e(TAG, "Something went wrong when pulling chat messages (attempt: $attempts)", e)
attempts++
val newMessageLimit = when (attempts) {
2 -> 50
3 -> 10
else -> 5
} }
.blockingSingle() fieldMap["limit"] = newMessageLimit
return result }
} catch (e: Exception) {
Log.e(TAG, "Something went wrong when pulling chat messages", e)
} }
Log.e(TAG, "All attempts to get messages from server failed")
return null return null
} }
@ -370,7 +446,12 @@ class OfflineFirstChatRepository @Inject constructor(
return null return null
} }
val result = getMessagesFromServer(bundle) ?: return listOf() val result = getMessagesFromServer(bundle)
if (result == null) {
Log.d(TAG, "No result from server")
return null
}
var chatMessagesFromSync: List<ChatMessageEntity>? = null var chatMessagesFromSync: List<ChatMessageEntity>? = null
val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap<String, Int> val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap<String, Int>
@ -471,7 +552,7 @@ class OfflineFirstChatRepository @Inject constructor(
ChatMessage.SystemMessageType.CLEARED_CHAT -> { ChatMessage.SystemMessageType.CLEARED_CHAT -> {
// for lookIntoFuture just deleting everything would be fine. // for lookIntoFuture just deleting everything would be fine.
// But lets say we did not open the chat for a while and in between it was cleared. // But lets say we did not open the chat for a while and in between it was cleared.
// We just load the last 100 messages but this don't contain the system message. // We just load the last messages but this don't contain the system message.
// We scroll up and load the system message. Deleting everything is not an option as we // We scroll up and load the system message. Deleting everything is not an option as we
// would loose the messages that we want to keep. We only want to // would loose the messages that we want to keep. We only want to
// delete the messages and chatBlocks older than the system message. // delete the messages and chatBlocks older than the system message.
@ -488,13 +569,12 @@ class OfflineFirstChatRepository @Inject constructor(
* 304 is returned when oldest message of chat was queried or when long polling request returned with no * 304 is returned when oldest message of chat was queried or when long polling request returned with no
* modification. hasHistory is only set to false, when 304 was returned for the the oldest message * modification. hasHistory is only set to false, when 304 was returned for the the oldest message
*/ */
private fun getHasHistory(statusCode: Int, lookIntoFuture: Boolean): Boolean { private fun getHasHistory(statusCode: Int, lookIntoFuture: Boolean): Boolean =
return if (statusCode == HTTP_CODE_NOT_MODIFIED) { if (statusCode == HTTP_CODE_NOT_MODIFIED) {
lookIntoFuture lookIntoFuture
} else { } else {
true true
} }
}
private suspend fun getBlockOfMessage(queriedMessageId: Int?): ChatBlockEntity? { private suspend fun getBlockOfMessage(queriedMessageId: Int?): ChatBlockEntity? {
var blockContainingQueriedMessage: ChatBlockEntity? = null var blockContainingQueriedMessage: ChatBlockEntity? = null
@ -563,7 +643,7 @@ class OfflineFirstChatRepository @Inject constructor(
} }
} }
private suspend fun showLast100MessagesBeforeAndEqual(internalConversationId: String, messageId: Long) { private suspend fun showMessagesBeforeAndEqual(internalConversationId: String, messageId: Long, limit: Int) {
suspend fun getMessagesBeforeAndEqual( suspend fun getMessagesBeforeAndEqual(
messageId: Long, messageId: Long,
internalConversationId: String, internalConversationId: String,
@ -580,7 +660,7 @@ class OfflineFirstChatRepository @Inject constructor(
val list = getMessagesBeforeAndEqual( val list = getMessagesBeforeAndEqual(
messageId, messageId,
internalConversationId, internalConversationId,
100 limit
) )
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
@ -589,7 +669,7 @@ class OfflineFirstChatRepository @Inject constructor(
} }
} }
private suspend fun showLast100MessagesBefore(internalConversationId: String, messageId: Long) { private suspend fun showMessagesBefore(internalConversationId: String, messageId: Long, limit: Int) {
suspend fun getMessagesBefore( suspend fun getMessagesBefore(
messageId: Long, messageId: Long,
internalConversationId: String, internalConversationId: String,
@ -606,7 +686,7 @@ class OfflineFirstChatRepository @Inject constructor(
val list = getMessagesBefore( val list = getMessagesBefore(
messageId, messageId,
internalConversationId, internalConversationId,
100 limit
) )
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
@ -638,5 +718,6 @@ class OfflineFirstChatRepository @Inject constructor(
private const val HTTP_CODE_PRECONDITION_FAILED = 412 private const val HTTP_CODE_PRECONDITION_FAILED = 412
private const val HALF_SECOND = 500L private const val HALF_SECOND = 500L
private const val DELAY_TO_ENSURE_MESSAGES_ARE_ADDED: Long = 100 private const val DELAY_TO_ENSURE_MESSAGES_ARE_ADDED: Long = 100
private const val DEFAULT_MESSAGES_LIMIT = 100
} }
} }

View file

@ -225,7 +225,7 @@ class ChatViewModel @Inject constructor(
fun getRoom(user: User, token: String) { fun getRoom(user: User, token: String) {
_getRoomViewState.value = GetRoomStartState _getRoomViewState.value = GetRoomStartState
conversationRepository.getConversationSettings(token) conversationRepository.getRoom(token)
// chatNetworkDataSource.getRoom(user, token) // chatNetworkDataSource.getRoom(user, token)
// .subscribeOn(Schedulers.io()) // .subscribeOn(Schedulers.io())

View file

@ -35,5 +35,5 @@ interface OfflineConversationsRepository {
* Called once onStart to emit a conversation to [conversationFlow] * Called once onStart to emit a conversation to [conversationFlow]
* to be handled asynchronously. * to be handled asynchronously.
*/ */
fun getConversationSettings(roomToken: String): Job fun getRoom(roomToken: String): Job
} }

View file

@ -56,17 +56,19 @@ class OfflineFirstConversationsRepository @Inject constructor(
override fun getRooms(): Job = override fun getRooms(): Job =
scope.launch { scope.launch {
val resultsFromSync = sync() val initialConversationModels = getListOfConversations(user.id!!)
if (!resultsFromSync.isNullOrEmpty()) { _roomListFlow.emit(initialConversationModels)
val conversations = resultsFromSync.map(ConversationEntity::asModel)
_roomListFlow.emit(conversations) if (monitor.isOnline.first()) {
} else { val conversationEntitiesFromSync = getRoomsFromServer()
val conversationsFromDb = getListOfConversations(user.id!!) if (!conversationEntitiesFromSync.isNullOrEmpty()) {
_roomListFlow.emit(conversationsFromDb) val conversationModelsFromSync = conversationEntitiesFromSync.map(ConversationEntity::asModel)
_roomListFlow.emit(conversationModelsFromSync)
}
} }
} }
override fun getConversationSettings(roomToken: String): Job = override fun getRoom(roomToken: String): Job =
scope.launch { scope.launch {
val id = user.id!! val id = user.id!!
val model = getConversation(id, roomToken) val model = getConversation(id, roomToken)
@ -100,7 +102,7 @@ class OfflineFirstConversationsRepository @Inject constructor(
} }
} }
private suspend fun sync(): List<ConversationEntity>? { private suspend fun getRoomsFromServer(): List<ConversationEntity>? {
var conversationsFromSync: List<ConversationEntity>? = null var conversationsFromSync: List<ConversationEntity>? = null
if (!monitor.isOnline.first()) { if (!monitor.isOnline.first()) {
@ -129,10 +131,12 @@ class OfflineFirstConversationsRepository @Inject constructor(
} }
private suspend fun deleteLeftConversations(conversationsFromSync: List<ConversationEntity>) { private suspend fun deleteLeftConversations(conversationsFromSync: List<ConversationEntity>) {
val conversationsFromSyncIds = conversationsFromSync.map { it.internalId }.toSet()
val oldConversationsFromDb = dao.getConversationsForUser(user.id!!).first() val oldConversationsFromDb = dao.getConversationsForUser(user.id!!).first()
val conversationsToDelete = oldConversationsFromDb.filterNot { conversationsFromSync.contains(it) } val conversationIdsToDelete = oldConversationsFromDb
val conversationIdsToDelete = conversationsToDelete.map { it.internalId } .map { it.internalId }
.filterNot { it in conversationsFromSyncIds }
dao.deleteConversations(conversationIdsToDelete) dao.deleteConversations(conversationIdsToDelete)
} }