package awais.instagrabber.managers import android.content.ContentResolver import android.net.Uri import android.util.Log import androidx.core.util.Pair import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations.distinctUntilChanged import androidx.lifecycle.Transformations.map import awais.instagrabber.R import awais.instagrabber.customviews.emoji.Emoji import awais.instagrabber.models.Resource import awais.instagrabber.models.Resource.Companion.error import awais.instagrabber.models.Resource.Companion.loading import awais.instagrabber.models.Resource.Companion.success import awais.instagrabber.models.enums.DirectItemType import awais.instagrabber.repositories.requests.UploadFinishOptions import awais.instagrabber.repositories.requests.VideoOptions import awais.instagrabber.repositories.requests.directmessages.ThreadIdsOrUserIds import awais.instagrabber.repositories.requests.directmessages.ThreadIdsOrUserIds.Companion.of import awais.instagrabber.repositories.responses.User import awais.instagrabber.repositories.responses.directmessages.* import awais.instagrabber.repositories.responses.giphy.GiphyGif import awais.instagrabber.utils.* import awais.instagrabber.utils.MediaUploader.MediaUploadResponse import awais.instagrabber.utils.MediaUploader.uploadPhoto import awais.instagrabber.utils.MediaUploader.uploadVideo import awais.instagrabber.utils.MediaUtils.OnInfoLoadListener import awais.instagrabber.utils.MediaUtils.VideoInfo import awais.instagrabber.utils.TextUtils.isEmpty import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.webservices.DirectMessagesRepository import awais.instagrabber.webservices.FriendshipRepository import awais.instagrabber.webservices.MediaRepository import com.google.common.collect.ImmutableList import com.google.common.collect.Iterables import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import retrofit2.Call import java.io.IOException import java.net.HttpURLConnection import java.util.* import java.util.stream.Collectors class ThreadManager( private val threadId: String, pending: Boolean, private val currentUser: User?, private val contentResolver: ContentResolver, private val viewerId: Long, private val csrfToken: String, private val deviceUuid: String, ) { private val _fetching = MutableLiveData>() val fetching: LiveData> = _fetching private val _replyToItem = MutableLiveData() val replyToItem: LiveData = _replyToItem private val _pendingRequests = MutableLiveData(null) val pendingRequests: LiveData = _pendingRequests private val inboxManager: InboxManager = if (pending) DirectMessagesManager.pendingInboxManager else DirectMessagesManager.inboxManager private val threadIdsOrUserIds: ThreadIdsOrUserIds = of(threadId) private val friendshipRepository: FriendshipRepository by lazy { FriendshipRepository.getInstance() } private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() } private val directMessagesRepository by lazy { DirectMessagesRepository.getInstance() } val thread: LiveData by lazy { distinctUntilChanged(map(inboxManager.getInbox()) { inboxResource: Resource? -> if (inboxResource == null) return@map null val (threads) = inboxResource.data ?: return@map null if (threads.isNullOrEmpty()) return@map null val thread = threads.firstOrNull { it.threadId == threadId } thread?.also { cursor = thread.oldestCursor hasOlder = thread.hasOlder } }) } val inputMode: LiveData by lazy { distinctUntilChanged(map(thread) { it?.inputMode ?: 1 }) } val threadTitle: LiveData by lazy { distinctUntilChanged(map(thread) { it?.threadTitle }) } val users: LiveData> by lazy { distinctUntilChanged(map(thread) { it?.users ?: emptyList() }) } val usersWithCurrent: LiveData> by lazy { distinctUntilChanged(map(thread) { if (it == null) return@map emptyList() getUsersWithCurrentUser(it) }) } val leftUsers: LiveData> by lazy { distinctUntilChanged(map(thread) { it?.leftUsers ?: emptyList() }) } val usersAndLeftUsers: LiveData, List>> by lazy { distinctUntilChanged(map(thread) { if (it == null) return@map Pair, List>(emptyList(), emptyList()) val users = getUsersWithCurrentUser(it) val leftUsers = it.leftUsers Pair(users, leftUsers) }) } val isPending: LiveData by lazy { distinctUntilChanged(map(thread) { it?.pending ?: true }) } val adminUserIds: LiveData> by lazy { distinctUntilChanged(map(thread) { it?.adminUserIds ?: emptyList() }) } val items: LiveData> by lazy { distinctUntilChanged(map(thread) { it?.items ?: emptyList() }) } val isViewerAdmin: LiveData by lazy { distinctUntilChanged(map(thread) { it?.adminUserIds?.contains(viewerId) ?: false }) } val isGroup: LiveData by lazy { distinctUntilChanged(map(thread) { it?.isGroup ?: false }) } val isMuted: LiveData by lazy { distinctUntilChanged(map(thread) { it?.muted ?: false }) } val isApprovalRequiredToJoin: LiveData by lazy { distinctUntilChanged(map(thread) { it?.approvalRequiredForNewMembers ?: false }) } val isMentionsMuted: LiveData by lazy { distinctUntilChanged(map(thread) { it?.mentionsMuted ?: false }) } val pendingRequestsCount: LiveData by lazy { distinctUntilChanged(map(_pendingRequests) { it?.totalParticipantRequests ?: 0 }) } val inviter: LiveData by lazy { distinctUntilChanged(map(thread) { it?.inviter }) } private var hasOlder = true private var cursor: String? = null private var chatsRequest: Call? = null private fun getUsersWithCurrentUser(t: DirectThread): List { val builder = ImmutableList.builder() if (currentUser != null) { builder.add(currentUser) } val users: List? = t.users if (users != null) { builder.addAll(users) } return builder.build() } fun fetchChats(scope: CoroutineScope) { val fetchingValue = _fetching.value if (fetchingValue != null && fetchingValue.status === Resource.Status.LOADING || !hasOlder) return _fetching.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { val threadFeedResponse = directMessagesRepository.fetchThread(threadId, cursor) if (threadFeedResponse.status != null && threadFeedResponse.status != "ok") { _fetching.postValue(error(R.string.generic_not_ok_response, null)) return@launch } val thread = threadFeedResponse.thread if (thread == null) { _fetching.postValue(error("thread is null!", null)) return@launch } setThread(thread) _fetching.postValue(success(Any())) } catch (e: Exception) { Log.e(TAG, "Failed fetching dm chats", e) _fetching.postValue(error(e.message, null)) hasOlder = false } } if (cursor == null) { fetchPendingRequests(scope) } } fun fetchPendingRequests(scope: CoroutineScope) { val isGroup = isGroup.value if (isGroup == null || !isGroup) return scope.launch(Dispatchers.IO) { try { val response = directMessagesRepository.participantRequests(threadId, 1) _pendingRequests.postValue(response) } catch (e: Exception) { Log.e(TAG, "fetchPendingRequests: ", e) } } } private fun setThread(thread: DirectThread, skipItems: Boolean) { // if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) { // fetchPendingRequests(); // } val items = thread.items if (skipItems) { val currentThread = this.thread.value if (currentThread != null) { thread.items = currentThread.items } } if (!skipItems && !cursor.isNullOrBlank()) { val currentThread = this.thread.value if (currentThread != null) { val currentItems = currentThread.items val list = if (currentItems == null) LinkedList() else LinkedList(currentItems) if (items != null) { list.addAll(items) } thread.items = list } } inboxManager.setThread(threadId, thread) } private fun setThread(thread: DirectThread) { setThread(thread, false) } private fun setThreadUsers(users: List?, leftUsers: List?) { val currentThread = thread.value ?: return val thread: DirectThread = try { currentThread.clone() as DirectThread } catch (e: CloneNotSupportedException) { Log.e(TAG, "setThreadUsers: ", e) return } if (users != null) { thread.users = users } if (leftUsers != null) { thread.leftUsers = leftUsers } inboxManager.setThread(threadId, thread) } private fun addItems(index: Int, items: Collection) { inboxManager.addItemsToThread(threadId, index, items) } private fun addReaction(item: DirectItem, emoji: Emoji) { if (currentUser == null) return val isLike = emoji.unicode == "❤️" var reactions = item.reactions reactions = if (reactions == null) { DirectItemReactions(null, null) } else { try { reactions.clone() as DirectItemReactions } catch (e: CloneNotSupportedException) { Log.e(TAG, "addReaction: ", e) return } } if (isLike) { val likes = addEmoji(reactions.likes, null, false) reactions.likes = likes } val emojis = addEmoji(reactions.emojis, emoji.unicode, true) reactions.emojis = emojis val currentItems = items.value val items = if (currentItems == null) LinkedList() else LinkedList(currentItems) val index = getItemIndex(item, items) if (index >= 0) { try { val clone = items[index].clone() as DirectItem clone.reactions = reactions items[index] = clone } catch (e: CloneNotSupportedException) { Log.e(TAG, "addReaction: error cloning", e) } } inboxManager.setItemsToThread(threadId, items) } private fun removeReaction(item: DirectItem) { try { val itemClone = item.clone() as DirectItem val reactions = itemClone.reactions var reactionsClone: DirectItemReactions? = null if (reactions != null) { reactionsClone = reactions.clone() as DirectItemReactions } var likes: List? = null if (reactionsClone != null) { likes = reactionsClone.likes } if (likes != null) { val updatedLikes = likes.stream() .filter { (senderId) -> senderId != viewerId } .collect(Collectors.toList()) if (reactionsClone != null) { reactionsClone.likes = updatedLikes } } var emojis: List? = null if (reactionsClone != null) { emojis = reactionsClone.emojis } if (emojis != null) { val updatedEmojis = emojis.stream() .filter { (senderId) -> senderId != viewerId } .collect(Collectors.toList()) if (reactionsClone != null) { reactionsClone.emojis = updatedEmojis } } itemClone.reactions = reactionsClone val items = items.value val list = if (items == null) LinkedList() else LinkedList(items) val index = getItemIndex(item, list) if (index >= 0) { list[index] = itemClone } inboxManager.setItemsToThread(threadId, list) } catch (e: Exception) { Log.e(TAG, "removeReaction: ", e) } } private fun removeItem(item: DirectItem): Int { val items = items.value val list = if (items == null) LinkedList() else LinkedList(items) val index = getItemIndex(item, list) if (index >= 0) { list.removeAt(index) inboxManager.setItemsToThread(threadId, list) } return index } private fun addEmoji( reactionList: List?, emoji: String?, shouldReplaceIfAlreadyReacted: Boolean, ): List? { if (currentUser == null) return reactionList val temp: MutableList = if (reactionList == null) ArrayList() else ArrayList(reactionList) var index = -1 for (i in temp.indices) { val (senderId) = temp[i] if (senderId == currentUser.pk) { index = i break } } val reaction = DirectItemEmojiReaction( currentUser.pk, System.currentTimeMillis() * 1000, emoji, "none" ) if (index < 0) { temp.add(0, reaction) } else if (shouldReplaceIfAlreadyReacted) { temp[index] = reaction } return temp } fun sendText(text: String, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() val userId = getCurrentUserId(data) ?: return data val clientContext = UUID.randomUUID().toString() val replyToItemValue = _replyToItem.value val directItem = createText(userId, clientContext, text, replyToItemValue) // Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId()); directItem.isPending = true addItems(0, listOf(directItem)) data.postValue(loading(directItem)) val repliedToItemId = replyToItemValue?.itemId val repliedToClientContext = replyToItemValue?.clientContext scope.launch(Dispatchers.IO) { try { val response = directMessagesRepository.broadcastText( csrfToken, viewerId, deviceUuid, clientContext, threadIdsOrUserIds, text, repliedToItemId, repliedToClientContext ) parseResponse(response, data, directItem) } catch (e: Exception) { data.postValue(error(e.message, directItem)) Log.e(TAG, "sendText: ", e) } } return data } fun sendUri(uri: Uri, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() val mimeType = Utils.getMimeType(uri, contentResolver) if (isEmpty(mimeType)) { data.postValue(error("Unknown MediaType", null)) return data } val isPhoto = mimeType != null && mimeType.startsWith("image") if (isPhoto) { sendPhoto(data, uri, scope) return data } if (mimeType != null && mimeType.startsWith("video")) { sendVideo(data, uri, scope) } return data } fun sendAnimatedMedia(giphyGif: GiphyGif, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() val userId = getCurrentUserId(data) ?: return data val clientContext = UUID.randomUUID().toString() val directItem = createAnimatedMedia(userId, clientContext, giphyGif) directItem.isPending = true addItems(0, listOf(directItem)) data.postValue(loading(directItem)) scope.launch(Dispatchers.IO) { try { val request = directMessagesRepository.broadcastAnimatedMedia( csrfToken, userId, deviceUuid, clientContext, threadIdsOrUserIds, giphyGif ) parseResponse(request, data, directItem) } catch (e: Exception) { data.postValue(error(e.message, directItem)) Log.e(TAG, "sendAnimatedMedia: ", e) } } return data } fun sendVoice( data: MutableLiveData>, uri: Uri, waveform: List, samplingFreq: Int, duration: Long, byteLength: Long, scope: CoroutineScope, ) { if (duration > 60000) { // instagram does not allow uploading audio longer than 60 secs for Direct messages data.postValue(error(R.string.dms_ERROR_AUDIO_TOO_LONG, null)) return } val userId = getCurrentUserId(data) ?: return val clientContext = UUID.randomUUID().toString() val directItem = createVoice(userId, clientContext, uri, duration, waveform, samplingFreq) directItem.isPending = true addItems(0, listOf(directItem)) data.postValue(loading(directItem)) val uploadDmVoiceOptions = createUploadDmVoiceOptions(byteLength, duration) scope.launch(Dispatchers.IO) { try { val response = uploadVideo(uri, contentResolver, uploadDmVoiceOptions) // Log.d(TAG, "onUploadComplete: " + response); if (handleInvalidResponse(data, response)) return@launch val uploadFinishOptions = UploadFinishOptions( uploadDmVoiceOptions.uploadId, "4", null ) mediaRepository.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions) val broadcastResponse = directMessagesRepository.broadcastVoice( csrfToken, viewerId, deviceUuid, clientContext, threadIdsOrUserIds, uploadDmVoiceOptions.uploadId, waveform, samplingFreq ) parseResponse(broadcastResponse, data, directItem) } catch (e: Exception) { data.postValue(error(e.message, directItem)) Log.e(TAG, "sendVoice: ", e) } } } fun sendReaction( item: DirectItem, emoji: Emoji, scope: CoroutineScope, ): LiveData> { val data = MutableLiveData>() val userId = getCurrentUserId(data) if (userId == null) { data.postValue(error("userId is null", null)) return data } val clientContext = UUID.randomUUID().toString() // Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId()); data.postValue(loading(item)) addReaction(item, emoji) var emojiUnicode: String? = null if (emoji.unicode != "❤️") { emojiUnicode = emoji.unicode } val itemId = item.itemId if (itemId == null) { data.postValue(error("itemId is null", null)) return data } scope.launch(Dispatchers.IO) { try { directMessagesRepository.broadcastReaction( csrfToken, userId, deviceUuid, clientContext, threadIdsOrUserIds, itemId, emojiUnicode, false ) } catch (e: Exception) { data.postValue(error(e.message, null)) Log.e(TAG, "sendReaction: ", e) } } return data } fun sendDeleteReaction(itemId: String, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() val item = getItem(itemId) if (item == null) { data.postValue(error("Invalid item", null)) return data } val reactions = item.reactions if (reactions == null) { // already removed? data.postValue(success(item)) return data } removeReaction(item) val clientContext = UUID.randomUUID().toString() val itemId1 = item.itemId if (itemId1 == null) { data.postValue(error("itemId is null", null)) return data } scope.launch(Dispatchers.IO) { try { directMessagesRepository.broadcastReaction( csrfToken, viewerId, deviceUuid, clientContext, threadIdsOrUserIds, itemId1, null, true ) } catch (e: Exception) { data.postValue(error(e.message, null)) Log.e(TAG, "sendDeleteReaction: ", e) } } return data } fun unsend(item: DirectItem, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() val index = removeItem(item) val itemId = item.itemId if (itemId == null) { data.postValue(error("itemId is null", null)) return data } scope.launch(Dispatchers.IO) { try { directMessagesRepository.deleteItem(csrfToken, deviceUuid, threadId, itemId) } catch (e: Exception) { // add the item back if unsuccessful addItems(index, listOf(item)) data.postValue(error(e.message, item)) Log.e(TAG, "unsend: ", e) } } return data } fun forward( recipients: Set, itemToForward: DirectItem, scope: CoroutineScope, ) { for (recipient in recipients) { forward(recipient, itemToForward, scope) } } fun forward( recipient: RankedRecipient, itemToForward: DirectItem, scope: CoroutineScope, ) { if (recipient.thread == null && recipient.user != null) { scope.launch(Dispatchers.IO) { // create thread and forward val thread = DirectMessagesManager.createThread(recipient.user.pk) forward(thread, itemToForward, scope) } return } if (recipient.thread != null) { // just forward val thread = recipient.thread forward(thread, itemToForward, scope) } } fun setReplyToItem(item: DirectItem?) { // Log.d(TAG, "setReplyToItem: " + item); _replyToItem.postValue(item) } private fun forward( thread: DirectThread, itemToForward: DirectItem, scope: CoroutineScope, ): LiveData> { val data = MutableLiveData>() val forwardItemId = itemToForward.itemId if (forwardItemId == null) { data.postValue(error("item id is null", null)) return data } val itemType = itemToForward.itemType if (itemType == null) { data.postValue(error("item type is null", null)) return data } val itemTypeName = DirectItemType.getName(itemType) if (itemTypeName == null) { Log.e(TAG, "forward: itemTypeName was null!") data.postValue(error("itemTypeName is null", null)) return data } data.postValue(loading(null)) if (thread.threadId == null) { Log.e(TAG, "forward: threadId was null!") data.postValue(error("threadId is null", null)) return data } scope.launch(Dispatchers.IO) { try { directMessagesRepository.forward( thread.threadId, itemTypeName, threadId, forwardItemId ) data.postValue(success(Any())) } catch (e: Exception) { Log.e(TAG, "forward: ", e) data.postValue(error(e.message, null)) } } return data } fun acceptRequest(scope: CoroutineScope): LiveData> { val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { directMessagesRepository.approveRequest(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) } catch (e: Exception) { Log.e(TAG, "acceptRequest: ", e) data.postValue(error(e.message, null)) } } return data } fun declineRequest(scope: CoroutineScope): LiveData> { val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { directMessagesRepository.declineRequest(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) } catch (e: Exception) { Log.e(TAG, "declineRequest: ", e) data.postValue(error(e.message, null)) } } return data } fun refreshChats(scope: CoroutineScope) { val isFetching = _fetching.value if (isFetching != null && isFetching.status === Resource.Status.LOADING) { stopCurrentRequest() } cursor = null hasOlder = true fetchChats(scope) } private fun sendPhoto( data: MutableLiveData>, uri: Uri, scope: CoroutineScope, ) { try { val dimensions = BitmapUtils.decodeDimensions(contentResolver, uri) if (dimensions == null) { data.postValue(error("Decoding dimensions failed", null)) return } sendPhoto(data, uri, dimensions.first, dimensions.second, scope) } catch (e: IOException) { data.postValue(error(e.message, null)) Log.e(TAG, "sendPhoto: ", e) } } private fun sendPhoto( data: MutableLiveData>, uri: Uri, width: Int, height: Int, scope: CoroutineScope, ) { val clientContext = UUID.randomUUID().toString() val directItem = createImageOrVideo(viewerId, clientContext, uri, width, height, false) directItem.isPending = true addItems(0, listOf(directItem)) data.postValue(loading(directItem)) scope.launch(Dispatchers.IO) { try { val response = uploadPhoto(uri, contentResolver) if (handleInvalidResponse(data, response)) return@launch val response1 = response.response ?: return@launch val uploadId = response1.optString("upload_id") val response2 = directMessagesRepository.broadcastPhoto(csrfToken, viewerId, deviceUuid, clientContext, threadIdsOrUserIds, uploadId) parseResponse(response2, data, directItem) } catch (e: Exception) { data.postValue(error(e.message, null)) Log.e(TAG, "sendPhoto: ", e) } } } private fun sendVideo( data: MutableLiveData>, uri: Uri, scope: CoroutineScope, ) { MediaUtils.getVideoInfo(contentResolver, uri, object : OnInfoLoadListener { override fun onLoad(info: VideoInfo?) { if (info == null) { data.postValue(error("Could not get the video info", null)) return } sendVideo(data, uri, info.size, info.duration, info.width, info.height, scope) } override fun onFailure(t: Throwable) { data.postValue(error(t.message, null)) } }) } private fun sendVideo( data: MutableLiveData>, uri: Uri, byteLength: Long, duration: Long, width: Int, height: Int, scope: CoroutineScope, ) { if (duration > 60000) { // instagram does not allow uploading videos longer than 60 secs for Direct messages data.postValue(error(R.string.dms_ERROR_VIDEO_TOO_LONG, null)) return } val userId = getCurrentUserId(data) ?: return val clientContext = UUID.randomUUID().toString() val directItem = createImageOrVideo(userId, clientContext, uri, width, height, true) directItem.isPending = true addItems(0, listOf(directItem)) data.postValue(loading(directItem)) val uploadDmVideoOptions = createUploadDmVideoOptions(byteLength, duration, width, height) scope.launch(Dispatchers.IO) { try { val response = uploadVideo(uri, contentResolver, uploadDmVideoOptions) // Log.d(TAG, "onUploadComplete: " + response); if (handleInvalidResponse(data, response)) return@launch val uploadFinishOptions = UploadFinishOptions( uploadDmVideoOptions.uploadId, "2", VideoOptions(duration / 1000f, emptyList(), 0, false) ) mediaRepository.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions) val broadcastResponse = directMessagesRepository.broadcastVideo( csrfToken, viewerId, deviceUuid, clientContext, threadIdsOrUserIds, uploadDmVideoOptions.uploadId, "", true ) parseResponse(broadcastResponse, data, directItem) } catch (e: Exception) { data.postValue(error(e.message, directItem)) Log.e(TAG, "sendVideo: ", e) } } } private fun parseResponse( response: DirectThreadBroadcastResponse, data: MutableLiveData>, directItem: DirectItem, ) { val payloadClientContext: String? val timestamp: Long val itemId: String? val payload = response.payload if (payload == null) { val messageMetadata = response.messageMetadata if (messageMetadata == null || messageMetadata.isEmpty()) { data.postValue(success(directItem)) return } val (clientContext, itemId1, timestamp1) = messageMetadata[0] payloadClientContext = clientContext itemId = itemId1 timestamp = timestamp1 } else { payloadClientContext = payload.clientContext timestamp = payload.timestamp itemId = payload.itemId } if (payloadClientContext == null) { data.postValue(error("clientContext in response was null", null)) return } updateItemSent(payloadClientContext, timestamp, itemId) data.postValue(success(directItem)) } private fun updateItemSent( clientContext: String, timestamp: Long, itemId: String?, ) { val items = items.value val list = if (items == null) LinkedList() else LinkedList(items) val index = list.indexOfFirst { it?.clientContext == clientContext } if (index < 0) return val directItem = list[index] try { val itemClone = directItem.clone() as DirectItem itemClone.itemId = itemId itemClone.isPending = false itemClone.setTimestamp(timestamp) list[index] = itemClone inboxManager.setItemsToThread(threadId, list) } catch (e: CloneNotSupportedException) { Log.e(TAG, "updateItemSent: ", e) } } private fun handleInvalidResponse( data: MutableLiveData>, response: MediaUploadResponse, ): Boolean { val responseJson = response.response if (responseJson == null || response.responseCode != HttpURLConnection.HTTP_OK) { data.postValue(error(R.string.generic_not_ok_response, null)) return true } val status = responseJson.optString("status") if (isEmpty(status) || status != "ok") { data.postValue(error(R.string.generic_not_ok_response, null)) return true } return false } private fun getItemIndex(item: DirectItem, list: List): Int { return Iterables.indexOf(list) { i: DirectItem? -> i != null && i.itemId == item.itemId } } private fun getItem(itemId: String): DirectItem? { val items = items.value ?: return null return items.asSequence() .filter { it.itemId == itemId } .firstOrNull() } private fun stopCurrentRequest() { chatsRequest?.let { if (it.isExecuted || it.isCanceled) return it.cancel() } _fetching.postValue(success(Any())) } private fun getCurrentUserId(data: MutableLiveData>): Long? { if (currentUser == null || currentUser.pk <= 0) { data.postValue(error(R.string.dms_ERROR_INVALID_USER, null)) return null } return currentUser.pk } fun removeThread() { val pendingValue = isPending.value val threadInPending = pendingValue != null && pendingValue inboxManager.removeThread(threadId) if (threadInPending) { val totalValue = inboxManager.getPendingRequestsTotal().value ?: return inboxManager.setPendingRequestsTotal(totalValue - 1) } } fun updateTitle(newTitle: String, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { val response = directMessagesRepository.updateTitle(csrfToken, deviceUuid, threadId, newTitle.trim()) handleDetailsChangeResponse(data, response) } catch (e: Exception) { } } return data } fun addMembers(users: Set, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { val response = directMessagesRepository.addUsers( csrfToken, deviceUuid, threadId, users.map { obj: User -> obj.pk } ) handleDetailsChangeResponse(data, response) } catch (e: Exception) { Log.e(TAG, "addMembers: ", e) data.postValue(error(e.message, null)) } } return data } fun removeMember(user: User, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { directMessagesRepository.removeUsers(csrfToken, deviceUuid, threadId, setOf(user.pk)) data.postValue(success(Any())) var activeUsers = users.value var leftUsersValue = leftUsers.value if (activeUsers == null) { activeUsers = emptyList() } if (leftUsersValue == null) { leftUsersValue = emptyList() } val updatedActiveUsers = activeUsers.filter { u: User -> u.pk != user.pk } val updatedLeftUsersBuilder = ImmutableList.builder().addAll(leftUsersValue) if (!leftUsersValue.contains(user)) { updatedLeftUsersBuilder.add(user) } val updatedLeftUsers = updatedLeftUsersBuilder.build() setThreadUsers(updatedActiveUsers, updatedLeftUsers) } catch (e: Exception) { Log.e(TAG, "removeMember: ", e) data.postValue(error(e.message, null)) } } return data } fun isAdmin(user: User): Boolean { val adminUserIdsValue = adminUserIds.value return adminUserIdsValue != null && adminUserIdsValue.contains(user.pk) } fun makeAdmin(user: User, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() if (isAdmin(user)) return data scope.launch(Dispatchers.IO) { try { directMessagesRepository.addAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk)) val currentAdminIds = adminUserIds.value val updatedAdminIds = ImmutableList.builder() .addAll(currentAdminIds ?: emptyList()) .add(user.pk) .build() val currentThread = thread.value ?: return@launch try { val thread = currentThread.clone() as DirectThread thread.adminUserIds = updatedAdminIds inboxManager.setThread(threadId, thread) } catch (e: CloneNotSupportedException) { Log.e(TAG, "makeAdmin: ", e) } data.postValue(success(Any())) } catch (e: Exception) { Log.e(TAG, "makeAdmin: ", e) data.postValue(error(e.message, null)) } } return data } fun removeAdmin(user: User, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() if (!isAdmin(user)) return data scope.launch(Dispatchers.IO) { try { directMessagesRepository.removeAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk)) val currentAdmins = adminUserIds.value ?: return@launch val updatedAdminUserIds = currentAdmins.filter { userId1: Long -> userId1 != user.pk } val currentThread = thread.value ?: return@launch try { val thread = currentThread.clone() as DirectThread thread.adminUserIds = updatedAdminUserIds inboxManager.setThread(threadId, thread) } catch (e: CloneNotSupportedException) { Log.e(TAG, "removeAdmin: ", e) } data.postValue(success(Any())) } catch (e: Exception) { Log.e(TAG, "removeAdmin: ", e) data.postValue(error(e.message, null)) } } return data } fun mute(scope: CoroutineScope): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) val muted = isMuted.value if (muted != null && muted) { data.postValue(success(Any())) return data } scope.launch(Dispatchers.IO) { try { directMessagesRepository.mute(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) val currentThread = thread.value ?: return@launch try { val thread = currentThread.clone() as DirectThread thread.muted = true inboxManager.setThread(threadId, thread) } catch (e: CloneNotSupportedException) { Log.e(TAG, "mute: ", e) } } catch (e: Exception) { Log.e(TAG, "mute: ", e) data.postValue(error(e.message, null)) } } return data } fun unmute(scope: CoroutineScope): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) val muted = isMuted.value if (muted != null && !muted) { data.postValue(success(Any())) return data } scope.launch(Dispatchers.IO) { try { directMessagesRepository.unmute(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) val currentThread = thread.value ?: return@launch try { val thread = currentThread.clone() as DirectThread thread.muted = false inboxManager.setThread(threadId, thread) } catch (e: CloneNotSupportedException) { Log.e(TAG, "unmute: ", e) } } catch (e: Exception) { Log.e(TAG, "unmute: ", e) data.postValue(error(e.message, null)) } } return data } fun muteMentions(scope: CoroutineScope): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) val mentionsMuted = isMentionsMuted.value if (mentionsMuted != null && mentionsMuted) { data.postValue(success(Any())) return data } scope.launch(Dispatchers.IO) { try { directMessagesRepository.muteMentions(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) val currentThread = thread.value ?: return@launch try { val thread = currentThread.clone() as DirectThread thread.mentionsMuted = true inboxManager.setThread(threadId, thread) } catch (e: CloneNotSupportedException) { Log.e(TAG, "muteMentions: ", e) } } catch (e: Exception) { Log.e(TAG, "muteMentions: ", e) data.postValue(error(e.message, null)) } } return data } fun unmuteMentions(scope: CoroutineScope): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) val mentionsMuted = isMentionsMuted.value if (mentionsMuted != null && !mentionsMuted) { data.postValue(success(Any())) return data } scope.launch(Dispatchers.IO) { try { directMessagesRepository.unmuteMentions(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) val currentThread = thread.value ?: return@launch try { val thread = currentThread.clone() as DirectThread thread.mentionsMuted = false inboxManager.setThread(threadId, thread) } catch (e: CloneNotSupportedException) { Log.e(TAG, "unmuteMentions: ", e) } } catch (e: Exception) { Log.e(TAG, "unmuteMentions: ", e) data.postValue(error(e.message, null)) } } return data } fun blockUser(user: User, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { friendshipRepository.changeBlock(csrfToken, viewerId, deviceUuid, false, user.pk) refreshChats(scope) } catch (e: Exception) { Log.e(TAG, "onFailure: ", e) data.postValue(error(e.message, null)) } } return data } fun unblockUser(user: User, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { friendshipRepository.changeBlock(csrfToken, viewerId, deviceUuid, true, user.pk) refreshChats(scope) } catch (e: Exception) { Log.e(TAG, "onFailure: ", e) data.postValue(error(e.message, null)) } } return data } fun restrictUser(user: User, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { friendshipRepository.toggleRestrict(csrfToken, deviceUuid, user.pk, true) refreshChats(scope) } catch (e: Exception) { Log.e(TAG, "onFailure: ", e) data.postValue(error(e.message, null)) } } return data } fun unRestrictUser(user: User, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { friendshipRepository.toggleRestrict(csrfToken, deviceUuid, user.pk, false) refreshChats(scope) } catch (e: Exception) { Log.e(TAG, "onFailure: ", e) data.postValue(error(e.message, null)) } } return data } fun approveUsers(users: List, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { val response = directMessagesRepository.approveParticipantRequests( csrfToken, deviceUuid, threadId, users.map { obj: User -> obj.pk } ) handleDetailsChangeResponse(data, response) pendingUserApproveDenySuccessAction(users) } catch (e: Exception) { Log.e(TAG, "approveUsers: ", e) data.postValue(error(e.message, null)) } } return data } fun denyUsers(users: List, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { val response = directMessagesRepository.declineParticipantRequests( csrfToken, deviceUuid, threadId, users.map { obj: User -> obj.pk } ) handleDetailsChangeResponse(data, response) pendingUserApproveDenySuccessAction(users) } catch (e: Exception) { Log.e(TAG, "denyUsers: ", e) data.postValue(error(e.message, null)) } } return data } private fun pendingUserApproveDenySuccessAction(users: List) { val pendingRequestsValue = _pendingRequests.value ?: return val pendingUsers = pendingRequestsValue.users if (pendingUsers == null || pendingUsers.isEmpty()) return val filtered = pendingUsers.filter { o: User -> !users.contains(o) } try { val clone = pendingRequestsValue.clone() as DirectThreadParticipantRequestsResponse clone.users = filtered val totalParticipantRequests = clone.totalParticipantRequests clone.totalParticipantRequests = if (totalParticipantRequests > 0) totalParticipantRequests - 1 else 0 _pendingRequests.postValue(clone) } catch (e: CloneNotSupportedException) { Log.e(TAG, "pendingUserApproveDenySuccessAction: ", e) } } fun approvalRequired(scope: CoroutineScope): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) val approvalRequiredToJoin = isApprovalRequiredToJoin.value if (approvalRequiredToJoin != null && approvalRequiredToJoin) { data.postValue(success(Any())) return data } scope.launch(Dispatchers.IO) { try { val response = directMessagesRepository.approvalRequired(csrfToken, deviceUuid, threadId) handleDetailsChangeResponse(data, response) val currentThread = thread.value ?: return@launch try { val thread = currentThread.clone() as DirectThread thread.approvalRequiredForNewMembers = true inboxManager.setThread(threadId, thread) } catch (e: CloneNotSupportedException) { Log.e(TAG, "onResponse: ", e) } } catch (e: Exception) { Log.e(TAG, "approvalRequired: ", e) data.postValue(error(e.message, null)) } } return data } fun approvalNotRequired(scope: CoroutineScope): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) val approvalRequiredToJoin = isApprovalRequiredToJoin.value if (approvalRequiredToJoin != null && !approvalRequiredToJoin) { data.postValue(success(Any())) return data } scope.launch(Dispatchers.IO) { try { val request = directMessagesRepository.approvalNotRequired(csrfToken, deviceUuid, threadId) handleDetailsChangeResponse(data, request) val currentThread = thread.value ?: return@launch try { val thread = currentThread.clone() as DirectThread thread.approvalRequiredForNewMembers = false inboxManager.setThread(threadId, thread) } catch (e: CloneNotSupportedException) { Log.e(TAG, "onResponse: ", e) } } catch (e: Exception) { Log.e(TAG, "approvalNotRequired: ", e) data.postValue(error(e.message, null)) } } return data } fun leave(scope: CoroutineScope): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { val request = directMessagesRepository.leave(csrfToken, deviceUuid, threadId) handleDetailsChangeResponse(data, request) } catch (e: Exception) { Log.e(TAG, "leave: ", e) data.postValue(error(e.message, null)) } } return data } fun end(scope: CoroutineScope): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { val request = directMessagesRepository.end(csrfToken, deviceUuid, threadId) handleDetailsChangeResponse(data, request) val currentThread = thread.value ?: return@launch try { val thread = currentThread.clone() as DirectThread thread.inputMode = 1 inboxManager.setThread(threadId, thread) } catch (e: CloneNotSupportedException) { Log.e(TAG, "onResponse: ", e) } } catch (e: Exception) { Log.e(TAG, "leave: ", e) data.postValue(error(e.message, null)) } } return data } private fun handleDetailsChangeResponse( data: MutableLiveData>, response: DirectThreadDetailsChangeResponse, ) { data.postValue(success(Any())) val thread = response.thread if (thread != null) { setThread(thread, true) } } fun markAsSeen( directItem: DirectItem, scope: CoroutineScope, ): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { val response = directMessagesRepository.markAsSeen(csrfToken, deviceUuid, threadId, directItem) if (response == null) { data.postValue(error(R.string.generic_null_response, null)) return@launch } if (currentUser == null) return@launch inboxManager.fetchUnseenCount(scope) val payload = response.payload ?: return@launch val timestamp = payload.timestamp val thread = thread.value ?: return@launch val currentLastSeenAt = thread.lastSeenAt val lastSeenAt = if (currentLastSeenAt == null) HashMap() else HashMap(currentLastSeenAt) lastSeenAt[currentUser.pk] = DirectThreadLastSeenAt(timestamp, directItem.itemId) thread.lastSeenAt = lastSeenAt setThread(thread, true) data.postValue(success(Any())) } catch (e: Exception) { Log.e(TAG, "markAsSeen: ", e) data.postValue(error(e.message, null)) } } return data } }