diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java index 4815859d..d46e9399 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -617,11 +617,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private void setObservers() { threadLiveData = viewModel.getThread(); - if (threadLiveData == null) { - final NavController navController = NavHostFragment.findNavController(this); - navController.navigateUp(); - return; - } + // if (threadLiveData == null) { + // final NavController navController = NavHostFragment.findNavController(this); + // navController.navigateUp(); + // return; + // } pendingLiveData = viewModel.isPending(); pendingLiveData.observe(getViewLifecycleOwner(), isPending -> { if (isPending == null) { diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt b/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt index c26ba261..c716c3ca 100644 --- a/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt @@ -73,7 +73,7 @@ class ThreadManager private constructor( 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() + val thread = threads.firstOrNull { it.threadId == threadId } thread?.also { cursor = thread.oldestCursor hasOlder = thread.hasOlder diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java deleted file mode 100644 index 4ea7100b..00000000 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java +++ /dev/null @@ -1,336 +0,0 @@ -package awais.instagrabber.viewmodels; - -import android.app.Application; -import android.content.ContentResolver; -import android.media.MediaScannerConnection; -import android.net.Uri; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Transformations; - -import java.io.File; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import awais.instagrabber.customviews.emoji.Emoji; -import awais.instagrabber.managers.DirectMessagesManager; -import awais.instagrabber.managers.InboxManager; -import awais.instagrabber.managers.ThreadManager; -import awais.instagrabber.models.Resource; -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.DirectThreadLastSeenAt; -import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; -import awais.instagrabber.repositories.responses.giphy.GiphyGif; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DirectoryUtils; -import awais.instagrabber.utils.MediaController; -import awais.instagrabber.utils.MediaUtils; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.VoiceRecorder; - -import static awais.instagrabber.utils.Utils.settingsHelper; - -public class DirectThreadViewModel extends AndroidViewModel { - private static final String TAG = DirectThreadViewModel.class.getSimpleName(); - // private static final String ERROR_INVALID_THREAD = "Invalid thread"; - - private final ContentResolver contentResolver; - private final File recordingsDir; - private final Application application; - private final long viewerId; - private final String threadId; - private final User currentUser; - - private ThreadManager threadManager; - private VoiceRecorder voiceRecorder; - - public DirectThreadViewModel(@NonNull final Application application, - @NonNull final String threadId, - final boolean pending, - @NonNull final User currentUser) { - super(application); - this.application = application; - this.threadId = threadId; - this.currentUser = currentUser; - 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 (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) { - throw new IllegalArgumentException("User is not logged in!"); - } - contentResolver = application.getContentResolver(); - recordingsDir = DirectoryUtils.getOutputMediaDirectory(application, "Recordings"); - final DirectMessagesManager messagesManager = DirectMessagesManager.INSTANCE; - threadManager = messagesManager.getThreadManager(threadId, pending, currentUser, contentResolver); - threadManager.fetchPendingRequests(); - } - - public void moveFromPending() { - final DirectMessagesManager messagesManager = DirectMessagesManager.INSTANCE; - messagesManager.moveThreadFromPending(threadId); - threadManager = messagesManager.getThreadManager(threadId, false, currentUser, contentResolver); - } - - public void removeThread() { - threadManager.removeThread(); - } - - public String getThreadId() { - return threadId; - } - - public LiveData getThreadTitle() { - return threadManager.getThreadTitle(); - } - - public LiveData getThread() { - return threadManager.getThread(); - } - - public LiveData> getItems() { - return Transformations.map(threadManager.getItems(), items -> items.stream() - .filter(directItem -> directItem.getHideInThread() == 0) - .collect(Collectors.toList())); - } - - public LiveData> isFetching() { - return threadManager.isFetching(); - } - - public LiveData> getUsers() { - return threadManager.getUsers(); - } - - public LiveData> getLeftUsers() { - return threadManager.getLeftUsers(); - } - - public LiveData getPendingRequestsCount() { - return threadManager.getPendingRequestsCount(); - } - - public LiveData getInputMode() { - return threadManager.getInputMode(); - } - - public LiveData isPending() { - return threadManager.isPending(); - } - - public long getViewerId() { - return viewerId; - } - - public LiveData getReplyToItem() { - return threadManager.getReplyToItem(); - } - - public void fetchChats() { - threadManager.fetchChats(); - } - - public void refreshChats() { - threadManager.refreshChats(); - } - - public LiveData> sendText(final String text) { - return threadManager.sendText(text); - } - - public LiveData> sendUri(final MediaController.MediaEntry entry) { - return threadManager.sendUri(entry); - } - - public LiveData> sendUri(final Uri uri) { - return threadManager.sendUri(uri); - } - - public LiveData> startRecording() { - final MutableLiveData> data = new MutableLiveData<>(); - voiceRecorder = new VoiceRecorder(recordingsDir, new VoiceRecorder.VoiceRecorderCallback() { - @Override - public void onStart() {} - - @Override - public void onComplete(final VoiceRecorder.VoiceRecordingResult result) { - Log.d(TAG, "onComplete: recording complete. Scanning file..."); - MediaScannerConnection.scanFile( - application, - new String[]{result.getFile().getAbsolutePath()}, - new String[]{result.getMimeType()}, - (path, uri) -> { - if (uri == null) { - final String msg = "Scan failed!"; - Log.e(TAG, msg); - data.postValue(Resource.error(msg, null)); - return; - } - Log.d(TAG, "onComplete: scan complete"); - MediaUtils.getVoiceInfo(contentResolver, uri, new MediaUtils.OnInfoLoadListener() { - @Override - public void onLoad(@Nullable final MediaUtils.VideoInfo videoInfo) { - if (videoInfo == null) return; - threadManager.sendVoice(data, - uri, - result.getWaveform(), - result.getSamplingFreq(), - videoInfo == null ? 0 : videoInfo.duration, - videoInfo == null ? 0 : videoInfo.size); - } - - @Override - public void onFailure(final Throwable t) { - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - } - ); - } - - @Override - public void onCancel() { - - } - }); - voiceRecorder.startRecording(); - return data; - } - - public void stopRecording(final boolean delete) { - if (voiceRecorder == null) return; - voiceRecorder.stopRecording(delete); - voiceRecorder = null; - } - - public LiveData> sendReaction(@NonNull final DirectItem item, @NonNull final Emoji emoji) { - return threadManager.sendReaction(item, emoji); - } - - public LiveData> sendDeleteReaction(@NonNull final String itemId) { - return threadManager.sendDeleteReaction(itemId); - } - - public LiveData> unsend(final DirectItem item) { - return threadManager.unsend(item); - } - - public LiveData> sendAnimatedMedia(@NonNull final GiphyGif giphyGif) { - return threadManager.sendAnimatedMedia(giphyGif); - } - - public User getCurrentUser() { - return currentUser; - } - - @Nullable - public User getUser(final long userId) { - final LiveData> users = getUsers(); - User match = null; - if (users != null && users.getValue() != null) { - final List userList = users.getValue(); - match = userList.stream() - .filter(Objects::nonNull) - .filter(user -> user.getPk() == userId) - .findFirst() - .orElse(null); - } - if (match == null) { - final LiveData> leftUsers = getLeftUsers(); - if (leftUsers != null && leftUsers.getValue() != null) { - final List userList = leftUsers.getValue(); - match = userList.stream() - .filter(Objects::nonNull) - .filter(user -> user.getPk() == userId) - .findFirst() - .orElse(null); - } - } - return match; - } - - public void forward(final Set recipients, final DirectItem itemToForward) { - threadManager.forward(recipients, itemToForward); - } - - public void forward(final RankedRecipient recipient, final DirectItem itemToForward) { - threadManager.forward(recipient, itemToForward); - } - - public void setReplyToItem(@Nullable final DirectItem item) { - // Log.d(TAG, "setReplyToItem: " + item); - threadManager.setReplyToItem(item); - } - - public LiveData> acceptRequest() { - return threadManager.acceptRequest(); - } - - public LiveData> declineRequest() { - return threadManager.declineRequest(); - } - - public LiveData> markAsSeen() { - if (currentUser == null) { - return getSuccessEventResObjectLiveData(); - } - final DirectThread thread = getThread().getValue(); - if (thread == null) { - return getSuccessEventResObjectLiveData(); - } - final List items = thread.getItems(); - if (items == null || items.isEmpty()) { - return getSuccessEventResObjectLiveData(); - } - final Optional itemOptional = items.stream() - .filter(item -> item.getUserId() != currentUser.getPk()) - .findFirst(); - if (!itemOptional.isPresent()) { - return getSuccessEventResObjectLiveData(); - } - final DirectItem directItem = itemOptional.get(); - final Map lastSeenAt = thread.getLastSeenAt(); - if (lastSeenAt != null) { - final DirectThreadLastSeenAt seenAt = lastSeenAt.get(currentUser.getPk()); - try { - if (seenAt != null - && (Objects.equals(seenAt.getItemId(), directItem.getItemId()) - || Long.parseLong(seenAt.getTimestamp()) >= directItem.getTimestamp())) { - return getSuccessEventResObjectLiveData(); - } - } catch (Exception ignored) { - return getSuccessEventResObjectLiveData(); - } - } - return threadManager.markAsSeen(directItem); - } - - @NonNull - private MutableLiveData> getSuccessEventResObjectLiveData() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.success(new Object())); - return data; - } - - public void deleteThreadIfRequired() { - final DirectThread thread = getThread().getValue(); - if (thread == null) return; - if (thread.isTemp() && (thread.getItems() == null || thread.getItems().isEmpty())) { - final InboxManager inboxManager = DirectMessagesManager.INSTANCE.getInboxManager(); - inboxManager.removeThread(threadId); - } - } - -} diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt new file mode 100644 index 00000000..ab2c3067 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt @@ -0,0 +1,230 @@ +package awais.instagrabber.viewmodels + +import android.app.Application +import android.content.ContentResolver +import android.media.MediaScannerConnection +import android.net.Uri +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import awais.instagrabber.customviews.emoji.Emoji +import awais.instagrabber.managers.DirectMessagesManager +import awais.instagrabber.managers.DirectMessagesManager.inboxManager +import awais.instagrabber.managers.ThreadManager +import awais.instagrabber.models.Resource +import awais.instagrabber.models.Resource.Companion.error +import awais.instagrabber.models.Resource.Companion.success +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.RankedRecipient +import awais.instagrabber.repositories.responses.giphy.GiphyGif +import awais.instagrabber.utils.* +import awais.instagrabber.utils.MediaUtils.OnInfoLoadListener +import awais.instagrabber.utils.MediaUtils.VideoInfo +import awais.instagrabber.utils.VoiceRecorder.VoiceRecorderCallback +import awais.instagrabber.utils.VoiceRecorder.VoiceRecordingResult +import awais.instagrabber.utils.extensions.TAG +import java.io.File +import java.util.* + +class DirectThreadViewModel( + application: Application, + val threadId: String, + pending: Boolean, + val currentUser: User, +) : AndroidViewModel(application) { + // private val TAG = DirectThreadViewModel::class.java.simpleName + + // private static final String ERROR_INVALID_THREAD = "Invalid thread"; + private val contentResolver: ContentResolver = application.contentResolver + private val recordingsDir: File = DirectoryUtils.getOutputMediaDirectory(application, "Recordings") + private var voiceRecorder: VoiceRecorder? = null + private lateinit var threadManager: ThreadManager + + val viewerId: Long + val threadTitle: LiveData by lazy { threadManager.threadTitle } + val thread: LiveData by lazy { threadManager.thread } + val items: LiveData> by lazy { + Transformations.map(threadManager.items) { it.filter { thread -> thread.hideInThread == 0 } } + } + val isFetching: LiveData> by lazy { threadManager.isFetching() } + val users: LiveData> by lazy { threadManager.users } + val leftUsers: LiveData> by lazy { threadManager.leftUsers } + val pendingRequestsCount: LiveData by lazy { threadManager.pendingRequestsCount } + val inputMode: LiveData by lazy { threadManager.inputMode } + val isPending: LiveData by lazy { threadManager.isPending } + val replyToItem: LiveData by lazy { threadManager.getReplyToItem() } + + fun moveFromPending() { + val messagesManager = DirectMessagesManager + messagesManager.moveThreadFromPending(threadId) + threadManager = messagesManager.getThreadManager(threadId, false, currentUser, contentResolver) + } + + fun removeThread() { + threadManager.removeThread() + } + + fun fetchChats() { + threadManager.fetchChats() + } + + fun refreshChats() { + threadManager.refreshChats() + } + + fun sendText(text: String): LiveData> { + return threadManager.sendText(text) + } + + fun sendUri(entry: MediaController.MediaEntry): LiveData> { + return threadManager.sendUri(entry) + } + + fun sendUri(uri: Uri): LiveData> { + return threadManager.sendUri(uri) + } + + fun startRecording(): LiveData> { + val data = MutableLiveData>() + voiceRecorder = VoiceRecorder(recordingsDir, object : VoiceRecorderCallback { + override fun onStart() {} + override fun onComplete(result: VoiceRecordingResult) { + Log.d(TAG, "onComplete: recording complete. Scanning file...") + MediaScannerConnection.scanFile( + getApplication(), + arrayOf(result.file.absolutePath), + arrayOf(result.mimeType) + ) { _: String?, uri: Uri? -> + if (uri == null) { + val msg = "Scan failed!" + Log.e(TAG, msg) + data.postValue(error(msg, null)) + return@scanFile + } + Log.d(TAG, "onComplete: scan complete") + MediaUtils.getVoiceInfo(contentResolver, uri, object : OnInfoLoadListener { + override fun onLoad(videoInfo: VideoInfo?) { + if (videoInfo == null) return + threadManager.sendVoice(data, + uri, + result.waveform, + result.samplingFreq, + videoInfo.duration, + videoInfo.size) + } + + override fun onFailure(t: Throwable) { + data.postValue(error(t.message, null)) + } + }) + } + } + + override fun onCancel() {} + }) + voiceRecorder?.startRecording() + return data + } + + fun stopRecording(delete: Boolean) { + voiceRecorder?.stopRecording(delete) + voiceRecorder = null + } + + fun sendReaction(item: DirectItem, emoji: Emoji): LiveData> { + return threadManager.sendReaction(item, emoji) + } + + fun sendDeleteReaction(itemId: String): LiveData> { + return threadManager.sendDeleteReaction(itemId) + } + + fun unsend(item: DirectItem): LiveData> { + return threadManager.unsend(item) + } + + fun sendAnimatedMedia(giphyGif: GiphyGif): LiveData> { + return threadManager.sendAnimatedMedia(giphyGif) + } + + fun getUser(userId: Long): User? { + var match: User? = null + users.value?.let { match = it.firstOrNull { user -> user.pk == userId } } + if (match == null) { + leftUsers.value?.let { match = it.firstOrNull { user -> user.pk == userId } } + } + return match + } + + fun forward(recipients: Set, itemToForward: DirectItem) { + threadManager.forward(recipients, itemToForward) + } + + fun forward(recipient: RankedRecipient, itemToForward: DirectItem) { + threadManager.forward(recipient, itemToForward) + } + + fun setReplyToItem(item: DirectItem?) { + // Log.d(TAG, "setReplyToItem: " + item); + threadManager.setReplyToItem(item) + } + + fun acceptRequest(): LiveData> { + return threadManager.acceptRequest() + } + + fun declineRequest(): LiveData> { + return threadManager.declineRequest() + } + + fun markAsSeen(): LiveData> { + val thread = thread.value ?: return successEventResObjectLiveData + val items = thread.items + if (items.isNullOrEmpty()) return successEventResObjectLiveData + val directItem = items.firstOrNull { (_, userId) -> userId != currentUser.pk } ?: return successEventResObjectLiveData + val lastSeenAt = thread.lastSeenAt + if (lastSeenAt != null) { + val seenAt = lastSeenAt[currentUser.pk] ?: return successEventResObjectLiveData + try { + val timestamp = seenAt.timestamp ?: return successEventResObjectLiveData + val itemIdMatches = seenAt.itemId == directItem.itemId + val timestampMatches = timestamp.toLong() >= directItem.getTimestamp() + if (itemIdMatches || timestampMatches) { + return successEventResObjectLiveData + } + } catch (ignored: Exception) { + return successEventResObjectLiveData + } + } + return threadManager.markAsSeen(directItem) + } + + private val successEventResObjectLiveData: MutableLiveData> + get() { + val data = MutableLiveData>() + data.postValue(success(Any())) + return data + } + + fun deleteThreadIfRequired() { + val thread = thread.value ?: return + if (thread.isTemp && thread.items.isNullOrEmpty()) { + val inboxManager = inboxManager + inboxManager.removeThread(threadId) + } + } + + init { + val cookie = Utils.settingsHelper.getString(Constants.COOKIE) + 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!" } + threadManager = DirectMessagesManager.getThreadManager(threadId, pending, currentUser, contentResolver) + threadManager.fetchPendingRequests() + } +} \ No newline at end of file