diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index 12f38b1a..e692da19 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -1088,7 +1088,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); return; } - final InboxManager inboxManager = DirectMessagesManager.getInstance().getInboxManager(); + final InboxManager inboxManager = DirectMessagesManager.INSTANCE.getInboxManager(); final DirectThread thread = response.body(); if (!inboxManager.containsThread(thread.getThreadId())) { thread.setTemp(true); diff --git a/app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt b/app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt index 412038b2..43a51601 100644 --- a/app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt +++ b/app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt @@ -1,275 +1,222 @@ -package awais.instagrabber.managers; - -import android.content.ContentResolver; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; - -import com.google.common.collect.Iterables; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.UUID; -import java.util.function.Function; - -import awais.instagrabber.models.Resource; -import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.repositories.responses.directmessages.DirectItem; -import awais.instagrabber.repositories.responses.directmessages.DirectThread; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; -import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.webservices.DirectMessagesService; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -import static awais.instagrabber.utils.Utils.settingsHelper; - -public final class DirectMessagesManager { - private static final String TAG = DirectMessagesManager.class.getSimpleName(); - private static final Object LOCK = new Object(); - - private static DirectMessagesManager instance; - - private final InboxManager inboxManager; - private final InboxManager pendingInboxManager; - - private DirectMessagesService service; - - public static DirectMessagesManager getInstance() { - if (instance == null) { - synchronized (LOCK) { - if (instance == null) { - instance = new DirectMessagesManager(); - } - } - } - return instance; - } - - private DirectMessagesManager() { - inboxManager = InboxManager.getInstance(false); - pendingInboxManager = InboxManager.getInstance(true); - final String cookie = settingsHelper.getString(Constants.COOKIE); - final long viewerId = CookieUtils.getUserIdFromCookie(cookie); - final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); - final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); - if (csrfToken == null) return; - service = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid); - } - - public void moveThreadFromPending(@NonNull final String threadId) { - final List pendingThreads = pendingInboxManager.getThreads().getValue(); - if (pendingThreads == null) return; - final int index = Iterables.indexOf(pendingThreads, t -> t != null && t.getThreadId().equals(threadId)); - if (index < 0) return; - final DirectThread thread = pendingThreads.get(index); - final DirectItem threadFirstDirectItem = thread.getFirstDirectItem(); - if (threadFirstDirectItem == null) return; - final List threads = inboxManager.getThreads().getValue(); - int insertIndex = 0; +package awais.instagrabber.managers + +import android.content.ContentResolver +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import awais.instagrabber.managers.ThreadManager.Companion.getInstance +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.repositories.requests.directmessages.ThreadIdOrUserIds.Companion.of +import awais.instagrabber.repositories.responses.User +import awais.instagrabber.repositories.responses.directmessages.DirectThread +import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse +import awais.instagrabber.repositories.responses.directmessages.RankedRecipient +import awais.instagrabber.utils.Constants +import awais.instagrabber.utils.Utils +import awais.instagrabber.utils.getCsrfTokenFromCookie +import awais.instagrabber.utils.getUserIdFromCookie +import awais.instagrabber.webservices.DirectMessagesService +import awais.instagrabber.webservices.DirectMessagesService.Companion.getInstance +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.IOException +import java.util.* + +object DirectMessagesManager { + val inboxManager: InboxManager by lazy { InboxManager.getInstance(false) } + val pendingInboxManager: InboxManager by lazy { InboxManager.getInstance(true) } + + private val TAG = DirectMessagesManager::class.java.simpleName + private val viewerId: Long + private val deviceUuid: String + private val csrfToken: String + private val service: DirectMessagesService + + fun moveThreadFromPending(threadId: String) { + val pendingThreads = pendingInboxManager.threads.value ?: return + val index = pendingThreads.indexOfFirst { it.threadId == threadId } + if (index < 0) return + val thread = pendingThreads[index] + val threadFirstDirectItem = thread.firstDirectItem ?: return + val threads = inboxManager.threads.value + var insertIndex = 0 if (threads != null) { - for (final DirectThread tempThread : threads) { - final DirectItem firstDirectItem = tempThread.getFirstDirectItem(); - if (firstDirectItem == null) continue; - final long timestamp = firstDirectItem.getTimestamp(); + for (tempThread in threads) { + val firstDirectItem = tempThread.firstDirectItem ?: continue + val timestamp = firstDirectItem.getTimestamp() if (timestamp < threadFirstDirectItem.getTimestamp()) { - break; + break } - insertIndex++; + insertIndex++ } } - thread.setPending(false); - inboxManager.addThread(thread, insertIndex); - pendingInboxManager.removeThread(threadId); - final Integer currentTotal = inboxManager.getPendingRequestsTotal().getValue(); - if (currentTotal == null) return; - inboxManager.setPendingRequestsTotal(currentTotal - 1); - } - - public InboxManager getInboxManager() { - return inboxManager; + thread.pending = false + inboxManager.addThread(thread, insertIndex) + pendingInboxManager.removeThread(threadId) + val currentTotal = inboxManager.getPendingRequestsTotal().value ?: return + inboxManager.setPendingRequestsTotal(currentTotal - 1) } - public InboxManager getPendingInboxManager() { - return pendingInboxManager; + fun getThreadManager( + threadId: String, + pending: Boolean, + currentUser: User, + contentResolver: ContentResolver, + ): ThreadManager { + return getInstance(threadId, pending, currentUser, contentResolver, viewerId, csrfToken, deviceUuid) } - public ThreadManager getThreadManager(@NonNull final String threadId, - final boolean pending, - @NonNull final User currentUser, - @NonNull final ContentResolver contentResolver) { - return ThreadManager.getInstance(threadId, pending, currentUser, contentResolver); - } - - public void createThread(final long userPk, - @Nullable final Function callback) { - if (service == null) return; - final Call createThreadRequest = service.createThread(Collections.singletonList(userPk), null); - createThreadRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - if (response.errorBody() != null) { + fun createThread( + userPk: Long, + callback: ((DirectThread) -> Unit)?, + ) { + val createThreadRequest = service.createThread(listOf(userPk), null) + createThreadRequest.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + val errorBody = response.errorBody() + if (errorBody != null) { try { - final String string = response.errorBody().string(); - final String msg = String.format(Locale.US, - "onResponse: url: %s, responseCode: %d, errorBody: %s", - call.request().url().toString(), - response.code(), - string); - Log.e(TAG, msg); - } catch (IOException e) { - Log.e(TAG, "onResponse: ", e); + val string = errorBody.string() + val msg = String.format(Locale.US, + "onResponse: url: %s, responseCode: %d, errorBody: %s", + call.request().url().toString(), + response.code(), + string) + Log.e(TAG, msg) + } catch (e: IOException) { + Log.e(TAG, "onResponse: ", e) } - return; + return } - Log.e(TAG, "onResponse: request was not successful and response error body was null"); - return; + Log.e(TAG, "onResponse: request was not successful and response error body was null") + return } - final DirectThread thread = response.body(); + val thread = response.body() if (thread == null) { - Log.e(TAG, "onResponse: thread is null"); - return; - } - if (callback != null) { - callback.apply(thread); + Log.e(TAG, "onResponse: thread is null") + return } + callback?.invoke(thread) } - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - - } - }); + override fun onFailure(call: Call, t: Throwable) {} + }) } - public void sendMedia(@NonNull final Set recipients, final String mediaId) { - final int[] resultsCount = {0}; - final Function callback = unused -> { - resultsCount[0]++; - if (resultsCount[0] == recipients.size()) { - inboxManager.refresh(); + fun sendMedia(recipients: Set, mediaId: String) { + val resultsCount = intArrayOf(0) + val callback: () -> Unit = { + resultsCount[0]++ + if (resultsCount[0] == recipients.size) { + inboxManager.refresh() } - return null; - }; - for (final RankedRecipient recipient : recipients) { - if (recipient == null) continue; - sendMedia(recipient, mediaId, false, callback); + } + for (recipient in recipients) { + sendMedia(recipient, mediaId, false, callback) } } - public void sendMedia(@NonNull final RankedRecipient recipient, final String mediaId) { - sendMedia(recipient, mediaId, true, null); + fun sendMedia(recipient: RankedRecipient, mediaId: String) { + sendMedia(recipient, mediaId, true, null) } - private void sendMedia(@NonNull final RankedRecipient recipient, - @NonNull final String mediaId, - final boolean refreshInbox, - @Nullable final Function callback) { - if (recipient.getThread() == null && recipient.getUser() != null) { + private fun sendMedia( + recipient: RankedRecipient, + mediaId: String, + refreshInbox: Boolean, + callback: (() -> Unit)?, + ) { + if (recipient.thread == null && recipient.user != null) { // create thread and forward - createThread(recipient.getUser().getPk(), directThread -> { - sendMedia(directThread, mediaId, unused -> { + createThread(recipient.user.pk) { (threadId) -> + val threadIdTemp = threadId ?: return@createThread + sendMedia(threadIdTemp, mediaId) { if (refreshInbox) { - inboxManager.refresh(); - } - if (callback != null) { - callback.apply(null); + inboxManager.refresh() } - return null; - }); - return null; - }); + callback?.invoke() + } + } } - if (recipient.getThread() == null) return; + if (recipient.thread == null) return // just forward - final DirectThread thread = recipient.getThread(); - sendMedia(thread, mediaId, unused -> { + val thread = recipient.thread + val threadId = thread.threadId ?: return + sendMedia(threadId, mediaId) { if (refreshInbox) { - inboxManager.refresh(); - } - if (callback != null) { - callback.apply(null); + inboxManager.refresh() } - return null; - }); - } - - @NonNull - public LiveData> sendMedia(@NonNull final DirectThread thread, - @NonNull final String mediaId, - @Nullable final Function callback) { - return sendMedia(thread.getThreadId(), mediaId, callback); + callback?.invoke() + } } - @NonNull - public LiveData> sendMedia(@NonNull final String threadId, - @NonNull final String mediaId, - @Nullable final Function callback) { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Call request = service.broadcastMediaShare( - UUID.randomUUID().toString(), - ThreadIdOrUserIds.of(threadId), - mediaId - ); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (response.isSuccessful()) { - data.postValue(Resource.success(new Object())); - if (callback != null) { - callback.apply(null); - } - return; + private fun sendMedia( + threadId: String, + mediaId: String, + callback: (() -> Unit)?, + ): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val request = service.broadcastMediaShare( + UUID.randomUUID().toString(), + of(threadId), + mediaId + ) + request.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (response.isSuccessful) { + data.postValue(success(Any())) + callback?.invoke() + return } - if (response.errorBody() != null) { + val errorBody = response.errorBody() + if (errorBody != null) { try { - final String string = response.errorBody().string(); - final String msg = String.format(Locale.US, - "onResponse: url: %s, responseCode: %d, errorBody: %s", - call.request().url().toString(), - response.code(), - string); - Log.e(TAG, msg); - data.postValue(Resource.error(msg, null)); - } catch (IOException e) { - Log.e(TAG, "onResponse: ", e); - data.postValue(Resource.error(e.getMessage(), null)); - } - if (callback != null) { - callback.apply(null); + val string = errorBody.string() + val msg = String.format(Locale.US, + "onResponse: url: %s, responseCode: %d, errorBody: %s", + call.request().url().toString(), + response.code(), + string) + Log.e(TAG, msg) + data.postValue(error(msg, null)) + } catch (e: IOException) { + Log.e(TAG, "onResponse: ", e) + data.postValue(error(e.message, null)) } - return; - } - final String msg = "onResponse: request was not successful and response error body was null"; - Log.e(TAG, msg); - data.postValue(Resource.error(msg, null)); - if (callback != null) { - callback.apply(null); + callback?.invoke() + return } + val msg = "onResponse: request was not successful and response error body was null" + Log.e(TAG, msg) + data.postValue(error(msg, null)) + callback?.invoke() } - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - if (callback != null) { - callback.apply(null); - } + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + callback?.invoke() } - }); - return data; + }) + return data + } + + init { + val cookie = Utils.settingsHelper.getString(Constants.COOKIE) + viewerId = getUserIdFromCookie(cookie) + deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) + val csrfToken = getCsrfTokenFromCookie(cookie) + require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" } + this.csrfToken = csrfToken + service = getInstance(csrfToken, viewerId, deviceUuid) } -} +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/managers/InboxManager.kt b/app/src/main/java/awais/instagrabber/managers/InboxManager.kt index 82019161..963b2699 100644 --- a/app/src/main/java/awais/instagrabber/managers/InboxManager.kt +++ b/app/src/main/java/awais/instagrabber/managers/InboxManager.kt @@ -1,382 +1,349 @@ -package awais.instagrabber.managers; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Transformations; - -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; - -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import awais.instagrabber.R; -import awais.instagrabber.models.Resource; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; -import awais.instagrabber.repositories.responses.directmessages.DirectInbox; -import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; -import awais.instagrabber.repositories.responses.directmessages.DirectItem; -import awais.instagrabber.repositories.responses.directmessages.DirectThread; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.webservices.DirectMessagesService; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -import static androidx.lifecycle.Transformations.distinctUntilChanged; -import static awais.instagrabber.utils.Utils.settingsHelper; - -public final class InboxManager { - private static final String TAG = InboxManager.class.getSimpleName(); - private static final LoadingCache THREAD_LOCKS = CacheBuilder - .newBuilder() - .expireAfterAccess(1, TimeUnit.MINUTES) // max lock time ever expected - .build(CacheLoader.from(Object::new)); - private static final Comparator THREAD_COMPARATOR = (t1, t2) -> { - final DirectItem t1FirstDirectItem = t1.getFirstDirectItem(); - final DirectItem t2FirstDirectItem = t2.getFirstDirectItem(); - if (t1FirstDirectItem == null && t2FirstDirectItem == null) return 0; - if (t1FirstDirectItem == null) return 1; - if (t2FirstDirectItem == null) return -1; - return Long.compare(t2FirstDirectItem.getTimestamp(), t1FirstDirectItem.getTimestamp()); - }; - - private final MutableLiveData> inbox = new MutableLiveData<>(); - private final MutableLiveData> unseenCount = new MutableLiveData<>(); - private final MutableLiveData pendingRequestsTotal = new MutableLiveData<>(0); - - private final LiveData> threads; - private final DirectMessagesService service; - private final boolean pending; - - private Call inboxRequest; - private Call unseenCountRequest; - private long seqId; - private String cursor; - private boolean hasOlder = true; - private User viewer; - - @NonNull - public static InboxManager getInstance(final boolean pending) { - return new InboxManager(pending); - } - - private InboxManager(final boolean pending) { - this.pending = pending; - final String cookie = settingsHelper.getString(Constants.COOKIE); - final long userId = CookieUtils.getUserIdFromCookie(cookie); - final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); - final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); - if (TextUtils.isEmpty(csrfToken)) { - throw new IllegalArgumentException("csrfToken is empty!"); - } else if (userId == 0) { - throw new IllegalArgumentException("user id invalid"); - } else if (TextUtils.isEmpty(deviceUuid)) { - throw new IllegalArgumentException("device uuid is empty!"); - } - service = DirectMessagesService.getInstance(csrfToken, userId, deviceUuid); - - // Transformations - threads = distinctUntilChanged(Transformations.map(inbox, inboxResource -> { - if (inboxResource == null) { - return Collections.emptyList(); - } - final DirectInbox inbox = inboxResource.data; - if (inbox == null) { - return Collections.emptyList(); - } - return ImmutableList.sortedCopyOf(THREAD_COMPARATOR, inbox.getThreads()); - })); - - fetchInbox(); - if (!pending) { - fetchUnseenCount(); - } - } - - public LiveData> getInbox() { - return distinctUntilChanged(inbox); - } - - public LiveData> getThreads() { - return threads; - } - - public LiveData> getUnseenCount() { - return distinctUntilChanged(unseenCount); +package awais.instagrabber.managers + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import awais.instagrabber.R +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.repositories.responses.User +import awais.instagrabber.repositories.responses.directmessages.* +import awais.instagrabber.utils.Constants +import awais.instagrabber.utils.Utils +import awais.instagrabber.utils.getCsrfTokenFromCookie +import awais.instagrabber.utils.getUserIdFromCookie +import awais.instagrabber.webservices.DirectMessagesService +import awais.instagrabber.webservices.DirectMessagesService.Companion.getInstance +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import com.google.common.collect.ImmutableList +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.* +import java.util.concurrent.TimeUnit + +class InboxManager private constructor(private val pending: Boolean) { + private val inbox = MutableLiveData>() + private val unseenCount = MutableLiveData>() + private val pendingRequestsTotal = MutableLiveData(0) + val threads: LiveData> + private val service: DirectMessagesService + private var inboxRequest: Call? = null + private var unseenCountRequest: Call? = null + private var seqId: Long = 0 + private var cursor: String? = null + private var hasOlder = true + var viewer: User? = null + private set + + fun getInbox(): LiveData> { + return Transformations.distinctUntilChanged(inbox) } - public LiveData getPendingRequestsTotal() { - return distinctUntilChanged(pendingRequestsTotal); + fun getUnseenCount(): LiveData> { + return Transformations.distinctUntilChanged(unseenCount) } - public User getViewer() { - return viewer; + fun getPendingRequestsTotal(): LiveData { + return Transformations.distinctUntilChanged(pendingRequestsTotal) } - public void fetchInbox() { - final Resource inboxResource = inbox.getValue(); - if ((inboxResource != null && inboxResource.status == Resource.Status.LOADING) || !hasOlder) return; - stopCurrentInboxRequest(); - inbox.postValue(Resource.loading(getCurrentDirectInbox())); - inboxRequest = pending ? service.fetchPendingInbox(cursor, seqId) : service.fetchInbox(cursor, seqId); - inboxRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - parseInboxResponse(response.body()); + fun fetchInbox() { + val inboxResource = inbox.value + if (inboxResource != null && inboxResource.status === Resource.Status.LOADING || !hasOlder) return + stopCurrentInboxRequest() + inbox.postValue(loading(currentDirectInbox)) + inboxRequest = if (pending) service.fetchPendingInbox(cursor, seqId) else service.fetchInbox(cursor, seqId) + inboxRequest?.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val body = response.body() + if (body == null) { + Log.e(TAG, "parseInboxResponse: Response is null") + inbox.postValue(error(R.string.generic_null_response, currentDirectInbox)) + hasOlder = false + return + } + parseInboxResponse(body) } - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Failed fetching dm inbox", t); - inbox.postValue(Resource.error(t.getMessage(), getCurrentDirectInbox())); - hasOlder = false; + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "Failed fetching dm inbox", t) + inbox.postValue(error(t.message, currentDirectInbox)) + hasOlder = false } - }); + }) } - public void fetchUnseenCount() { - final Resource unseenCountResource = unseenCount.getValue(); - if ((unseenCountResource != null && unseenCountResource.status == Resource.Status.LOADING)) return; - stopCurrentUnseenCountRequest(); - unseenCount.postValue(Resource.loading(getCurrentUnseenCount())); - unseenCountRequest = service.fetchUnseenCount(); - unseenCountRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final DirectBadgeCount directBadgeCount = response.body(); + fun fetchUnseenCount() { + val unseenCountResource = unseenCount.value + if (unseenCountResource != null && unseenCountResource.status === Resource.Status.LOADING) return + stopCurrentUnseenCountRequest() + unseenCount.postValue(loading(currentUnseenCount)) + unseenCountRequest = service.fetchUnseenCount() + unseenCountRequest?.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val directBadgeCount = response.body() if (directBadgeCount == null) { - Log.e(TAG, "onResponse: directBadgeCount Response is null"); - unseenCount.postValue(Resource.error(R.string.dms_inbox_error_null_count, getCurrentUnseenCount())); - return; + Log.e(TAG, "onResponse: directBadgeCount Response is null") + unseenCount.postValue(error(R.string.dms_inbox_error_null_count, currentUnseenCount)) + return } - unseenCount.postValue(Resource.success(directBadgeCount.getBadgeCount())); + unseenCount.postValue(success(directBadgeCount.badgeCount)) } - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Failed fetching unseen count", t); - unseenCount.postValue(Resource.error(t.getMessage(), getCurrentUnseenCount())); + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "Failed fetching unseen count", t) + unseenCount.postValue(error(t.message, currentUnseenCount)) } - }); + }) } - public void refresh() { - cursor = null; - seqId = 0; - hasOlder = true; - fetchInbox(); + fun refresh() { + cursor = null + seqId = 0 + hasOlder = true + fetchInbox() if (!pending) { - fetchUnseenCount(); + fetchUnseenCount() } } - private DirectInbox getCurrentDirectInbox() { - final Resource inboxResource = inbox.getValue(); - return inboxResource != null ? inboxResource.data : null; - } - - private void parseInboxResponse(final DirectInboxResponse response) { - if (response == null) { - Log.e(TAG, "parseInboxResponse: Response is null"); - inbox.postValue(Resource.error(R.string.generic_null_response, getCurrentDirectInbox())); - hasOlder = false; - return; + private val currentDirectInbox: DirectInbox? + get() { + val inboxResource = inbox.value + return inboxResource?.data } - if (!Objects.equals(response.getStatus(), "ok")) { - Log.e(TAG, "DM inbox fetch response: status not ok"); - inbox.postValue(Resource.error(R.string.generic_not_ok_response, getCurrentDirectInbox())); - hasOlder = false; - return; + + private fun parseInboxResponse(response: DirectInboxResponse) { + if (response.status != "ok") { + Log.e(TAG, "DM inbox fetch response: status not ok") + inbox.postValue(error(R.string.generic_not_ok_response, currentDirectInbox)) + hasOlder = false + return } - seqId = response.getSeqId(); + seqId = response.seqId if (viewer == null) { - viewer = response.getViewer(); + viewer = response.viewer } - final DirectInbox inbox = response.getInbox(); - if (inbox == null) return; - if (!TextUtils.isEmpty(cursor)) { - final DirectInbox currentDirectInbox = getCurrentDirectInbox(); - if (currentDirectInbox != null) { - List threads = currentDirectInbox.getThreads(); - threads = threads == null ? new LinkedList<>() : new LinkedList<>(threads); - threads.addAll(inbox.getThreads() == null ? Collections.emptyList() : inbox.getThreads()); - inbox.setThreads(threads); + val inbox = response.inbox ?: return + if (!cursor.isNullOrBlank()) { + val currentDirectInbox = currentDirectInbox + currentDirectInbox?.let { + val threads = it.threads + val threadsCopy = if (threads == null) LinkedList() else LinkedList(threads) + threadsCopy.addAll(inbox.threads ?: emptyList()) + inbox.threads = threads } } - this.inbox.postValue(Resource.success(inbox)); - cursor = inbox.getOldestCursor(); - hasOlder = inbox.getHasOlder(); - pendingRequestsTotal.postValue(response.getPendingRequestsTotal()); + this.inbox.postValue(success(inbox)) + cursor = inbox.oldestCursor + hasOlder = inbox.hasOlder + pendingRequestsTotal.postValue(response.pendingRequestsTotal) } - public void setThread(@NonNull final String threadId, - @NonNull final DirectThread thread) { - final DirectInbox inbox = getCurrentDirectInbox(); - if (inbox == null) return; - final int index = getThreadIndex(threadId, inbox); - setThread(inbox, index, thread); + fun setThread( + threadId: String, + thread: DirectThread, + ) { + val inbox = currentDirectInbox ?: return + val index = getThreadIndex(threadId, inbox) + setThread(inbox, index, thread) } - private void setThread(@NonNull final DirectInbox inbox, - final int index, - @NonNull final DirectThread thread) { - if (index < 0) return; - synchronized (this.inbox) { - final List threadsCopy = new LinkedList<>(inbox.getThreads()); - threadsCopy.set(index, thread); + private fun setThread( + inbox: DirectInbox, + index: Int, + thread: DirectThread, + ) { + if (index < 0) return + synchronized(this.inbox) { + val threads = inbox.threads + val threadsCopy = if (threads == null) LinkedList() else LinkedList(threads) + threadsCopy[index] = thread try { - final DirectInbox clone = (DirectInbox) inbox.clone(); - clone.setThreads(threadsCopy); - this.inbox.postValue(Resource.success(clone)); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "setThread: ", e); + val clone = inbox.clone() as DirectInbox + clone.threads = threadsCopy + this.inbox.postValue(success(clone)) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "setThread: ", e) } } } - public void addItemsToThread(@NonNull final String threadId, - final int insertIndex, - @NonNull final Collection items) { - final DirectInbox inbox = getCurrentDirectInbox(); - if (inbox == null) return; - synchronized (THREAD_LOCKS.getUnchecked(threadId)) { - final int index = getThreadIndex(threadId, inbox); - if (index < 0) return; - final List threads = inbox.getThreads(); - final DirectThread thread = threads.get(index); - List list = thread.getItems(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); + fun addItemsToThread( + threadId: String, + insertIndex: Int, + items: Collection, + ) { + val inbox = currentDirectInbox ?: return + synchronized(THREAD_LOCKS.getUnchecked(threadId)) { + val index = getThreadIndex(threadId, inbox) + if (index < 0) return + val threads = inbox.threads ?: return + val thread = threads[index] + val threadItems = thread.items + val list = if (threadItems == null) LinkedList() else LinkedList(threadItems) if (insertIndex >= 0) { - list.addAll(insertIndex, items); + list.addAll(insertIndex, items) } else { - list.addAll(items); + list.addAll(items) } try { - final DirectThread threadClone = (DirectThread) thread.clone(); - threadClone.setItems(list); - setThread(inbox, index, threadClone); - } catch (Exception e) { - Log.e(TAG, "addItemsToThread: ", e); + val threadClone = thread.clone() as DirectThread + threadClone.items = list + setThread(inbox, index, threadClone) + } catch (e: Exception) { + Log.e(TAG, "addItemsToThread: ", e) } } } - public void setItemsToThread(@NonNull final String threadId, - @NonNull final List updatedItems) { - final DirectInbox inbox = getCurrentDirectInbox(); - if (inbox == null) return; - synchronized (THREAD_LOCKS.getUnchecked(threadId)) { - final int index = getThreadIndex(threadId, inbox); - if (index < 0) return; - final List threads = inbox.getThreads(); - final DirectThread thread = threads.get(index); + fun setItemsToThread( + threadId: String, + updatedItems: List, + ) { + val inbox = currentDirectInbox ?: return + synchronized(THREAD_LOCKS.getUnchecked(threadId)) { + val index = getThreadIndex(threadId, inbox) + if (index < 0) return + val threads = inbox.threads ?: return + val thread = threads[index] try { - final DirectThread threadClone = (DirectThread) thread.clone(); - threadClone.setItems(updatedItems); - setThread(inbox, index, threadClone); - } catch (Exception e) { - Log.e(TAG, "setItemsToThread: ", e); + val threadClone = thread.clone() as DirectThread + threadClone.items = updatedItems + setThread(inbox, index, threadClone) + } catch (e: Exception) { + Log.e(TAG, "setItemsToThread: ", e) } } } - private int getThreadIndex(@NonNull final String threadId, - @NonNull final DirectInbox inbox) { - final List threads = inbox.getThreads(); - if (threads == null || threads.isEmpty()) { - return -1; - } - return Iterables.indexOf(threads, t -> { - if (t == null) return false; - return t.getThreadId().equals(threadId); - }); + private fun getThreadIndex( + threadId: String, + inbox: DirectInbox, + ): Int { + val threads = inbox.threads + return if (threads == null || threads.isEmpty()) { + -1 + } else threads.indexOfFirst { it.threadId == threadId } } - private Integer getCurrentUnseenCount() { - final Resource unseenCountResource = unseenCount.getValue(); - return unseenCountResource != null ? unseenCountResource.data : null; - } + private val currentUnseenCount: Int? + get() { + val unseenCountResource = unseenCount.value + return unseenCountResource?.data + } - private void stopCurrentInboxRequest() { - if (inboxRequest == null || inboxRequest.isCanceled() || inboxRequest.isExecuted()) return; - inboxRequest.cancel(); - inboxRequest = null; + private fun stopCurrentInboxRequest() { + inboxRequest?.let { + if (it.isCanceled || it.isExecuted) return + it.cancel() + } + inboxRequest = null } - private void stopCurrentUnseenCountRequest() { - if (unseenCountRequest == null || unseenCountRequest.isCanceled() || unseenCountRequest.isExecuted()) return; - unseenCountRequest.cancel(); - unseenCountRequest = null; + private fun stopCurrentUnseenCountRequest() { + unseenCountRequest?.let { + if (it.isCanceled || it.isExecuted) return + it.cancel() + } + unseenCountRequest = null } - public void onDestroy() { - stopCurrentInboxRequest(); - stopCurrentUnseenCountRequest(); + fun onDestroy() { + stopCurrentInboxRequest() + stopCurrentUnseenCountRequest() } - public void addThread(@NonNull final DirectThread thread, final int insertIndex) { - if (insertIndex < 0) return; - synchronized (this.inbox) { - final DirectInbox currentDirectInbox = getCurrentDirectInbox(); - if (currentDirectInbox == null) return; - final List threadsCopy = new LinkedList<>(currentDirectInbox.getThreads()); - threadsCopy.add(insertIndex, thread); + fun addThread(thread: DirectThread, insertIndex: Int) { + if (insertIndex < 0) return + synchronized(inbox) { + val currentDirectInbox = currentDirectInbox ?: return + val threads = currentDirectInbox.threads + val threadsCopy = if (threads == null) LinkedList() else LinkedList(threads) + threadsCopy.add(insertIndex, thread) try { - final DirectInbox clone = (DirectInbox) currentDirectInbox.clone(); - clone.setThreads(threadsCopy); - this.inbox.setValue(Resource.success(clone)); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "setThread: ", e); + val clone = currentDirectInbox.clone() as DirectInbox + clone.threads = threadsCopy + inbox.setValue(success(clone)) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "setThread: ", e) } } } - public void removeThread(@NonNull final String threadId) { - synchronized (this.inbox) { - final DirectInbox currentDirectInbox = getCurrentDirectInbox(); - if (currentDirectInbox == null) return; - final List threadsCopy = currentDirectInbox.getThreads() - .stream() - .filter(t -> !t.getThreadId().equals(threadId)) - .collect(Collectors.toList()); + fun removeThread(threadId: String) { + synchronized(inbox) { + val currentDirectInbox = currentDirectInbox ?: return + val threads = currentDirectInbox.threads ?: return + val threadsCopy = threads.asSequence().filter { it.threadId != threadId }.toList() try { - final DirectInbox clone = (DirectInbox) currentDirectInbox.clone(); - clone.setThreads(threadsCopy); - this.inbox.postValue(Resource.success(clone)); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "setThread: ", e); + val clone = currentDirectInbox.clone() as DirectInbox + clone.threads = threadsCopy + inbox.postValue(success(clone)) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "setThread: ", e) } } } - public void setPendingRequestsTotal(final int total) { - pendingRequestsTotal.postValue(total); + fun setPendingRequestsTotal(total: Int) { + pendingRequestsTotal.postValue(total) } - public boolean containsThread(final String threadId) { - if (threadId == null) return false; - synchronized (this.inbox) { - final DirectInbox currentDirectInbox = getCurrentDirectInbox(); - if (currentDirectInbox == null) return false; - final List threads = currentDirectInbox.getThreads(); - if (threads == null) return false; - return threads.stream().anyMatch(thread -> Objects.equals(thread.getThreadId(), threadId)); + fun containsThread(threadId: String?): Boolean { + if (threadId == null) return false + synchronized(inbox) { + val currentDirectInbox = currentDirectInbox ?: return false + val threads = currentDirectInbox.threads ?: return false + return threads.any { it.threadId == threadId } + } + } + + companion object { + private val TAG = InboxManager::class.java.simpleName + private val THREAD_LOCKS = CacheBuilder + .newBuilder() + .expireAfterAccess(1, TimeUnit.MINUTES) // max lock time ever expected + .build(CacheLoader.from { Object() }) + private val THREAD_COMPARATOR = Comparator { t1: DirectThread, t2: DirectThread -> + val t1FirstDirectItem = t1.firstDirectItem + val t2FirstDirectItem = t2.firstDirectItem + if (t1FirstDirectItem == null && t2FirstDirectItem == null) return@Comparator 0 + if (t1FirstDirectItem == null) return@Comparator 1 + if (t2FirstDirectItem == null) return@Comparator -1 + t2FirstDirectItem.getTimestamp().compareTo(t1FirstDirectItem.getTimestamp()) + } + + fun getInstance(pending: Boolean): InboxManager { + return InboxManager(pending) + } + } + + init { + val cookie = Utils.settingsHelper.getString(Constants.COOKIE) + val viewerId = getUserIdFromCookie(cookie) + val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) + val csrfToken = getCsrfTokenFromCookie(cookie) + require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" } + service = getInstance(csrfToken, viewerId, deviceUuid) + + // Transformations + threads = Transformations.distinctUntilChanged(Transformations.map(inbox) { inboxResource: Resource? -> + if (inboxResource == null) { + return@map emptyList() + } + val inbox = inboxResource.data + val threads = inbox?.threads ?: emptyList() + ImmutableList.sortedCopyOf(THREAD_COMPARATOR, threads) + }) + fetchInbox() + if (!pending) { + fetchUnseenCount() } } -} +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java deleted file mode 100644 index df3b9ff8..00000000 --- a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java +++ /dev/null @@ -1,1880 +0,0 @@ -package awais.instagrabber.managers; - -import android.content.ContentResolver; -import android.net.Uri; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.Pair; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; - -import org.json.JSONObject; - -import java.io.File; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -import awais.instagrabber.R; -import awais.instagrabber.customviews.emoji.Emoji; -import awais.instagrabber.models.Resource; -import awais.instagrabber.models.Resource.Status; -import awais.instagrabber.models.UploadVideoOptions; -import awais.instagrabber.models.enums.DirectItemType; -import awais.instagrabber.repositories.requests.UploadFinishOptions; -import awais.instagrabber.repositories.requests.VideoOptions; -import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds; -import awais.instagrabber.repositories.responses.FriendshipChangeResponse; -import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.repositories.responses.directmessages.DirectInbox; -import awais.instagrabber.repositories.responses.directmessages.DirectItem; -import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction; -import awais.instagrabber.repositories.responses.directmessages.DirectItemReactions; -import awais.instagrabber.repositories.responses.directmessages.DirectItemSeenResponse; -import awais.instagrabber.repositories.responses.directmessages.DirectItemSeenResponsePayload; -import awais.instagrabber.repositories.responses.directmessages.DirectThread; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponseMessageMetadata; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponsePayload; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; -import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; -import awais.instagrabber.repositories.responses.giphy.GiphyGif; -import awais.instagrabber.utils.BitmapUtils; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DirectItemFactory; -import awais.instagrabber.utils.MediaController; -import awais.instagrabber.utils.MediaUploadHelper; -import awais.instagrabber.utils.MediaUploader; -import awais.instagrabber.utils.MediaUtils; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; -import awais.instagrabber.webservices.DirectMessagesService; -import awais.instagrabber.webservices.FriendshipService; -import awais.instagrabber.webservices.MediaService; -import awais.instagrabber.webservices.ServiceCallback; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -import static androidx.lifecycle.Transformations.distinctUntilChanged; -import static androidx.lifecycle.Transformations.map; -import static awais.instagrabber.utils.Utils.settingsHelper; - -public final class ThreadManager { - private static final String TAG = ThreadManager.class.getSimpleName(); - private static final Object LOCK = new Object(); - private static final Map INSTANCE_MAP = new ConcurrentHashMap<>(); - - private final MutableLiveData> fetching = new MutableLiveData<>(); - private final MutableLiveData replyToItem = new MutableLiveData<>(); - private final MutableLiveData pendingRequests = new MutableLiveData<>(null); - - private final String threadId; - private final long viewerId; - private final ThreadIdOrUserIds threadIdOrUserIds; - private final User currentUser; - private final ContentResolver contentResolver; - private final DirectMessagesManager messagesManager; - - private DirectMessagesService service; - private MediaService mediaService; - private FriendshipService friendshipService; - private InboxManager inboxManager; - private LiveData thread; - private LiveData inputMode; - private LiveData threadTitle; - private LiveData> users; - private LiveData> usersWithCurrent; - private LiveData> leftUsers; - private LiveData, List>> usersAndLeftUsers; - private LiveData pending; - private LiveData> adminUserIds; - private LiveData> items; - private LiveData isViewerAdmin; - private LiveData isGroup; - private LiveData isMuted; - private LiveData isApprovalRequiredToJoin; - private LiveData isMentionsMuted; - private LiveData pendingRequestsCount; - private LiveData inviter; - private boolean hasOlder = true; - private String cursor; - private Call chatsRequest; - - public static ThreadManager getInstance(@NonNull final String threadId, - final boolean pending, - @NonNull final User currentUser, - @NonNull final ContentResolver contentResolver) { - ThreadManager instance = INSTANCE_MAP.get(threadId); - if (instance == null) { - synchronized (LOCK) { - instance = INSTANCE_MAP.get(threadId); - if (instance == null) { - instance = new ThreadManager(threadId, pending, currentUser, contentResolver); - INSTANCE_MAP.put(threadId, instance); - } - } - } - return instance; - } - - private String getThreadId() { - return threadId; - } - - private ThreadManager(@NonNull final String threadId, - final boolean pending, - @NonNull final User currentUser, - @NonNull final ContentResolver contentResolver) { - messagesManager = DirectMessagesManager.getInstance(); - this.inboxManager = pending ? messagesManager.getPendingInboxManager() : messagesManager.getInboxManager(); - this.threadId = threadId; - this.threadIdOrUserIds = ThreadIdOrUserIds.of(threadId); - this.currentUser = currentUser; - this.contentResolver = contentResolver; - final String cookie = settingsHelper.getString(Constants.COOKIE); - viewerId = CookieUtils.getUserIdFromCookie(cookie); - final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); - final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); - if (csrfToken == null) return; - // if (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) { - // throw new IllegalArgumentException("User is not logged in!"); - // } - service = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid); - mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId); - friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, viewerId); - setupTransformations(); - // fetchChats(); - } - - public void moveFromPending() { - final DirectMessagesManager messagesManager = DirectMessagesManager.getInstance(); - this.inboxManager = messagesManager.getInboxManager(); - setupTransformations(); - } - - private void setupTransformations() { - // Transformations - thread = distinctUntilChanged(map(inboxManager.getInbox(), inboxResource -> { - if (inboxResource == null) { - return null; - } - final DirectInbox inbox = inboxResource.data; - if (inbox == null) { - return null; - } - final List threads = inbox.getThreads(); - if (threads == null || threads.isEmpty()) { - return null; - } - final DirectThread thread = threads.stream() - .filter(t -> Objects.equals(t.getThreadId(), threadId)) - .findFirst() - .orElse(null); - if (thread != null) { - cursor = thread.getOldestCursor(); - hasOlder = thread.getHasOlder(); - } - return thread; - })); - inputMode = distinctUntilChanged(map(thread, t -> { - if (t == null) return 1; - return t.getInputMode(); - })); - threadTitle = distinctUntilChanged(map(thread, t -> { - if (t == null) return null; - return t.getThreadTitle(); - })); - users = distinctUntilChanged(map(thread, t -> { - if (t == null) return Collections.emptyList(); - return t.getUsers(); - })); - usersWithCurrent = distinctUntilChanged(map(thread, t -> { - if (t == null) return Collections.emptyList(); - return getUsersWithCurrentUser(t); - })); - leftUsers = distinctUntilChanged(map(thread, t -> { - if (t == null) return Collections.emptyList(); - return t.getLeftUsers(); - })); - usersAndLeftUsers = distinctUntilChanged(map(thread, t -> { - if (t == null) { - return new Pair<>(Collections.emptyList(), Collections.emptyList()); - } - final List users = getUsersWithCurrentUser(t); - final List leftUsers = t.getLeftUsers(); - return new Pair<>(users, leftUsers); - })); - pending = distinctUntilChanged(map(thread, t -> { - if (t == null) return true; - return t.getPending(); - })); - adminUserIds = distinctUntilChanged(map(thread, t -> { - if (t == null) return Collections.emptyList(); - return t.getAdminUserIds(); - })); - items = distinctUntilChanged(map(thread, t -> { - if (t == null) return Collections.emptyList(); - return t.getItems(); - })); - isViewerAdmin = distinctUntilChanged(map(thread, t -> { - if (t == null) return false; - return t.getAdminUserIds().contains(viewerId); - })); - isGroup = distinctUntilChanged(map(thread, t -> { - if (t == null) return false; - return t.isGroup(); - })); - isMuted = distinctUntilChanged(map(thread, t -> { - if (t == null) return false; - return t.getMuted(); - })); - isApprovalRequiredToJoin = distinctUntilChanged(map(thread, t -> { - if (t == null) return false; - return t.getApprovalRequiredForNewMembers(); - })); - isMentionsMuted = distinctUntilChanged(map(thread, t -> { - if (t == null) return false; - return t.getMentionsMuted(); - })); - pendingRequestsCount = distinctUntilChanged(map(pendingRequests, p -> { - if (p == null) return 0; - return p.getTotalParticipantRequests(); - })); - inviter = distinctUntilChanged(map(thread, t -> { - if (t == null) return null; - return t.getInviter(); - })); - } - - private List getUsersWithCurrentUser(final DirectThread t) { - final ImmutableList.Builder builder = ImmutableList.builder(); - if (currentUser != null) { - builder.add(currentUser); - } - final List users = t.getUsers(); - if (users != null) { - builder.addAll(users); - } - return builder.build(); - } - - public LiveData getThread() { - return thread; - } - - public LiveData getInputMode() { - return inputMode; - } - - public LiveData getThreadTitle() { - return threadTitle; - } - - public LiveData> getUsers() { - return users; - } - - public LiveData> getUsersWithCurrent() { - return usersWithCurrent; - } - - public LiveData> getLeftUsers() { - return leftUsers; - } - - public LiveData, List>> getUsersAndLeftUsers() { - return usersAndLeftUsers; - } - - public LiveData isPending() { - return pending; - } - - public LiveData> getAdminUserIds() { - return adminUserIds; - } - - public LiveData> getItems() { - return items; - } - - public LiveData> isFetching() { - return fetching; - } - - public LiveData getReplyToItem() { - return replyToItem; - } - - public LiveData getPendingRequestsCount() { - return pendingRequestsCount; - } - - public LiveData getPendingRequests() { - return pendingRequests; - } - - public LiveData isGroup() { - return isGroup; - } - - public LiveData isMuted() { - return isMuted; - } - - public LiveData isApprovalRequiredToJoin() { - return isApprovalRequiredToJoin; - } - - public LiveData isViewerAdmin() { - return isViewerAdmin; - } - - public LiveData isMentionsMuted() { - return isMentionsMuted; - } - - public void fetchChats() { - final Resource fetchingValue = fetching.getValue(); - if ((fetchingValue != null && fetchingValue.status == Status.LOADING) || !hasOlder) return; - fetching.postValue(Resource.loading(null)); - chatsRequest = service.fetchThread(threadId, cursor); - chatsRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final DirectThreadFeedResponse feedResponse = response.body(); - if (feedResponse == null) { - fetching.postValue(Resource.error(R.string.generic_null_response, null)); - Log.e(TAG, "onResponse: response was null!"); - return; - } - if (!feedResponse.getStatus().equals("ok")) { - fetching.postValue(Resource.error(R.string.generic_not_ok_response, null)); - return; - } - final DirectThread thread = feedResponse.getThread(); - setThread(thread); - fetching.postValue(Resource.success(new Object())); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Failed fetching dm chats", t); - fetching.postValue(Resource.error(t.getMessage(), null)); - hasOlder = false; - } - }); - if (cursor == null) { - fetchPendingRequests(); - } - } - - public void fetchPendingRequests() { - final Boolean isGroup = this.isGroup.getValue(); - if (isGroup == null || !isGroup) return; - final Call request = service.participantRequests(threadId, 1, null); - request.enqueue(new Callback() { - - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (!response.isSuccessful()) { - if (response.errorBody() != null) { - try { - final String string = response.errorBody().string(); - final String msg = String.format(Locale.US, - "onResponse: url: %s, responseCode: %d, errorBody: %s", - call.request().url().toString(), - response.code(), - string); - Log.e(TAG, msg); - } catch (IOException e) { - Log.e(TAG, "onResponse: ", e); - } - return; - } - Log.e(TAG, "onResponse: request was not successful and response error body was null"); - return; - } - final DirectThreadParticipantRequestsResponse body = response.body(); - if (body == null) { - Log.e(TAG, "onResponse: response body was null"); - return; - } - pendingRequests.postValue(body); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - } - }); - } - - private void setThread(@NonNull final DirectThread thread, final boolean skipItems) { - // if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) { - // fetchPendingRequests(); - // } - final List items = thread.getItems(); - if (skipItems) { - final DirectThread currentThread = this.thread.getValue(); - if (currentThread != null) { - thread.setItems(currentThread.getItems()); - } - } - if (!skipItems && !TextUtils.isEmpty(cursor)) { - final DirectThread currentThread = this.thread.getValue(); - if (currentThread != null) { - List list = currentThread.getItems(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - list.addAll(items); - thread.setItems(list); - } - } - inboxManager.setThread(threadId, thread); - } - - private void setThread(@NonNull final DirectThread thread) { - setThread(thread, false); - } - - private void setThreadUsers(final List users, final List leftUsers) { - final DirectThread currentThread = this.thread.getValue(); - if (currentThread == null) return; - final DirectThread thread; - try { - thread = (DirectThread) currentThread.clone(); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "setThreadUsers: ", e); - return; - } - if (users != null) { - thread.setUsers(users); - } - if (leftUsers != null) { - thread.setLeftUsers(leftUsers); - } - inboxManager.setThread(threadId, thread); - } - - private void addItems(final int index, final Collection items) { - if (items == null) return; - inboxManager.addItemsToThread(threadId, index, items); - } - - private void addReaction(@NonNull final DirectItem item, @NonNull final Emoji emoji) { - if (currentUser == null) return; - final boolean isLike = emoji.getUnicode().equals("❤️"); - DirectItemReactions reactions = item.getReactions(); - if (reactions == null) { - reactions = new DirectItemReactions(null, null); - } else { - try { - reactions = (DirectItemReactions) reactions.clone(); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "addReaction: ", e); - return; - } - } - if (isLike) { - final List likes = addEmoji(reactions.getLikes(), null, false); - reactions.setLikes(likes); - } - final List emojis = addEmoji(reactions.getEmojis(), emoji.getUnicode(), true); - reactions.setEmojis(emojis); - List list = this.items.getValue(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - int index = getItemIndex(item, list); - if (index >= 0) { - try { - final DirectItem clone = (DirectItem) list.get(index).clone(); - clone.setReactions(reactions); - list.set(index, clone); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "addReaction: error cloning", e); - } - } - inboxManager.setItemsToThread(threadId, list); - } - - private void removeReaction(final DirectItem item) { - try { - final DirectItem itemClone = (DirectItem) item.clone(); - final DirectItemReactions reactions = itemClone.getReactions(); - final DirectItemReactions reactionsClone = (DirectItemReactions) reactions.clone(); - final List likes = reactionsClone.getLikes(); - if (likes != null) { - final List updatedLikes = likes.stream() - .filter(like -> like.getSenderId() != viewerId) - .collect(Collectors.toList()); - reactionsClone.setLikes(updatedLikes); - } - final List emojis = reactionsClone.getEmojis(); - if (emojis != null) { - final List updatedEmojis = emojis.stream() - .filter(emoji -> emoji.getSenderId() != viewerId) - .collect(Collectors.toList()); - reactionsClone.setEmojis(updatedEmojis); - } - itemClone.setReactions(reactionsClone); - List list = this.items.getValue(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - int index = getItemIndex(item, list); - if (index >= 0) { - list.set(index, itemClone); - } - inboxManager.setItemsToThread(threadId, list); - } catch (Exception e) { - Log.e(TAG, "removeReaction: ", e); - } - } - - private int removeItem(final DirectItem item) { - if (item == null) return 0; - List list = this.items.getValue(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - int index = getItemIndex(item, list); - if (index >= 0) { - list.remove(index); - inboxManager.setItemsToThread(threadId, list); - } - return index; - } - - private List addEmoji(final List reactionList, - final String emoji, - final boolean shouldReplaceIfAlreadyReacted) { - if (currentUser == null) return reactionList; - final List temp = reactionList == null ? new ArrayList<>() : new ArrayList<>(reactionList); - int index = -1; - for (int i = 0; i < temp.size(); i++) { - final DirectItemEmojiReaction directItemEmojiReaction = temp.get(i); - if (directItemEmojiReaction.getSenderId() == currentUser.getPk()) { - index = i; - break; - } - } - final DirectItemEmojiReaction reaction = new DirectItemEmojiReaction( - currentUser.getPk(), - System.currentTimeMillis() * 1000, - emoji, - "none" - ); - if (index < 0) { - temp.add(0, reaction); - } else if (shouldReplaceIfAlreadyReacted) { - temp.set(index, reaction); - } - return temp; - } - - public LiveData> sendText(final String text) { - final MutableLiveData> data = new MutableLiveData<>(); - final Long userId = getCurrentUserId(data); - if (userId == null) return data; - final String clientContext = UUID.randomUUID().toString(); - final DirectItem replyToItemValue = replyToItem.getValue(); - final DirectItem directItem = DirectItemFactory.createText(userId, clientContext, text, replyToItemValue); - // Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId()); - directItem.setPending(true); - addItems(0, Collections.singletonList(directItem)); - data.postValue(Resource.loading(directItem)); - final String repliedToItemId = replyToItemValue != null ? replyToItemValue.getItemId() : null; - final String repliedToClientContext = replyToItemValue != null ? replyToItemValue.getClientContext() : null; - final Call request = service.broadcastText( - clientContext, - threadIdOrUserIds, - text, - repliedToItemId, - repliedToClientContext - ); - enqueueRequest(request, data, directItem); - return data; - } - - public LiveData> sendUri(final MediaController.MediaEntry entry) { - final MutableLiveData> data = new MutableLiveData<>(); - if (entry == null) { - data.postValue(Resource.error("Entry is null", null)); - return data; - } - final Uri uri = Uri.fromFile(new File(entry.path)); - if (!entry.isVideo) { - sendPhoto(data, uri, entry.width, entry.height); - return data; - } - sendVideo(data, uri, entry.size, entry.duration, entry.width, entry.height); - return data; - } - - public LiveData> sendUri(final Uri uri) { - final MutableLiveData> data = new MutableLiveData<>(); - if (uri == null) { - data.postValue(Resource.error("Uri is null", null)); - return data; - } - final String mimeType = Utils.getMimeType(uri, contentResolver); - if (TextUtils.isEmpty(mimeType)) { - data.postValue(Resource.error("Unknown MediaType", null)); - return data; - } - final boolean isPhoto = mimeType.startsWith("image"); - if (isPhoto) { - sendPhoto(data, uri); - return data; - } - if (mimeType.startsWith("video")) { - sendVideo(data, uri); - } - return data; - } - - public LiveData> sendAnimatedMedia(@NonNull final GiphyGif giphyGif) { - final MutableLiveData> data = new MutableLiveData<>(); - final Long userId = getCurrentUserId(data); - if (userId == null) return data; - final String clientContext = UUID.randomUUID().toString(); - final DirectItem directItem = DirectItemFactory.createAnimatedMedia(userId, clientContext, giphyGif); - directItem.setPending(true); - addItems(0, Collections.singletonList(directItem)); - data.postValue(Resource.loading(directItem)); - final Call request = service.broadcastAnimatedMedia( - clientContext, - threadIdOrUserIds, - giphyGif - ); - enqueueRequest(request, data, directItem); - return data; - } - - public void sendVoice(@NonNull final MutableLiveData> data, - @NonNull final Uri uri, - @NonNull final List waveform, - final int samplingFreq, - final long duration, - final long byteLength) { - if (duration > 60000) { - // instagram does not allow uploading audio longer than 60 secs for Direct messages - data.postValue(Resource.error(R.string.dms_ERROR_AUDIO_TOO_LONG, null)); - return; - } - final Long userId = getCurrentUserId(data); - if (userId == null) return; - final String clientContext = UUID.randomUUID().toString(); - final DirectItem directItem = DirectItemFactory.createVoice(userId, clientContext, uri, duration, waveform, samplingFreq); - directItem.setPending(true); - addItems(0, Collections.singletonList(directItem)); - data.postValue(Resource.loading(directItem)); - final UploadVideoOptions uploadDmVoiceOptions = MediaUploadHelper.createUploadDmVoiceOptions(byteLength, duration); - MediaUploader.uploadVideo(uri, contentResolver, uploadDmVoiceOptions, new MediaUploader.OnMediaUploadCompleteListener() { - @Override - public void onUploadComplete(final MediaUploader.MediaUploadResponse response) { - // Log.d(TAG, "onUploadComplete: " + response); - if (handleInvalidResponse(data, response)) return; - final UploadFinishOptions uploadFinishOptions = new UploadFinishOptions( - uploadDmVoiceOptions.getUploadId(), - "4", - null - ); - final Call uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions); - uploadFinishRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (response.isSuccessful()) { - final Call request = service.broadcastVoice( - clientContext, - threadIdOrUserIds, - uploadDmVoiceOptions.getUploadId(), - waveform, - samplingFreq - ); - enqueueRequest(request, data, directItem); - return; - } - if (response.errorBody() != null) { - handleErrorBody(call, response, data); - return; - } - data.postValue(Resource.error("uploadFinishRequest was not successful and response error body was null", directItem)); - Log.e(TAG, "uploadFinishRequest was not successful and response error body was null"); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "onFailure: ", t); - } - }); - } - - @Override - public void onFailure(final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "onFailure: ", t); - } - }); - } - - @NonNull - public LiveData> sendReaction(@NonNull final DirectItem item, - @NonNull final Emoji emoji) { - final MutableLiveData> data = new MutableLiveData<>(); - final Long userId = getCurrentUserId(data); - if (userId == null) { - data.postValue(Resource.error("userId is null", null)); - return data; - } - final String clientContext = UUID.randomUUID().toString(); - // Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId()); - data.postValue(Resource.loading(item)); - addReaction(item, emoji); - String emojiUnicode = null; - if (!emoji.getUnicode().equals("❤️")) { - emojiUnicode = emoji.getUnicode(); - } - final String itemId = item.getItemId(); - if (itemId == null) { - data.postValue(Resource.error("itemId is null", null)); - return data; - } - final Call request = service.broadcastReaction( - clientContext, - threadIdOrUserIds, - itemId, - emojiUnicode, - false - ); - handleBroadcastReactionRequest(data, item, request); - return data; - } - - public LiveData> sendDeleteReaction(@NonNull final String itemId) { - final MutableLiveData> data = new MutableLiveData<>(); - final DirectItem item = getItem(itemId); - if (item == null) { - data.postValue(Resource.error("Invalid item", null)); - return data; - } - final DirectItemReactions reactions = item.getReactions(); - if (reactions == null) { - // already removed? - data.postValue(Resource.success(item)); - return data; - } - removeReaction(item); - final String clientContext = UUID.randomUUID().toString(); - final String itemId1 = item.getItemId(); - if (itemId1 == null) { - data.postValue(Resource.error("itemId is null", null)); - return data; - } - final Call request = service.broadcastReaction(clientContext, threadIdOrUserIds, itemId1, null, true); - handleBroadcastReactionRequest(data, item, request); - return data; - } - - public LiveData> unsend(final DirectItem item) { - final MutableLiveData> data = new MutableLiveData<>(); - if (item == null) { - data.postValue(Resource.error("item is null", null)); - return data; - } - final int index = removeItem(item); - final Call request = service.deleteItem(threadId, item.getItemId()); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (response.isSuccessful()) { - // Log.d(TAG, "onResponse: " + response.body()); - return; - } - // add the item back if unsuccessful - addItems(index, Collections.singletonList(item)); - if (response.errorBody() != null) { - handleErrorBody(call, response, data); - return; - } - data.postValue(Resource.error(R.string.generic_failed_request, item)); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - data.postValue(Resource.error(t.getMessage(), item)); - Log.e(TAG, "enqueueRequest: onFailure: ", t); - } - }); - return data; - } - - public void forward(final Set recipients, final DirectItem itemToForward) { - if (recipients == null || itemToForward == null) return; - for (final RankedRecipient recipient : recipients) { - forward(recipient, itemToForward); - } - } - - public void forward(final RankedRecipient recipient, final DirectItem itemToForward) { - if (recipient == null || itemToForward == null) return; - if (recipient.getThread() == null && recipient.getUser() != null) { - // create thread and forward - messagesManager.createThread(recipient.getUser().getPk(), directThread -> { - forward(directThread, itemToForward); - return null; - }); - } - if (recipient.getThread() != null) { - // just forward - final DirectThread thread = recipient.getThread(); - forward(thread, itemToForward); - } - } - - public void setReplyToItem(final DirectItem item) { - // Log.d(TAG, "setReplyToItem: " + item); - replyToItem.postValue(item); - } - - @NonNull - private LiveData> forward(@NonNull final DirectThread thread, @NonNull final DirectItem itemToForward) { - final MutableLiveData> data = new MutableLiveData<>(); - if (itemToForward.getItemId() == null) { - data.postValue(Resource.error("item id is null", null)); - return data; - } - final DirectItemType itemType = itemToForward.getItemType(); - if (itemType == null) { - data.postValue(Resource.error("item type is null", null)); - return data; - } - final String itemTypeName = itemType.getName(); - if (itemTypeName == null) { - Log.e(TAG, "forward: itemTypeName was null!"); - data.postValue(Resource.error("itemTypeName is null", null)); - return data; - } - data.postValue(Resource.loading(null)); - final Call request = service.forward(thread.getThreadId(), - itemTypeName, - threadId, - itemToForward.getItemId()); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (response.isSuccessful()) { - data.postValue(Resource.success(new Object())); - return; - } - if (response.errorBody() != null) { - try { - final String string = response.errorBody().string(); - final String msg = String.format(Locale.US, - "onResponse: url: %s, responseCode: %d, errorBody: %s", - call.request().url().toString(), - response.code(), - string); - Log.e(TAG, msg); - data.postValue(Resource.error(msg, null)); - } catch (IOException e) { - Log.e(TAG, "onResponse: ", e); - data.postValue(Resource.error(e.getMessage(), null)); - } - return; - } - final String msg = "onResponse: request was not successful and response error body was null"; - Log.e(TAG, msg); - data.postValue(Resource.error(msg, null)); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> acceptRequest() { - final MutableLiveData> data = new MutableLiveData<>(); - final Call request = service.approveRequest(threadId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (!response.isSuccessful()) { - try { - final String string = response.errorBody() != null ? response.errorBody().string() : ""; - final String msg = String.format(Locale.US, - "onResponse: url: %s, responseCode: %d, errorBody: %s", - call.request().url().toString(), - response.code(), - string); - Log.e(TAG, msg); - data.postValue(Resource.error(msg, null)); - return; - } catch (IOException e) { - Log.e(TAG, "onResponse: ", e); - } - return; - } - data.postValue(Resource.success(new Object())); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> declineRequest() { - final MutableLiveData> data = new MutableLiveData<>(); - final Call request = service.declineRequest(threadId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (!response.isSuccessful()) { - try { - final String string = response.errorBody() != null ? response.errorBody().string() : ""; - final String msg = String.format(Locale.US, - "onResponse: url: %s, responseCode: %d, errorBody: %s", - call.request().url().toString(), - response.code(), - string); - Log.e(TAG, msg); - data.postValue(Resource.error(msg, null)); - return; - } catch (IOException e) { - Log.e(TAG, "onResponse: ", e); - } - return; - } - data.postValue(Resource.success(new Object())); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public void refreshChats() { - final Resource isFetching = fetching.getValue(); - if (isFetching != null && isFetching.status == Status.LOADING) { - stopCurrentRequest(); - } - cursor = null; - hasOlder = true; - fetchChats(); - } - - private void sendPhoto(@NonNull final MutableLiveData> data, - @NonNull final Uri uri) { - try { - final Pair dimensions = BitmapUtils.decodeDimensions(contentResolver, uri); - if (dimensions == null) { - data.postValue(Resource.error("Decoding dimensions failed", null)); - return; - } - sendPhoto(data, uri, dimensions.first, dimensions.second); - } catch (IOException e) { - data.postValue(Resource.error(e.getMessage(), null)); - Log.e(TAG, "sendPhoto: ", e); - } - } - - private void sendPhoto(@NonNull final MutableLiveData> data, - @NonNull final Uri uri, - final int width, - final int height) { - final Long userId = getCurrentUserId(data); - if (userId == null) return; - final String clientContext = UUID.randomUUID().toString(); - final DirectItem directItem = DirectItemFactory.createImageOrVideo(userId, clientContext, uri, width, height, false); - directItem.setPending(true); - addItems(0, Collections.singletonList(directItem)); - data.postValue(Resource.loading(directItem)); - MediaUploader.uploadPhoto(uri, contentResolver, new MediaUploader.OnMediaUploadCompleteListener() { - @Override - public void onUploadComplete(final MediaUploader.MediaUploadResponse response) { - if (handleInvalidResponse(data, response)) return; - final String uploadId = response.getResponse().optString("upload_id"); - final Call request = service.broadcastPhoto(clientContext, threadIdOrUserIds, uploadId); - enqueueRequest(request, data, directItem); - } - - @Override - public void onFailure(final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "onFailure: ", t); - } - }); - } - - private void sendVideo(@NonNull final MutableLiveData> data, - @NonNull final Uri uri) { - MediaUtils.getVideoInfo(contentResolver, uri, new MediaUtils.OnInfoLoadListener() { - @Override - public void onLoad(@Nullable final MediaUtils.VideoInfo info) { - if (info == null) { - data.postValue(Resource.error("Could not get the video info", null)); - return; - } - sendVideo(data, uri, info.size, info.duration, info.width, info.height); - } - - @Override - public void onFailure(final Throwable t) { - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - } - - private void sendVideo(@NonNull final MutableLiveData> data, - @NonNull final Uri uri, - final long byteLength, - final long duration, - final int width, - final int height) { - if (duration > 60000) { - // instagram does not allow uploading videos longer than 60 secs for Direct messages - data.postValue(Resource.error(R.string.dms_ERROR_VIDEO_TOO_LONG, null)); - return; - } - final Long userId = getCurrentUserId(data); - if (userId == null) return; - final String clientContext = UUID.randomUUID().toString(); - final DirectItem directItem = DirectItemFactory.createImageOrVideo(userId, clientContext, uri, width, height, true); - directItem.setPending(true); - addItems(0, Collections.singletonList(directItem)); - data.postValue(Resource.loading(directItem)); - final UploadVideoOptions uploadDmVideoOptions = MediaUploadHelper.createUploadDmVideoOptions(byteLength, duration, width, height); - MediaUploader.uploadVideo(uri, contentResolver, uploadDmVideoOptions, new MediaUploader.OnMediaUploadCompleteListener() { - @Override - public void onUploadComplete(final MediaUploader.MediaUploadResponse response) { - // Log.d(TAG, "onUploadComplete: " + response); - if (handleInvalidResponse(data, response)) return; - final UploadFinishOptions uploadFinishOptions = new UploadFinishOptions( - uploadDmVideoOptions.getUploadId(), - "2", - new VideoOptions(duration / 1000f, Collections.emptyList(), 0, false) - ); - final Call uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions); - uploadFinishRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (response.isSuccessful()) { - final Call request = service.broadcastVideo( - clientContext, - threadIdOrUserIds, - uploadDmVideoOptions.getUploadId(), - "", - true - ); - enqueueRequest(request, data, directItem); - return; - } - if (response.errorBody() != null) { - handleErrorBody(call, response, data); - return; - } - data.postValue(Resource.error("uploadFinishRequest was not successful and response error body was null", directItem)); - Log.e(TAG, "uploadFinishRequest was not successful and response error body was null"); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "onFailure: ", t); - } - }); - } - - @Override - public void onFailure(final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "onFailure: ", t); - } - }); - } - - private void enqueueRequest(@NonNull final Call request, - @NonNull final MutableLiveData> data, - @NonNull final DirectItem directItem) { - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (response.isSuccessful()) { - final DirectThreadBroadcastResponse broadcastResponse = response.body(); - if (broadcastResponse == null) { - data.postValue(Resource.error(R.string.generic_null_response, directItem)); - Log.e(TAG, "enqueueRequest: onResponse: response body is null"); - return; - } - final String payloadClientContext; - final long timestamp; - final String itemId; - final DirectThreadBroadcastResponsePayload payload = broadcastResponse.getPayload(); - if (payload == null) { - final List messageMetadata = broadcastResponse.getMessageMetadata(); - if (messageMetadata == null || messageMetadata.isEmpty()) { - data.postValue(Resource.success(directItem)); - return; - } - final DirectThreadBroadcastResponseMessageMetadata metadata = messageMetadata.get(0); - payloadClientContext = metadata.getClientContext(); - itemId = metadata.getItemId(); - timestamp = metadata.getTimestamp(); - } else { - payloadClientContext = payload.getClientContext(); - timestamp = payload.getTimestamp(); - itemId = payload.getItemId(); - } - updateItemSent(payloadClientContext, timestamp, itemId); - data.postValue(Resource.success(directItem)); - return; - } - if (response.errorBody() != null) { - handleErrorBody(call, response, data); - } - data.postValue(Resource.error(R.string.generic_failed_request, directItem)); - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "enqueueRequest: onFailure: ", t); - } - }); - } - - private void updateItemSent(final String clientContext, final long timestamp, final String itemId) { - if (clientContext == null) return; - List list = this.items.getValue(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - final int index = Iterables.indexOf(list, item -> { - if (item == null) return false; - return item.getClientContext().equals(clientContext); - }); - if (index < 0) return; - final DirectItem directItem = list.get(index); - try { - final DirectItem itemClone = (DirectItem) directItem.clone(); - itemClone.setItemId(itemId); - itemClone.setPending(false); - itemClone.setTimestamp(timestamp); - list.set(index, itemClone); - inboxManager.setItemsToThread(threadId, list); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "updateItemSent: ", e); - } - } - - private void handleErrorBody(@NonNull final Call call, - @NonNull final Response response, - final MutableLiveData> data) { - try { - final String string = response.errorBody() != null ? response.errorBody().string() : ""; - final String msg = String.format(Locale.US, - "onResponse: url: %s, responseCode: %d, errorBody: %s", - call.request().url().toString(), - response.code(), - string); - if (data != null) { - data.postValue(Resource.error(msg, null)); - } - Log.e(TAG, msg); - } catch (IOException e) { - if (data != null) { - data.postValue(Resource.error(e.getMessage(), null)); - } - Log.e(TAG, "onResponse: ", e); - } - } - - private boolean handleInvalidResponse(final MutableLiveData> data, - @NonNull final MediaUploader.MediaUploadResponse response) { - final JSONObject responseJson = response.getResponse(); - if (responseJson == null || response.getResponseCode() != HttpURLConnection.HTTP_OK) { - data.postValue(Resource.error(R.string.generic_not_ok_response, null)); - return true; - } - final String status = responseJson.optString("status"); - if (TextUtils.isEmpty(status) || !status.equals("ok")) { - data.postValue(Resource.error(R.string.generic_not_ok_response, null)); - return true; - } - return false; - } - - private int getItemIndex(final DirectItem item, final List list) { - return Iterables.indexOf(list, i -> i != null && i.getItemId().equals(item.getItemId())); - } - - @Nullable - private DirectItem getItem(final String itemId) { - if (itemId == null) return null; - final List items = this.items.getValue(); - if (items == null) return null; - return items.stream() - .filter(directItem -> directItem.getItemId().equals(itemId)) - .findFirst() - .orElse(null); - } - - private void handleBroadcastReactionRequest(final MutableLiveData> data, - final DirectItem item, - @NonNull final Call request) { - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (!response.isSuccessful()) { - if (response.errorBody() != null) { - handleErrorBody(call, response, data); - return; - } - data.postValue(Resource.error(R.string.generic_failed_request, item)); - return; - } - final DirectThreadBroadcastResponse body = response.body(); - if (body == null) { - data.postValue(Resource.error(R.string.generic_null_response, item)); - } - // otherwise nothing to do? maybe update the timestamp in the emoji? - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - data.postValue(Resource.error(t.getMessage(), item)); - Log.e(TAG, "enqueueRequest: onFailure: ", t); - } - }); - } - - private void stopCurrentRequest() { - if (chatsRequest == null || chatsRequest.isExecuted() || chatsRequest.isCanceled()) { - return; - } - chatsRequest.cancel(); - fetching.postValue(Resource.success(new Object())); - } - - @Nullable - private Long getCurrentUserId(final MutableLiveData> data) { - if (currentUser == null || currentUser.getPk() <= 0) { - data.postValue(Resource.error(R.string.dms_ERROR_INVALID_USER, null)); - return null; - } - return currentUser.getPk(); - } - - public void removeThread() { - final Boolean pendingValue = pending.getValue(); - final boolean threadInPending = pendingValue != null && pendingValue; - inboxManager.removeThread(threadId); - if (threadInPending) { - final Integer totalValue = inboxManager.getPendingRequestsTotal().getValue(); - if (totalValue == null) return; - inboxManager.setPendingRequestsTotal(totalValue - 1); - } - } - - public LiveData> updateTitle(final String newTitle) { - final MutableLiveData> data = new MutableLiveData<>(); - final Call addUsersRequest = service.updateTitle(threadId, newTitle.trim()); - handleDetailsChangeRequest(data, addUsersRequest); - return data; - } - - public LiveData> addMembers(final Set users) { - final MutableLiveData> data = new MutableLiveData<>(); - final Call addUsersRequest = service.addUsers(threadId, - users.stream() - .filter(Objects::nonNull) - .map(User::getPk) - .collect(Collectors.toList())); - handleDetailsChangeRequest(data, addUsersRequest); - return data; - } - - public LiveData> removeMember(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - if (user == null) { - data.postValue(Resource.error("user is null!", null)); - return data; - } - final Call request = service.removeUsers(threadId, Collections.singleton(user.getPk())); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleErrorBody(call, response, data); - return; - } - data.postValue(Resource.success(new Object())); - List activeUsers = users.getValue(); - List leftUsersValue = leftUsers.getValue(); - if (activeUsers == null) { - activeUsers = Collections.emptyList(); - } - if (leftUsersValue == null) { - leftUsersValue = Collections.emptyList(); - } - final List updatedActiveUsers = activeUsers.stream() - .filter(Objects::nonNull) - .filter(u -> u.getPk() != user.getPk()) - .collect(Collectors.toList()); - final ImmutableList.Builder updatedLeftUsersBuilder = ImmutableList.builder().addAll(leftUsersValue); - if (!leftUsersValue.contains(user)) { - updatedLeftUsersBuilder.add(user); - } - final ImmutableList updatedLeftUsers = updatedLeftUsersBuilder.build(); - setThreadUsers(updatedActiveUsers, updatedLeftUsers); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public boolean isAdmin(final User user) { - if (user == null) return false; - final List adminUserIdsValue = adminUserIds.getValue(); - return adminUserIdsValue != null && adminUserIdsValue.contains(user.getPk()); - } - - public LiveData> makeAdmin(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - if (user == null) return data; - if (isAdmin(user)) return data; - final Call request = service.addAdmins(threadId, Collections.singleton(user.getPk())); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleErrorBody(call, response, data); - return; - } - final List currentAdminIds = adminUserIds.getValue(); - final ImmutableList updatedAdminIds = ImmutableList.builder() - .addAll(currentAdminIds != null ? currentAdminIds : Collections.emptyList()) - .add(user.getPk()) - .build(); - final DirectThread currentThread = ThreadManager.this.thread.getValue(); - if (currentThread == null) return; - try { - final DirectThread thread = (DirectThread) currentThread.clone(); - thread.setAdminUserIds(updatedAdminIds); - inboxManager.setThread(threadId, thread); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "onResponse: ", e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> removeAdmin(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - if (user == null) return data; - if (!isAdmin(user)) return data; - final Call request = service.removeAdmins(threadId, Collections.singleton(user.getPk())); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleErrorBody(call, response, data); - return; - } - final List currentAdmins = adminUserIds.getValue(); - if (currentAdmins == null) return; - final List updatedAdminUserIds = currentAdmins.stream() - .filter(Objects::nonNull) - .filter(userId1 -> userId1 != user.getPk()) - .collect(Collectors.toList()); - final DirectThread currentThread = ThreadManager.this.thread.getValue(); - if (currentThread == null) return; - try { - final DirectThread thread = (DirectThread) currentThread.clone(); - thread.setAdminUserIds(updatedAdminUserIds); - inboxManager.setThread(threadId, thread); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "onResponse: ", e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> mute() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Boolean muted = isMuted.getValue(); - if (muted != null && muted) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = service.mute(threadId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleErrorBody(call, response, data); - return; - } - data.postValue(Resource.success(new Object())); - final DirectThread currentThread = ThreadManager.this.thread.getValue(); - if (currentThread == null) return; - try { - final DirectThread thread = (DirectThread) currentThread.clone(); - thread.setMuted(true); - inboxManager.setThread(threadId, thread); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "onResponse: ", e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> unmute() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Boolean muted = isMuted.getValue(); - if (muted != null && !muted) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = service.unmute(threadId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleErrorBody(call, response, data); - return; - } - data.postValue(Resource.success(new Object())); - final DirectThread currentThread = ThreadManager.this.thread.getValue(); - if (currentThread == null) return; - try { - final DirectThread thread = (DirectThread) currentThread.clone(); - thread.setMuted(false); - inboxManager.setThread(threadId, thread); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "onResponse: ", e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> muteMentions() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Boolean mentionsMuted = isMentionsMuted.getValue(); - if (mentionsMuted != null && mentionsMuted) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = service.muteMentions(threadId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleErrorBody(call, response, data); - return; - } - data.postValue(Resource.success(new Object())); - final DirectThread currentThread = ThreadManager.this.thread.getValue(); - if (currentThread == null) return; - try { - final DirectThread thread = (DirectThread) currentThread.clone(); - thread.setMentionsMuted(true); - inboxManager.setThread(threadId, thread); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "onResponse: ", e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> unmuteMentions() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Boolean mentionsMuted = isMentionsMuted.getValue(); - if (mentionsMuted != null && !mentionsMuted) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = service.unmuteMentions(threadId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleErrorBody(call, response, data); - return; - } - data.postValue(Resource.success(new Object())); - final DirectThread currentThread = ThreadManager.this.thread.getValue(); - if (currentThread == null) return; - try { - final DirectThread thread = (DirectThread) currentThread.clone(); - thread.setMentionsMuted(false); - inboxManager.setThread(threadId, thread); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "onResponse: ", e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> blockUser(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - if (user == null) return data; - friendshipService.changeBlock(false, user.getPk(), new ServiceCallback() { - @Override - public void onSuccess(final FriendshipChangeResponse result) { - refreshChats(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> unblockUser(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - if (user == null) return data; - friendshipService.changeBlock(true, user.getPk(), new ServiceCallback() { - @Override - public void onSuccess(final FriendshipChangeResponse result) { - refreshChats(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> restrictUser(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - if (user == null) return data; - friendshipService.toggleRestrict(user.getPk(), true, new ServiceCallback() { - @Override - public void onSuccess(final FriendshipRestrictResponse result) { - refreshChats(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> unRestrictUser(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - if (user == null) return data; - friendshipService.toggleRestrict(user.getPk(), false, new ServiceCallback() { - @Override - public void onSuccess(final FriendshipRestrictResponse result) { - refreshChats(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - public LiveData> approveUsers(final List users) { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Call approveUsersRequest = service - .approveParticipantRequests(threadId, - users.stream() - .filter(Objects::nonNull) - .map(User::getPk) - .collect(Collectors.toList())); - handleDetailsChangeRequest(data, approveUsersRequest, () -> pendingUserApproveDenySuccessAction(users)); - return data; - } - - public LiveData> denyUsers(final List users) { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Call approveUsersRequest = service - .declineParticipantRequests(threadId, - users.stream().map(User::getPk).collect(Collectors.toList())); - handleDetailsChangeRequest(data, approveUsersRequest, () -> pendingUserApproveDenySuccessAction(users)); - return data; - } - - private void pendingUserApproveDenySuccessAction(final List users) { - final DirectThreadParticipantRequestsResponse pendingRequestsValue = pendingRequests.getValue(); - if (pendingRequestsValue == null) return; - final List pendingUsers = pendingRequestsValue.getUsers(); - if (pendingUsers == null || pendingUsers.isEmpty()) return; - final List filtered = pendingUsers.stream() - .filter(o -> !users.contains(o)) - .collect(Collectors.toList()); - try { - final DirectThreadParticipantRequestsResponse clone = (DirectThreadParticipantRequestsResponse) pendingRequestsValue.clone(); - clone.setUsers(filtered); - final int totalParticipantRequests = clone.getTotalParticipantRequests(); - clone.setTotalParticipantRequests(totalParticipantRequests > 0 ? totalParticipantRequests - 1 : 0); - pendingRequests.postValue(clone); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "pendingUserApproveDenySuccessAction: ", e); - } - } - - public LiveData> approvalRequired() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Boolean approvalRequiredToJoin = isApprovalRequiredToJoin.getValue(); - if (approvalRequiredToJoin != null && approvalRequiredToJoin) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = service.approvalRequired(threadId); - handleDetailsChangeRequest(data, request, () -> { - final DirectThread currentThread = ThreadManager.this.thread.getValue(); - if (currentThread == null) return; - try { - final DirectThread thread = (DirectThread) currentThread.clone(); - thread.setApprovalRequiredForNewMembers(true); - inboxManager.setThread(threadId, thread); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "onResponse: ", e); - } - }); - return data; - } - - public LiveData> approvalNotRequired() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Boolean approvalRequiredToJoin = isApprovalRequiredToJoin.getValue(); - if (approvalRequiredToJoin != null && !approvalRequiredToJoin) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = service.approvalNotRequired(threadId); - handleDetailsChangeRequest(data, request, () -> { - final DirectThread currentThread = ThreadManager.this.thread.getValue(); - if (currentThread == null) return; - try { - final DirectThread thread = (DirectThread) currentThread.clone(); - thread.setApprovalRequiredForNewMembers(false); - inboxManager.setThread(threadId, thread); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "onResponse: ", e); - } - }); - return data; - } - - public LiveData> leave() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Call request = service.leave(threadId); - handleDetailsChangeRequest(data, request); - return data; - } - - public LiveData> end() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Call request = service.end(threadId); - handleDetailsChangeRequest(data, request, () -> { - final DirectThread currentThread = ThreadManager.this.thread.getValue(); - if (currentThread == null) return; - try { - final DirectThread thread = (DirectThread) currentThread.clone(); - thread.setInputMode(1); - inboxManager.setThread(threadId, thread); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "onResponse: ", e); - } - }); - return data; - } - - private void handleDetailsChangeRequest(final MutableLiveData> data, - final Call request) { - handleDetailsChangeRequest(data, request, null); - } - - private void handleDetailsChangeRequest(final MutableLiveData> data, - final Call request, - @Nullable final OnSuccessAction action) { - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (!response.isSuccessful()) { - handleErrorBody(call, response, data); - return; - } - final DirectThreadDetailsChangeResponse changeResponse = response.body(); - if (changeResponse == null) { - data.postValue(Resource.error(R.string.generic_null_response, null)); - return; - } - data.postValue(Resource.success(new Object())); - final DirectThread thread = changeResponse.getThread(); - if (thread != null) { - setThread(thread, true); - } - if (action != null) { - action.onSuccess(); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - } - - public LiveData getInviter() { - return inviter; - } - - public LiveData> markAsSeen(@NonNull final DirectItem directItem) { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Call request = service.markAsSeen(threadId, directItem); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (currentUser == null) return; - if (!response.isSuccessful()) { - handleErrorBody(call, response, data); - return; - } - final DirectItemSeenResponse seenResponse = response.body(); - if (seenResponse == null) { - data.postValue(Resource.error(R.string.generic_null_response, null)); - return; - } - inboxManager.fetchUnseenCount(); - final DirectItemSeenResponsePayload payload = seenResponse.getPayload(); - if (payload == null) return; - final String timestamp = payload.getTimestamp(); - final DirectThread thread = ThreadManager.this.thread.getValue(); - if (thread == null) return; - Map lastSeenAt = thread.getLastSeenAt(); - lastSeenAt = lastSeenAt == null ? new HashMap<>() : new HashMap<>(lastSeenAt); - lastSeenAt.put(currentUser.getPk(), new DirectThreadLastSeenAt(timestamp, directItem.getItemId())); - thread.setLastSeenAt(lastSeenAt); - setThread(thread, true); - data.postValue(Resource.success(new Object())); - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - private interface OnSuccessAction { - void onSuccess(); - } -} diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt b/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt new file mode 100644 index 00000000..c26ba261 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt @@ -0,0 +1,1675 @@ +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.repositories.requests.UploadFinishOptions +import awais.instagrabber.repositories.requests.VideoOptions +import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds +import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds.Companion.of +import awais.instagrabber.repositories.responses.FriendshipChangeResponse +import awais.instagrabber.repositories.responses.FriendshipRestrictResponse +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.OnMediaUploadCompleteListener +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.webservices.DirectMessagesService +import awais.instagrabber.webservices.FriendshipService +import awais.instagrabber.webservices.MediaService +import awais.instagrabber.webservices.ServiceCallback +import com.google.common.collect.ImmutableList +import com.google.common.collect.Iterables +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.File +import java.io.IOException +import java.net.HttpURLConnection +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.stream.Collectors + +class ThreadManager private constructor( + private val threadId: String, + pending: Boolean, + currentUser: User, + contentResolver: ContentResolver, + viewerId: Long, + csrfToken: String, + deviceUuid: String, +) { + private val fetching = MutableLiveData>() + private val replyToItem = MutableLiveData() + private val pendingRequests = MutableLiveData(null) + private val inboxManager: InboxManager = if (pending) DirectMessagesManager.pendingInboxManager else DirectMessagesManager.inboxManager + private val viewerId: Long + private val threadIdOrUserIds: ThreadIdOrUserIds = of(threadId) + private val currentUser: User? + private val contentResolver: ContentResolver + private val service: DirectMessagesService + private val mediaService: MediaService + private val friendshipService: FriendshipService + + 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.asSequence().filterNotNull().firstOrNull() + 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 isFetching(): LiveData> { + return fetching + } + + fun getReplyToItem(): LiveData { + return replyToItem + } + + fun getPendingRequests(): LiveData { + return pendingRequests + } + + fun fetchChats() { + val fetchingValue = fetching.value + if (fetchingValue != null && fetchingValue.status === Resource.Status.LOADING || !hasOlder) return + fetching.postValue(loading(null)) + chatsRequest = service.fetchThread(threadId, cursor) + chatsRequest?.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val feedResponse = response.body() + if (feedResponse == null) { + fetching.postValue(error(R.string.generic_null_response, null)) + Log.e(TAG, "onResponse: response was null!") + return + } + if (feedResponse.status != null && feedResponse.status != "ok") { + fetching.postValue(error(R.string.generic_not_ok_response, null)) + return + } + val thread = feedResponse.thread + if (thread == null) { + fetching.postValue(error("thread is null!", null)) + return + } + setThread(thread) + fetching.postValue(success(Any())) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "Failed fetching dm chats", t) + fetching.postValue(error(t.message, null)) + hasOlder = false + } + }) + if (cursor == null) { + fetchPendingRequests() + } + } + + fun fetchPendingRequests() { + val isGroup = isGroup.value + if (isGroup == null || !isGroup) return + val request = service.participantRequests(threadId, 1, null) + request.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + if (response.errorBody() != null) { + try { + val string = response.errorBody()?.string() ?: "" + val msg = String.format(Locale.US, + "onResponse: url: %s, responseCode: %d, errorBody: %s", + call.request().url().toString(), + response.code(), + string) + Log.e(TAG, msg) + } catch (e: IOException) { + Log.e(TAG, "onResponse: ", e) + } + return + } + Log.e(TAG, "onResponse: request was not successful and response error body was null") + return + } + val body = response.body() + if (body == null) { + Log.e(TAG, "onResponse: response body was null") + return + } + pendingRequests.postValue(body) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + } + }) + } + + 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): 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 + val request = service.broadcastText( + clientContext, + threadIdOrUserIds, + text, + repliedToItemId, + repliedToClientContext + ) + enqueueRequest(request, data, directItem) + return data + } + + fun sendUri(entry: MediaController.MediaEntry): LiveData> { + val data = MutableLiveData>() + val uri = Uri.fromFile(File(entry.path)) + if (!entry.isVideo) { + sendPhoto(data, uri, entry.width, entry.height) + return data + } + sendVideo(data, uri, entry.size, entry.duration, entry.width, entry.height) + return data + } + + fun sendUri(uri: Uri): 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) + return data + } + if (mimeType != null && mimeType.startsWith("video")) { + sendVideo(data, uri) + } + return data + } + + fun sendAnimatedMedia(giphyGif: GiphyGif): 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)) + val request = service.broadcastAnimatedMedia( + clientContext, + threadIdOrUserIds, + giphyGif + ) + enqueueRequest(request, data, directItem) + return data + } + + fun sendVoice( + data: MutableLiveData>, + uri: Uri, + waveform: List, + samplingFreq: Int, + duration: Long, + byteLength: Long, + ) { + 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) + uploadVideo(uri, contentResolver, uploadDmVoiceOptions, object : OnMediaUploadCompleteListener { + override fun onUploadComplete(response: MediaUploadResponse) { + // Log.d(TAG, "onUploadComplete: " + response); + if (handleInvalidResponse(data, response)) return + val uploadFinishOptions = UploadFinishOptions( + uploadDmVoiceOptions.uploadId, + "4", + null + ) + val uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions) + uploadFinishRequest.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + val request = service.broadcastVoice( + clientContext, + threadIdOrUserIds, + uploadDmVoiceOptions.uploadId, + waveform, + samplingFreq + ) + enqueueRequest(request, data, directItem) + return + } + if (response.errorBody() != null) { + handleErrorBody(call, response, data) + return + } + data.postValue(error("uploadFinishRequest was not successful and response error body was null", directItem)) + Log.e(TAG, "uploadFinishRequest was not successful and response error body was null") + } + + override fun onFailure(call: Call, t: Throwable) { + data.postValue(error(t.message, directItem)) + Log.e(TAG, "onFailure: ", t) + } + }) + } + + override fun onFailure(t: Throwable) { + data.postValue(error(t.message, directItem)) + Log.e(TAG, "onFailure: ", t) + } + }) + } + + fun sendReaction( + item: DirectItem, + emoji: Emoji, + ): 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 + } + val request = service.broadcastReaction( + clientContext, + threadIdOrUserIds, + itemId, + emojiUnicode, + false + ) + handleBroadcastReactionRequest(data, item, request) + return data + } + + fun sendDeleteReaction(itemId: String): 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 + } + val request = service.broadcastReaction(clientContext, threadIdOrUserIds, itemId1, null, true) + handleBroadcastReactionRequest(data, item, request) + return data + } + + fun unsend(item: DirectItem): LiveData> { + val data = MutableLiveData>() + val index = removeItem(item) + val itemId = item.itemId + if (itemId == null) { + data.postValue(error("itemId is null", null)) + return data + } + val request = service.deleteItem(threadId, itemId) + request.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + // Log.d(TAG, "onResponse: " + response.body()); + return + } + // add the item back if unsuccessful + addItems(index, listOf(item)) + if (response.errorBody() != null) { + handleErrorBody(call, response, data) + return + } + data.postValue(error(R.string.generic_failed_request, item)) + } + + override fun onFailure(call: Call, t: Throwable) { + data.postValue(error(t.message, item)) + Log.e(TAG, "enqueueRequest: onFailure: ", t) + } + }) + return data + } + + fun forward(recipients: Set, itemToForward: DirectItem) { + for (recipient in recipients) { + forward(recipient, itemToForward) + } + } + + fun forward(recipient: RankedRecipient, itemToForward: DirectItem) { + if (recipient.thread == null && recipient.user != null) { + // create thread and forward + DirectMessagesManager.createThread(recipient.user.pk) { forward(it, itemToForward) } + } + if (recipient.thread != null) { + // just forward + val thread = recipient.thread + forward(thread, itemToForward) + } + } + + fun setReplyToItem(item: DirectItem?) { + // Log.d(TAG, "setReplyToItem: " + item); + replyToItem.postValue(item) + } + + private fun forward(thread: DirectThread, itemToForward: DirectItem): 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 = itemType.getName() + 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 + } + val request = service.forward(thread.threadId, + itemTypeName, + threadId, + forwardItemId) + request.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (response.isSuccessful) { + data.postValue(success(Any())) + return + } + val errorBody = response.errorBody() + if (errorBody != null) { + try { + val string = errorBody.string() + val msg = String.format(Locale.US, + "onResponse: url: %s, responseCode: %d, errorBody: %s", + call.request().url().toString(), + response.code(), + string) + Log.e(TAG, msg) + data.postValue(error(msg, null)) + } catch (e: IOException) { + Log.e(TAG, "onResponse: ", e) + data.postValue(error(e.message, null)) + } + return + } + val msg = "onResponse: request was not successful and response error body was null" + Log.e(TAG, msg) + data.postValue(error(msg, null)) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun acceptRequest(): LiveData> { + val data = MutableLiveData>() + val request = service.approveRequest(threadId) + request.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + try { + val string = response.errorBody()?.string() ?: "" + val msg = String.format(Locale.US, + "onResponse: url: %s, responseCode: %d, errorBody: %s", + call.request().url().toString(), + response.code(), + string) + Log.e(TAG, msg) + data.postValue(error(msg, null)) + return + } catch (e: IOException) { + Log.e(TAG, "onResponse: ", e) + } + return + } + data.postValue(success(Any())) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun declineRequest(): LiveData> { + val data = MutableLiveData>() + val request = service.declineRequest(threadId) + request.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + try { + val string = response.errorBody()?.string() ?: "" + val msg = String.format(Locale.US, + "onResponse: url: %s, responseCode: %d, errorBody: %s", + call.request().url().toString(), + response.code(), + string) + Log.e(TAG, msg) + data.postValue(error(msg, null)) + return + } catch (e: IOException) { + Log.e(TAG, "onResponse: ", e) + } + return + } + data.postValue(success(Any())) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun refreshChats() { + val isFetching = fetching.value + if (isFetching != null && isFetching.status === Resource.Status.LOADING) { + stopCurrentRequest() + } + cursor = null + hasOlder = true + fetchChats() + } + + private fun sendPhoto( + data: MutableLiveData>, + uri: Uri, + ) { + 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) + } 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, + ) { + val userId = getCurrentUserId(data) ?: return + val clientContext = UUID.randomUUID().toString() + val directItem = createImageOrVideo(userId, clientContext, uri, width, height, false) + directItem.isPending = true + addItems(0, listOf(directItem)) + data.postValue(loading(directItem)) + uploadPhoto(uri, contentResolver, object : OnMediaUploadCompleteListener { + override fun onUploadComplete(response: MediaUploadResponse) { + if (handleInvalidResponse(data, response)) return + val response1 = response.response ?: return + val uploadId = response1.optString("upload_id") + val request = service.broadcastPhoto(clientContext, threadIdOrUserIds, uploadId) + enqueueRequest(request, data, directItem) + } + + override fun onFailure(t: Throwable) { + data.postValue(error(t.message, directItem)) + Log.e(TAG, "onFailure: ", t) + } + }) + } + + private fun sendVideo( + data: MutableLiveData>, + uri: Uri, + ) { + 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) + } + + 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, + ) { + 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) + uploadVideo(uri, contentResolver, uploadDmVideoOptions, object : OnMediaUploadCompleteListener { + override fun onUploadComplete(response: MediaUploadResponse) { + // Log.d(TAG, "onUploadComplete: " + response); + if (handleInvalidResponse(data, response)) return + val uploadFinishOptions = UploadFinishOptions( + uploadDmVideoOptions.uploadId, + "2", + VideoOptions(duration / 1000f, emptyList(), 0, false) + ) + val uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions) + uploadFinishRequest.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + val request = service.broadcastVideo( + clientContext, + threadIdOrUserIds, + uploadDmVideoOptions.uploadId, + "", + true + ) + enqueueRequest(request, data, directItem) + return + } + if (response.errorBody() != null) { + handleErrorBody(call, response, data) + return + } + data.postValue(error("uploadFinishRequest was not successful and response error body was null", directItem)) + Log.e(TAG, "uploadFinishRequest was not successful and response error body was null") + } + + override fun onFailure(call: Call, t: Throwable) { + data.postValue(error(t.message, directItem)) + Log.e(TAG, "onFailure: ", t) + } + }) + } + + override fun onFailure(t: Throwable) { + data.postValue(error(t.message, directItem)) + Log.e(TAG, "onFailure: ", t) + } + }) + } + + private fun enqueueRequest( + request: Call, + data: MutableLiveData>, + directItem: DirectItem, + ) { + request.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (response.isSuccessful) { + val broadcastResponse = response.body() + if (broadcastResponse == null) { + data.postValue(error(R.string.generic_null_response, directItem)) + Log.e(TAG, "enqueueRequest: onResponse: response body is null") + return + } + val payloadClientContext: String? + val timestamp: Long + val itemId: String? + val payload = broadcastResponse.payload + if (payload == null) { + val messageMetadata = broadcastResponse.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)) + return + } + if (response.errorBody() != null) { + handleErrorBody(call, response, data) + } + data.postValue(error(R.string.generic_failed_request, directItem)) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + data.postValue(error(t.message, directItem)) + Log.e(TAG, "enqueueRequest: onFailure: ", t) + } + }) + } + + 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 handleErrorBody( + call: Call<*>, + response: Response<*>, + data: MutableLiveData>?, + ) { + try { + val string = response.errorBody()?.string() ?: "" + val msg = String.format(Locale.US, + "onResponse: url: %s, responseCode: %d, errorBody: %s", + call.request().url().toString(), + response.code(), + string) + data?.postValue(error(msg, null)) + Log.e(TAG, msg) + } catch (e: IOException) { + data?.postValue(error(e.message, null)) + Log.e(TAG, "onResponse: ", 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 handleBroadcastReactionRequest( + data: MutableLiveData>, + item: DirectItem, + request: Call, + ) { + request.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + if (response.errorBody() != null) { + handleErrorBody(call, response, data) + return + } + data.postValue(error(R.string.generic_failed_request, item)) + return + } + val body = response.body() + if (body == null) { + data.postValue(error(R.string.generic_null_response, item)) + } + // otherwise nothing to do? maybe update the timestamp in the emoji? + } + + override fun onFailure(call: Call, t: Throwable) { + data.postValue(error(t.message, item)) + Log.e(TAG, "enqueueRequest: onFailure: ", t) + } + }) + } + + 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): LiveData> { + val data = MutableLiveData>() + val addUsersRequest = service.updateTitle(threadId, newTitle.trim { it <= ' ' }) + handleDetailsChangeRequest(data, addUsersRequest) + return data + } + + fun addMembers(users: Set): LiveData> { + val data = MutableLiveData>() + val addUsersRequest = service.addUsers( + threadId, + users.stream() + .filter { obj: User? -> Objects.nonNull(obj) } + .map { obj: User -> obj.pk } + .collect(Collectors.toList()) + ) + handleDetailsChangeRequest(data, addUsersRequest) + return data + } + + fun removeMember(user: User): LiveData> { + val data = MutableLiveData>() + val request = service.removeUsers(threadId, setOf(user.pk)) + request.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + handleErrorBody(call, response, data) + return + } + 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.stream() + .filter { obj: User? -> Objects.nonNull(obj) } + .filter { u: User -> u.pk != user.pk } + .collect(Collectors.toList()) + val updatedLeftUsersBuilder = ImmutableList.builder().addAll(leftUsersValue) + if (!leftUsersValue.contains(user)) { + updatedLeftUsersBuilder.add(user) + } + val updatedLeftUsers = updatedLeftUsersBuilder.build() + setThreadUsers(updatedActiveUsers, updatedLeftUsers) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun isAdmin(user: User): Boolean { + val adminUserIdsValue = adminUserIds.value + return adminUserIdsValue != null && adminUserIdsValue.contains(user.pk) + } + + fun makeAdmin(user: User): LiveData> { + val data = MutableLiveData>() + if (isAdmin(user)) return data + val request = service.addAdmins(threadId, setOf(user.pk)) + request.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + handleErrorBody(call, response, data) + return + } + val currentAdminIds = adminUserIds.value + val updatedAdminIds = ImmutableList.builder() + .addAll(currentAdminIds ?: emptyList()) + .add(user.pk) + .build() + val currentThread = thread.value ?: return + try { + val thread = currentThread.clone() as DirectThread + thread.adminUserIds = updatedAdminIds + inboxManager.setThread(threadId, thread) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "onResponse: ", e) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun removeAdmin(user: User): LiveData> { + val data = MutableLiveData>() + if (!isAdmin(user)) return data + val request = service.removeAdmins(threadId, setOf(user.pk)) + request.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + handleErrorBody(call, response, data) + return + } + val currentAdmins = adminUserIds.value ?: return + val updatedAdminUserIds = currentAdmins.stream() + .filter { obj: Long? -> Objects.nonNull(obj) } + .filter { userId1: Long -> userId1 != user.pk } + .collect(Collectors.toList()) + val currentThread = thread.value ?: return + try { + val thread = currentThread.clone() as DirectThread + thread.adminUserIds = updatedAdminUserIds + inboxManager.setThread(threadId, thread) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "onResponse: ", e) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun mute(): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val muted = isMuted.value + if (muted != null && muted) { + data.postValue(success(Any())) + return data + } + val request = service.mute(threadId) + request.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + handleErrorBody(call, response, data) + return + } + data.postValue(success(Any())) + val currentThread = thread.value ?: return + try { + val thread = currentThread.clone() as DirectThread + thread.muted = true + inboxManager.setThread(threadId, thread) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "onResponse: ", e) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun unmute(): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val muted = isMuted.value + if (muted != null && !muted) { + data.postValue(success(Any())) + return data + } + val request = service.unmute(threadId) + request.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + handleErrorBody(call, response, data) + return + } + data.postValue(success(Any())) + val currentThread = thread.value ?: return + try { + val thread = currentThread.clone() as DirectThread + thread.muted = false + inboxManager.setThread(threadId, thread) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "onResponse: ", e) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun muteMentions(): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val mentionsMuted = isMentionsMuted.value + if (mentionsMuted != null && mentionsMuted) { + data.postValue(success(Any())) + return data + } + val request = service.muteMentions(threadId) + request.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + handleErrorBody(call, response, data) + return + } + data.postValue(success(Any())) + val currentThread = thread.value ?: return + try { + val thread = currentThread.clone() as DirectThread + thread.mentionsMuted = true + inboxManager.setThread(threadId, thread) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "onResponse: ", e) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun unmuteMentions(): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val mentionsMuted = isMentionsMuted.value + if (mentionsMuted != null && !mentionsMuted) { + data.postValue(success(Any())) + return data + } + val request = service.unmuteMentions(threadId) + request.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + handleErrorBody(call, response, data) + return + } + data.postValue(success(Any())) + val currentThread = thread.value ?: return + try { + val thread = currentThread.clone() as DirectThread + thread.mentionsMuted = false + inboxManager.setThread(threadId, thread) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "onResponse: ", e) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun blockUser(user: User): LiveData> { + val data = MutableLiveData>() + friendshipService.changeBlock(false, user.pk, object : ServiceCallback { + override fun onSuccess(result: FriendshipChangeResponse?) { + refreshChats() + } + + override fun onFailure(t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun unblockUser(user: User): LiveData> { + val data = MutableLiveData>() + friendshipService.changeBlock(true, user.pk, object : ServiceCallback { + override fun onSuccess(result: FriendshipChangeResponse?) { + refreshChats() + } + + override fun onFailure(t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun restrictUser(user: User): LiveData> { + val data = MutableLiveData>() + friendshipService.toggleRestrict(user.pk, true, object : ServiceCallback { + override fun onSuccess(result: FriendshipRestrictResponse?) { + refreshChats() + } + + override fun onFailure(t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun unRestrictUser(user: User): LiveData> { + val data = MutableLiveData>() + friendshipService.toggleRestrict(user.pk, false, object : ServiceCallback { + override fun onSuccess(result: FriendshipRestrictResponse?) { + refreshChats() + } + + override fun onFailure(t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + fun approveUsers(users: List): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val approveUsersRequest = service.approveParticipantRequests( + threadId, + users.stream() + .filter { obj: User? -> Objects.nonNull(obj) } + .map { obj: User -> obj.pk } + .collect(Collectors.toList()) + ) + handleDetailsChangeRequest(data, approveUsersRequest, object : OnSuccessAction { + override fun onSuccess() { + pendingUserApproveDenySuccessAction(users) + } + }) + return data + } + + fun denyUsers(users: List): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val approveUsersRequest = service.declineParticipantRequests( + threadId, + users.stream() + .map { obj: User -> obj.pk } + .collect(Collectors.toList()) + ) + handleDetailsChangeRequest(data, approveUsersRequest, object : OnSuccessAction { + override fun onSuccess() { + pendingUserApproveDenySuccessAction(users) + } + }) + 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.stream() + .filter { o: User -> !users.contains(o) } + .collect(Collectors.toList()) + 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(): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val approvalRequiredToJoin = isApprovalRequiredToJoin.value + if (approvalRequiredToJoin != null && approvalRequiredToJoin) { + data.postValue(success(Any())) + return data + } + val request = service.approvalRequired(threadId) + handleDetailsChangeRequest(data, request, object : OnSuccessAction { + override fun onSuccess() { + val currentThread = thread.value ?: return + try { + val thread = currentThread.clone() as DirectThread + thread.approvalRequiredForNewMembers = true + inboxManager.setThread(threadId, thread) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "onResponse: ", e) + } + } + }) + return data + } + + fun approvalNotRequired(): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val approvalRequiredToJoin = isApprovalRequiredToJoin.value + if (approvalRequiredToJoin != null && !approvalRequiredToJoin) { + data.postValue(success(Any())) + return data + } + val request = service.approvalNotRequired(threadId) + handleDetailsChangeRequest(data, request, object : OnSuccessAction { + override fun onSuccess() { + val currentThread = thread.value ?: return + try { + val thread = currentThread.clone() as DirectThread + thread.approvalRequiredForNewMembers = false + inboxManager.setThread(threadId, thread) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "onResponse: ", e) + } + } + }) + return data + } + + fun leave(): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val request = service.leave(threadId) + handleDetailsChangeRequest(data, request) + return data + } + + fun end(): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val request = service.end(threadId) + handleDetailsChangeRequest(data, request, object : OnSuccessAction { + override fun onSuccess() { + val currentThread = thread.value ?: return + try { + val thread = currentThread.clone() as DirectThread + thread.inputMode = 1 + inboxManager.setThread(threadId, thread) + } catch (e: CloneNotSupportedException) { + Log.e(TAG, "onResponse: ", e) + } + } + }) + return data + } + + private fun handleDetailsChangeRequest( + data: MutableLiveData>, + request: Call, + action: OnSuccessAction? = null, + ) { + request.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + handleErrorBody(call, response, data) + return + } + val changeResponse = response.body() + if (changeResponse == null) { + data.postValue(error(R.string.generic_null_response, null)) + return + } + data.postValue(success(Any())) + val thread = changeResponse.thread + if (thread != null) { + setThread(thread, true) + } + action?.onSuccess() + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + } + + fun markAsSeen(directItem: DirectItem): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val request = service.markAsSeen(threadId, directItem) + if (request == null) { + data.postValue(error("request was null", null)) + return data + } + request.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (currentUser == null) return + if (!response.isSuccessful) { + handleErrorBody(call, response, data) + return + } + val seenResponse = response.body() + if (seenResponse == null) { + data.postValue(error(R.string.generic_null_response, null)) + return + } + inboxManager.fetchUnseenCount() + val payload = seenResponse.payload ?: return + val timestamp = payload.timestamp + val thread = thread.value ?: return + 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())) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + Log.e(TAG, "onFailure: ", t) + data.postValue(error(t.message, null)) + } + }) + return data + } + + private interface OnSuccessAction { + fun onSuccess() + } + + companion object { + private val TAG = ThreadManager::class.java.simpleName + private val LOCK = Any() + private val INSTANCE_MAP: MutableMap = ConcurrentHashMap() + + @JvmStatic + fun getInstance( + threadId: String, + pending: Boolean, + currentUser: User, + contentResolver: ContentResolver, + viewerId: Long, + csrfToken: String, + deviceUuid: String, + ): ThreadManager { + var instance = INSTANCE_MAP[threadId] + if (instance == null) { + synchronized(LOCK) { + instance = INSTANCE_MAP[threadId] + if (instance == null) { + instance = ThreadManager(threadId, pending, currentUser, contentResolver, viewerId, csrfToken, deviceUuid) + INSTANCE_MAP[threadId] = instance!! + } + } + } + return instance!! + } + } + + init { + this.currentUser = currentUser + this.contentResolver = contentResolver + this.viewerId = viewerId + service = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid) + mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId) + friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, viewerId) + // fetchChats(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/services/DMSyncService.java b/app/src/main/java/awais/instagrabber/services/DMSyncService.java index a5df0cd5..279effed 100644 --- a/app/src/main/java/awais/instagrabber/services/DMSyncService.java +++ b/app/src/main/java/awais/instagrabber/services/DMSyncService.java @@ -59,7 +59,7 @@ public class DMSyncService extends LifecycleService { super.onCreate(); startForeground(Constants.DM_CHECK_NOTIFICATION_ID, buildForegroundNotification()); Log.d(TAG, "onCreate: Service created"); - final DirectMessagesManager directMessagesManager = DirectMessagesManager.getInstance(); + final DirectMessagesManager directMessagesManager = DirectMessagesManager.INSTANCE; inboxManager = directMessagesManager.getInboxManager(); dmLastNotifiedRepository = DMLastNotifiedRepository.getInstance(DMLastNotifiedDataSource.getInstance(getApplicationContext())); } diff --git a/app/src/main/java/awais/instagrabber/utils/MediaUploader.java b/app/src/main/java/awais/instagrabber/utils/MediaUploader.java deleted file mode 100644 index 3045e7e5..00000000 --- a/app/src/main/java/awais/instagrabber/utils/MediaUploader.java +++ /dev/null @@ -1,193 +0,0 @@ -package awais.instagrabber.utils; - -import android.content.ContentResolver; -import android.graphics.Bitmap; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.json.JSONObject; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; - -import awais.instagrabber.models.UploadPhotoOptions; -import awais.instagrabber.models.UploadVideoOptions; -import awais.instagrabber.webservices.interceptors.AddCookiesInterceptor; -import okhttp3.Call; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import okio.BufferedSink; -import okio.Okio; -import okio.Source; - -public final class MediaUploader { - private static final String TAG = MediaUploader.class.getSimpleName(); - private static final String HOST = "https://i.instagram.com"; - private static final AppExecutors appExecutors = AppExecutors.INSTANCE; - - public static void uploadPhoto(@NonNull final Uri uri, - @NonNull final ContentResolver contentResolver, - @NonNull final OnMediaUploadCompleteListener listener) { - BitmapUtils.loadBitmap(contentResolver, uri, 1000, false, new BitmapUtils.ThumbnailLoadCallback() { - @Override - public void onLoad(@Nullable final Bitmap bitmap, final int width, final int height) { - if (bitmap == null) { - listener.onFailure(new RuntimeException("Bitmap result was null")); - return; - } - uploadPhoto(bitmap, listener); - } - - @Override - public void onFailure(@NonNull final Throwable t) { - listener.onFailure(t); - } - }); - } - - private static void uploadPhoto(@NonNull final Bitmap bitmap, - @NonNull final OnMediaUploadCompleteListener listener) { - appExecutors.getTasksThread().submit(() -> { - final File file; - final long byteLength; - try { - file = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null); - byteLength = file.length(); - } catch (Exception e) { - listener.onFailure(e); - return; - } - final UploadPhotoOptions options = MediaUploadHelper.createUploadPhotoOptions(byteLength); - final Map headers = MediaUploadHelper.getUploadPhotoHeaders(options); - final String url = HOST + "/rupload_igphoto/" + options.getName() + "/"; - appExecutors.getNetworkIO().execute(() -> { - try (FileInputStream input = new FileInputStream(file)) { - upload(input, url, headers, listener); - } catch (IOException e) { - listener.onFailure(e); - } finally { - //noinspection ResultOfMethodCallIgnored - file.delete(); - } - }); - }); - } - - public static void uploadVideo(final Uri uri, - final ContentResolver contentResolver, - final UploadVideoOptions options, - final OnMediaUploadCompleteListener listener) { - appExecutors.getTasksThread().submit(() -> { - final Map headers = MediaUploadHelper.getUploadVideoHeaders(options); - final String url = HOST + "/rupload_igvideo/" + options.getName() + "/"; - appExecutors.getNetworkIO().execute(() -> { - try (InputStream input = contentResolver.openInputStream(uri)) { - if (input == null) { - listener.onFailure(new RuntimeException("InputStream was null")); - return; - } - upload(input, url, headers, listener); - } catch (IOException e) { - listener.onFailure(e); - } - }); - }); - } - - private static void upload(@NonNull final InputStream input, - @NonNull final String url, - @NonNull final Map headers, - @NonNull final OnMediaUploadCompleteListener listener) { - try { - final OkHttpClient client = new OkHttpClient.Builder() - // .addInterceptor(new LoggingInterceptor()) - .addInterceptor(new AddCookiesInterceptor()) - .followRedirects(false) - .followSslRedirects(false) - .build(); - final Request request = new Request.Builder() - .headers(Headers.of(headers)) - .url(url) - .post(create(MediaType.parse("application/octet-stream"), input)) - .build(); - final Call call = client.newCall(request); - final Response response = call.execute(); - final ResponseBody body = response.body(); - if (!response.isSuccessful()) { - listener.onFailure(new IOException("Unexpected code " + response + (body != null ? ": " + body.string() : ""))); - return; - } - listener.onUploadComplete(new MediaUploadResponse(response.code(), body != null ? new JSONObject(body.string()) : null)); - } catch (Exception e) { - listener.onFailure(e); - } - } - - public interface OnMediaUploadCompleteListener { - void onUploadComplete(MediaUploadResponse response); - - void onFailure(Throwable t); - } - - private static RequestBody create(final MediaType mediaType, final InputStream inputStream) { - return new RequestBody() { - @Override - public MediaType contentType() { - return mediaType; - } - - @Override - public long contentLength() { - try { - return inputStream.available(); - } catch (IOException e) { - return 0; - } - } - - @Override - public void writeTo(@NonNull BufferedSink sink) throws IOException { - try (Source source = Okio.source(inputStream)) { - sink.writeAll(source); - } - } - }; - } - - public static class MediaUploadResponse { - private final int responseCode; - private final JSONObject response; - - public MediaUploadResponse(int responseCode, JSONObject response) { - this.responseCode = responseCode; - this.response = response; - } - - public int getResponseCode() { - return responseCode; - } - - public JSONObject getResponse() { - return response; - } - - @NonNull - @Override - public String toString() { - return "MediaUploadResponse{" + - "responseCode=" + responseCode + - ", response=" + response + - '}'; - } - } -} diff --git a/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt new file mode 100644 index 00000000..e9f7f5a8 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt @@ -0,0 +1,155 @@ +package awais.instagrabber.utils + +import android.content.ContentResolver +import android.graphics.Bitmap +import android.net.Uri +import awais.instagrabber.models.UploadVideoOptions +import awais.instagrabber.utils.BitmapUtils.ThumbnailLoadCallback +import awais.instagrabber.webservices.interceptors.AddCookiesInterceptor +import okhttp3.* +import okio.BufferedSink +import okio.source +import org.json.JSONObject +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream + +object MediaUploader { + private const val HOST = "https://i.instagram.com" + private val appExecutors = AppExecutors + + fun uploadPhoto( + uri: Uri, + contentResolver: ContentResolver, + listener: OnMediaUploadCompleteListener, + ) { + BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false, object : ThumbnailLoadCallback { + override fun onLoad(bitmap: Bitmap?, width: Int, height: Int) { + if (bitmap == null) { + listener.onFailure(RuntimeException("Bitmap result was null")) + return + } + uploadPhoto(bitmap, listener) + } + + override fun onFailure(t: Throwable) { + listener.onFailure(t) + } + }) + } + + private fun uploadPhoto( + bitmap: Bitmap, + listener: OnMediaUploadCompleteListener, + ) { + appExecutors.tasksThread.submit { + val file: File + val byteLength: Long + try { + file = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null) + byteLength = file.length() + } catch (e: Exception) { + listener.onFailure(e) + return@submit + } + val options = createUploadPhotoOptions(byteLength) + val headers = getUploadPhotoHeaders(options) + val url = HOST + "/rupload_igphoto/" + options.name + "/" + appExecutors.networkIO.execute { + try { + FileInputStream(file).use { input -> upload(input, url, headers, listener) } + } catch (e: IOException) { + listener.onFailure(e) + } finally { + file.delete() + } + } + } + } + + @JvmStatic + fun uploadVideo( + uri: Uri, + contentResolver: ContentResolver, + options: UploadVideoOptions, + listener: OnMediaUploadCompleteListener, + ) { + appExecutors.tasksThread.submit { + val headers = getUploadVideoHeaders(options) + val url = HOST + "/rupload_igvideo/" + options.name + "/" + appExecutors.networkIO.execute { + try { + contentResolver.openInputStream(uri).use { input -> + if (input == null) { + listener.onFailure(RuntimeException("InputStream was null")) + return@execute + } + upload(input, url, headers, listener) + } + } catch (e: IOException) { + listener.onFailure(e) + } + } + } + } + + private fun upload( + input: InputStream, + url: String, + headers: Map, + listener: OnMediaUploadCompleteListener, + ) { + try { + val client = OkHttpClient.Builder() + // .addInterceptor(new LoggingInterceptor()) + .addInterceptor(AddCookiesInterceptor()) + .followRedirects(false) + .followSslRedirects(false) + .build() + val request = Request.Builder() + .headers(Headers.of(headers)) + .url(url) + .post(create(MediaType.parse("application/octet-stream"), input)) + .build() + val call = client.newCall(request) + val response = call.execute() + val body = response.body() + if (!response.isSuccessful) { + listener.onFailure(IOException("Unexpected code " + response + if (body != null) ": " + body.string() else "")) + return + } + listener.onUploadComplete(MediaUploadResponse(response.code(), if (body != null) JSONObject(body.string()) else null)) + } catch (e: Exception) { + listener.onFailure(e) + } + } + + private fun create(mediaType: MediaType?, inputStream: InputStream): RequestBody { + return object : RequestBody() { + override fun contentType(): MediaType? { + return mediaType + } + + override fun contentLength(): Long { + return try { + inputStream.available().toLong() + } catch (e: IOException) { + 0 + } + } + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + inputStream.source().use { sink.writeAll(it) } + } + } + } + + interface OnMediaUploadCompleteListener { + fun onUploadComplete(response: MediaUploadResponse) + fun onFailure(t: Throwable) + } + + data class MediaUploadResponse(val responseCode: Int, val response: JSONObject?) +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java index f003f4b0..ca1906f3 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java @@ -18,7 +18,7 @@ public class DirectInboxViewModel extends ViewModel { private final InboxManager inboxManager; public DirectInboxViewModel() { - final DirectMessagesManager messagesManager = DirectMessagesManager.getInstance(); + final DirectMessagesManager messagesManager = DirectMessagesManager.INSTANCE; inboxManager = messagesManager.getInboxManager(); } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java index 85d28e7d..bf29e41e 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java @@ -18,7 +18,7 @@ public class DirectPendingInboxViewModel extends ViewModel { private final InboxManager inboxManager; public DirectPendingInboxViewModel() { - inboxManager = DirectMessagesManager.getInstance().getPendingInboxManager(); + inboxManager = DirectMessagesManager.INSTANCE.getPendingInboxManager(); inboxManager.fetchInbox(); } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java index 6f292bc2..ea6be8f1 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java @@ -57,7 +57,7 @@ public class DirectSettingsViewModel extends AndroidViewModel { } final ContentResolver contentResolver = application.getContentResolver(); resources = getApplication().getResources(); - final DirectMessagesManager messagesManager = DirectMessagesManager.getInstance(); + final DirectMessagesManager messagesManager = DirectMessagesManager.INSTANCE; threadManager = messagesManager.getThreadManager(threadId, pending, currentUser, contentResolver); } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java index a74c68a0..4ea7100b 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java @@ -52,8 +52,8 @@ public class DirectThreadViewModel extends AndroidViewModel { private final long viewerId; private final String threadId; private final User currentUser; - private final ThreadManager threadManager; + private ThreadManager threadManager; private VoiceRecorder voiceRecorder; public DirectThreadViewModel(@NonNull final Application application, @@ -73,14 +73,15 @@ public class DirectThreadViewModel extends AndroidViewModel { } contentResolver = application.getContentResolver(); recordingsDir = DirectoryUtils.getOutputMediaDirectory(application, "Recordings"); - final DirectMessagesManager messagesManager = DirectMessagesManager.getInstance(); + final DirectMessagesManager messagesManager = DirectMessagesManager.INSTANCE; threadManager = messagesManager.getThreadManager(threadId, pending, currentUser, contentResolver); threadManager.fetchPendingRequests(); } public void moveFromPending() { - DirectMessagesManager.getInstance().moveThreadFromPending(threadId); - threadManager.moveFromPending(); + final DirectMessagesManager messagesManager = DirectMessagesManager.INSTANCE; + messagesManager.moveThreadFromPending(threadId); + threadManager = messagesManager.getThreadManager(threadId, false, currentUser, contentResolver); } public void removeThread() { @@ -268,7 +269,7 @@ public class DirectThreadViewModel extends AndroidViewModel { threadManager.forward(recipient, itemToForward); } - public void setReplyToItem(final DirectItem item) { + public void setReplyToItem(@Nullable final DirectItem item) { // Log.d(TAG, "setReplyToItem: " + item); threadManager.setReplyToItem(item); } @@ -327,7 +328,7 @@ public class DirectThreadViewModel extends AndroidViewModel { final DirectThread thread = getThread().getValue(); if (thread == null) return; if (thread.isTemp() && (thread.getItems() == null || thread.getItems().isEmpty())) { - final InboxManager inboxManager = DirectMessagesManager.getInstance().getInboxManager(); + final InboxManager inboxManager = DirectMessagesManager.INSTANCE.getInboxManager(); inboxManager.removeThread(threadId); } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.java index 6b9466bc..e0c36a1b 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.java @@ -333,14 +333,14 @@ public class PostViewV2ViewModel extends ViewModel { public void shareDm(@NonNull final RankedRecipient result) { if (messageManager == null) { - messageManager = DirectMessagesManager.getInstance(); + messageManager = DirectMessagesManager.INSTANCE; } messageManager.sendMedia(result, media.getId()); } public void shareDm(@NonNull final Set recipients) { if (messageManager == null) { - messageManager = DirectMessagesManager.getInstance(); + messageManager = DirectMessagesManager.INSTANCE; } messageManager.sendMedia(recipients, media.getId()); }