Browse Source

Convert to kotlin, suspend funs, and viewModelScope

renovate/org.robolectric-robolectric-4.x
Ammar Githam 4 years ago
parent
commit
87e6e4440f
  1. 5
      app/build.gradle
  2. 2
      app/src/main/java/awais/instagrabber/activities/MainActivity.kt
  3. 13
      app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java
  4. 16
      app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt
  5. 71
      app/src/main/java/awais/instagrabber/managers/InboxManager.kt
  6. 158
      app/src/main/java/awais/instagrabber/managers/ThreadManager.kt
  7. 8
      app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.kt
  8. 2
      app/src/main/java/awais/instagrabber/services/DMSyncService.java
  9. 56
      app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java
  10. 36
      app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.kt
  11. 48
      app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java
  12. 34
      app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.kt
  13. 299
      app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java
  14. 201
      app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.kt
  15. 13
      app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt
  16. 506
      app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt
  17. 10
      app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.kt

5
app/build.gradle

@ -182,8 +182,13 @@ dependencies {
def core_version = "1.6.0-beta01"
implementation "androidx.core:core:$core_version"
// Fragment
implementation "androidx.fragment:fragment-ktx:1.3.4"
// Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
// Room
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"

2
app/src/main/java/awais/instagrabber/activities/MainActivity.kt

@ -185,7 +185,7 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
private fun initDmUnreadCount() {
if (!isLoggedIn) return
val directInboxViewModel = ViewModelProvider(this).get(DirectInboxViewModel::class.java)
directInboxViewModel.unseenCount.observe(this, { unseenCountResource: Resource<Int>? ->
directInboxViewModel.unseenCount.observe(this, { unseenCountResource: Resource<Int?>? ->
if (unseenCountResource == null) return@observe
val unseenCount = unseenCountResource.data
setNavBarDMUnreadCountBadge(unseenCount ?: 0)

13
app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java

@ -217,7 +217,7 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme
// wasPaused = true;
if (settingsHelper.getBoolean(PreferenceKeys.PLAY_IN_BACKGROUND)) return;
final Media media = viewModel.getMedia();
if (media == null || media.getMediaType() == null) return;
if (media.getMediaType() == null) return;
switch (media.getMediaType()) {
case MEDIA_TYPE_VIDEO:
if (videoPlayerViewHelper != null) {
@ -250,7 +250,7 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme
super.onDestroyView();
showSystemUI();
final Media media = viewModel.getMedia();
if (media == null || media.getMediaType() == null) return;
if (media.getMediaType() == null) return;
switch (media.getMediaType()) {
case MEDIA_TYPE_VIDEO:
if (videoPlayerViewHelper != null) {
@ -269,7 +269,6 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
final Media media = viewModel.getMedia();
if (media == null) return;
if (media.getMediaType() == MediaItemType.MEDIA_TYPE_SLIDER) {
outState.putInt(ARG_SLIDER_POSITION, sliderPosition);
}
@ -1440,9 +1439,7 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme
actionBar.hide();
}
final CollapsingToolbarLayout appbarLayout = activity.getCollapsingToolbarView();
if (appbarLayout != null) {
appbarLayout.setVisibility(View.GONE);
}
appbarLayout.setVisibility(View.GONE);
final Toolbar toolbar = activity.getToolbar();
if (toolbar != null) {
toolbar.setVisibility(View.GONE);
@ -1467,9 +1464,7 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme
actionBar.show();
}
final CollapsingToolbarLayout appbarLayout = activity.getCollapsingToolbarView();
if (appbarLayout != null) {
appbarLayout.setVisibility(View.VISIBLE);
}
appbarLayout.setVisibility(View.VISIBLE);
final Toolbar toolbar = activity.getToolbar();
if (toolbar != null) {
toolbar.setVisibility(View.VISIBLE);

16
app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt

@ -20,6 +20,7 @@ import awais.instagrabber.utils.getCsrfTokenFromCookie
import awais.instagrabber.utils.getUserIdFromCookie
import awais.instagrabber.webservices.DirectMessagesService
import awais.instagrabber.webservices.DirectMessagesService.Companion.getInstance
import kotlinx.coroutines.CoroutineScope
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -108,21 +109,21 @@ object DirectMessagesManager {
})
}
fun sendMedia(recipients: Set<RankedRecipient>, mediaId: String) {
fun sendMedia(recipients: Set<RankedRecipient>, mediaId: String, scope: CoroutineScope) {
val resultsCount = intArrayOf(0)
val callback: () -> Unit = {
resultsCount[0]++
if (resultsCount[0] == recipients.size) {
inboxManager.refresh()
inboxManager.refresh(scope)
}
}
for (recipient in recipients) {
sendMedia(recipient, mediaId, false, callback)
sendMedia(recipient, mediaId, false, callback, scope)
}
}
fun sendMedia(recipient: RankedRecipient, mediaId: String) {
sendMedia(recipient, mediaId, true, null)
fun sendMedia(recipient: RankedRecipient, mediaId: String, scope: CoroutineScope) {
sendMedia(recipient, mediaId, true, null, scope)
}
private fun sendMedia(
@ -130,6 +131,7 @@ object DirectMessagesManager {
mediaId: String,
refreshInbox: Boolean,
callback: (() -> Unit)?,
scope: CoroutineScope,
) {
if (recipient.thread == null && recipient.user != null) {
// create thread and forward
@ -137,7 +139,7 @@ object DirectMessagesManager {
val threadIdTemp = threadId ?: return@createThread
sendMedia(threadIdTemp, mediaId) {
if (refreshInbox) {
inboxManager.refresh()
inboxManager.refresh(scope)
}
callback?.invoke()
}
@ -149,7 +151,7 @@ object DirectMessagesManager {
val threadId = thread.threadId ?: return
sendMedia(threadId, mediaId) {
if (refreshInbox) {
inboxManager.refresh()
inboxManager.refresh(scope)
}
callback?.invoke()
}

71
app/src/main/java/awais/instagrabber/managers/InboxManager.kt

@ -11,15 +11,15 @@ 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.utils.*
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -27,7 +27,9 @@ import java.util.*
import java.util.concurrent.TimeUnit
class InboxManager private constructor(private val pending: Boolean) {
private val inbox = MutableLiveData<Resource<DirectInbox?>>()
// private val fetchInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner()
// private val fetchPendingInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner()
private val inbox = MutableLiveData<Resource<DirectInbox?>>(success(null))
private val unseenCount = MutableLiveData<Resource<Int?>>()
private val pendingRequestsTotal = MutableLiveData(0)
val threads: LiveData<List<DirectThread>>
@ -52,30 +54,37 @@ class InboxManager private constructor(private val pending: Boolean) {
return Transformations.distinctUntilChanged(pendingRequestsTotal)
}
fun fetchInbox() {
fun fetchInbox(scope: CoroutineScope) {
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<DirectInboxResponse?> {
override fun onResponse(call: Call<DirectInboxResponse?>, response: Response<DirectInboxResponse?>) {
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 fun onFailure(call: Call<DirectInboxResponse?>, t: Throwable) {
Log.e(TAG, "Failed fetching dm inbox", t)
inbox.postValue(error(t.message, currentDirectInbox))
scope.launch(Dispatchers.IO) {
try {
val inboxValue = if (pending) service.fetchPendingInbox(cursor, seqId) else service.fetchInbox(cursor, seqId)
parseInboxResponse(inboxValue)
} catch (e: Exception) {
inbox.postValue(error(e.message, currentDirectInbox))
hasOlder = false
}
})
// inboxRequest?.enqueue(object : Callback<DirectInboxResponse?> {
// override fun onResponse(call: Call<DirectInboxResponse?>, response: Response<DirectInboxResponse?>) {
// 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
// }
//
// }
//
// override fun onFailure(call: Call<DirectInboxResponse?>, t: Throwable) {
// Log.e(TAG, "Failed fetching dm inbox", t)
// inbox.postValue(error(t.message, currentDirectInbox))
// hasOlder = false
// }
// })
}
}
fun fetchUnseenCount() {
@ -102,11 +111,11 @@ class InboxManager private constructor(private val pending: Boolean) {
})
}
fun refresh() {
fun refresh(scope: CoroutineScope) {
cursor = null
seqId = 0
hasOlder = true
fetchInbox()
fetchInbox(scope)
if (!pending) {
fetchUnseenCount()
}
@ -333,15 +342,15 @@ class InboxManager private constructor(private val pending: Boolean) {
service = getInstance(csrfToken, viewerId, deviceUuid)
// Transformations
threads = Transformations.distinctUntilChanged(Transformations.map(inbox) { inboxResource: Resource<DirectInbox?>? ->
if (inboxResource == null) {
return@map emptyList()
}
threads = Transformations.distinctUntilChanged(Transformations.map(inbox) { inboxResource: Resource<DirectInbox?> ->
// if (inboxResource == null) {
// return@map emptyList()
// }
val inbox = inboxResource.data
val threads = inbox?.threads ?: emptyList()
ImmutableList.sortedCopyOf(THREAD_COMPARATOR, threads)
})
fetchInbox()
// fetchInbox()
if (!pending) {
fetchUnseenCount()
}

158
app/src/main/java/awais/instagrabber/managers/ThreadManager.kt

@ -37,6 +37,9 @@ import awais.instagrabber.webservices.MediaService
import awais.instagrabber.webservices.ServiceCallback
import com.google.common.collect.ImmutableList
import com.google.common.collect.Iterables
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -56,9 +59,12 @@ class ThreadManager private constructor(
csrfToken: String,
deviceUuid: String,
) {
private val fetching = MutableLiveData<Resource<Any?>>()
private val replyToItem = MutableLiveData<DirectItem?>()
private val pendingRequests = MutableLiveData<DirectThreadParticipantRequestsResponse?>(null)
private val _fetching = MutableLiveData<Resource<Any?>>()
val fetching: LiveData<Resource<Any?>> = _fetching
private val _replyToItem = MutableLiveData<DirectItem?>()
val replyToItem: LiveData<DirectItem?> = _replyToItem
private val _pendingRequests = MutableLiveData<DirectThreadParticipantRequestsResponse?>(null)
val pendingRequests: LiveData<DirectThreadParticipantRequestsResponse?> = _pendingRequests
private val inboxManager: InboxManager = if (pending) DirectMessagesManager.pendingInboxManager else DirectMessagesManager.inboxManager
private val viewerId: Long
private val threadIdOrUserIds: ThreadIdOrUserIds = of(threadId)
@ -106,7 +112,7 @@ class ThreadManager private constructor(
val isMuted: LiveData<Boolean> by lazy { distinctUntilChanged(map(thread) { it?.muted ?: false }) }
val isApprovalRequiredToJoin: LiveData<Boolean> by lazy { distinctUntilChanged(map(thread) { it?.approvalRequiredForNewMembers ?: false }) }
val isMentionsMuted: LiveData<Boolean> by lazy { distinctUntilChanged(map(thread) { it?.mentionsMuted ?: false }) }
val pendingRequestsCount: LiveData<Int> by lazy { distinctUntilChanged(map(pendingRequests) { it?.totalParticipantRequests ?: 0 }) }
val pendingRequestsCount: LiveData<Int> by lazy { distinctUntilChanged(map(_pendingRequests) { it?.totalParticipantRequests ?: 0 }) }
val inviter: LiveData<User?> by lazy { distinctUntilChanged(map(thread) { it?.inviter }) }
private var hasOlder = true
@ -125,50 +131,58 @@ class ThreadManager private constructor(
return builder.build()
}
fun isFetching(): LiveData<Resource<Any?>> {
return fetching
}
fun getReplyToItem(): LiveData<DirectItem?> {
return replyToItem
}
fun getPendingRequests(): LiveData<DirectThreadParticipantRequestsResponse?> {
return pendingRequests
}
fun fetchChats() {
val fetchingValue = fetching.value
fun fetchChats(scope: CoroutineScope) {
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<DirectThreadFeedResponse?> {
override fun onResponse(call: Call<DirectThreadFeedResponse?>, response: Response<DirectThreadFeedResponse?>) {
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
_fetching.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val threadFeedResponse = service.fetchThread(threadId, cursor)
if (threadFeedResponse.status != null && threadFeedResponse.status != "ok") {
_fetching.postValue(error(R.string.generic_not_ok_response, null))
return@launch
}
val thread = feedResponse.thread
val thread = threadFeedResponse.thread
if (thread == null) {
fetching.postValue(error("thread is null!", null))
return
_fetching.postValue(error("thread is null!", null))
return@launch
}
setThread(thread)
fetching.postValue(success(Any()))
}
override fun onFailure(call: Call<DirectThreadFeedResponse?>, t: Throwable) {
Log.e(TAG, "Failed fetching dm chats", t)
fetching.postValue(error(t.message, null))
_fetching.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "Failed fetching dm chats", e)
_fetching.postValue(error(e.message, null))
hasOlder = false
}
})
// chatsRequest?.enqueue(object : Callback<DirectThreadFeedResponse?> {
// override fun onResponse(call: Call<DirectThreadFeedResponse?>, response: Response<DirectThreadFeedResponse?>) {
// 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<DirectThreadFeedResponse?>, t: Throwable) {
// Log.e(TAG, "Failed fetching dm chats", t)
// fetching.postValue(error(t.message, null))
// hasOlder = false
// }
// })
}
if (cursor == null) {
fetchPendingRequests()
}
@ -206,7 +220,7 @@ class ThreadManager private constructor(
Log.e(TAG, "onResponse: response body was null")
return
}
pendingRequests.postValue(body)
_pendingRequests.postValue(body)
}
override fun onFailure(call: Call<DirectThreadParticipantRequestsResponse?>, t: Throwable) {
@ -389,7 +403,7 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>()
val userId = getCurrentUserId(data) ?: return data
val clientContext = UUID.randomUUID().toString()
val replyToItemValue = replyToItem.value
val replyToItemValue = _replyToItem.value
val directItem = createText(userId, clientContext, text, replyToItemValue)
// Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId());
directItem.isPending = true
@ -630,7 +644,7 @@ class ThreadManager private constructor(
fun setReplyToItem(item: DirectItem?) {
// Log.d(TAG, "setReplyToItem: " + item);
replyToItem.postValue(item)
_replyToItem.postValue(item)
}
private fun forward(thread: DirectThread, itemToForward: DirectItem): LiveData<Resource<Any?>> {
@ -770,14 +784,14 @@ class ThreadManager private constructor(
return data
}
fun refreshChats() {
val isFetching = fetching.value
fun refreshChats(scope: CoroutineScope) {
val isFetching = _fetching.value
if (isFetching != null && isFetching.status === Resource.Status.LOADING) {
stopCurrentRequest()
}
cursor = null
hasOlder = true
fetchChats()
fetchChats(scope)
}
private fun sendPhoto(
@ -1076,7 +1090,7 @@ class ThreadManager private constructor(
if (it.isExecuted || it.isCanceled) return
it.cancel()
}
fetching.postValue(success(Any()))
_fetching.postValue(success(Any()))
}
private fun getCurrentUserId(data: MutableLiveData<Resource<Any?>>): Long? {
@ -1108,10 +1122,7 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>()
val addUsersRequest = service.addUsers(
threadId,
users.stream()
.filter { obj: User? -> Objects.nonNull(obj) }
.map { obj: User -> obj.pk }
.collect(Collectors.toList())
users.map { obj: User -> obj.pk }
)
handleDetailsChangeRequest(data, addUsersRequest)
return data
@ -1135,10 +1146,7 @@ class ThreadManager private constructor(
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 updatedActiveUsers = activeUsers.filter { u: User -> u.pk != user.pk }
val updatedLeftUsersBuilder = ImmutableList.builder<User>().addAll(leftUsersValue)
if (!leftUsersValue.contains(user)) {
updatedLeftUsersBuilder.add(user)
@ -1204,10 +1212,7 @@ class ThreadManager private constructor(
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 updatedAdminUserIds = currentAdmins.filter { userId1: Long -> userId1 != user.pk }
val currentThread = thread.value ?: return
try {
val thread = currentThread.clone() as DirectThread
@ -1362,11 +1367,11 @@ class ThreadManager private constructor(
return data
}
fun blockUser(user: User): LiveData<Resource<Any?>> {
fun blockUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
friendshipService.changeBlock(false, user.pk, object : ServiceCallback<FriendshipChangeResponse?> {
override fun onSuccess(result: FriendshipChangeResponse?) {
refreshChats()
refreshChats(scope)
}
override fun onFailure(t: Throwable) {
@ -1377,11 +1382,11 @@ class ThreadManager private constructor(
return data
}
fun unblockUser(user: User): LiveData<Resource<Any?>> {
fun unblockUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
friendshipService.changeBlock(true, user.pk, object : ServiceCallback<FriendshipChangeResponse?> {
override fun onSuccess(result: FriendshipChangeResponse?) {
refreshChats()
refreshChats(scope)
}
override fun onFailure(t: Throwable) {
@ -1392,11 +1397,11 @@ class ThreadManager private constructor(
return data
}
fun restrictUser(user: User): LiveData<Resource<Any?>> {
fun restrictUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
friendshipService.toggleRestrict(user.pk, true, object : ServiceCallback<FriendshipRestrictResponse?> {
override fun onSuccess(result: FriendshipRestrictResponse?) {
refreshChats()
refreshChats(scope)
}
override fun onFailure(t: Throwable) {
@ -1407,11 +1412,11 @@ class ThreadManager private constructor(
return data
}
fun unRestrictUser(user: User): LiveData<Resource<Any?>> {
fun unRestrictUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
friendshipService.toggleRestrict(user.pk, false, object : ServiceCallback<FriendshipRestrictResponse?> {
override fun onSuccess(result: FriendshipRestrictResponse?) {
refreshChats()
refreshChats(scope)
}
override fun onFailure(t: Throwable) {
@ -1427,10 +1432,7 @@ class ThreadManager private constructor(
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())
users.map { obj: User -> obj.pk }
)
handleDetailsChangeRequest(data, approveUsersRequest, object : OnSuccessAction {
override fun onSuccess() {
@ -1445,9 +1447,7 @@ class ThreadManager private constructor(
data.postValue(loading(null))
val approveUsersRequest = service.declineParticipantRequests(
threadId,
users.stream()
.map { obj: User -> obj.pk }
.collect(Collectors.toList())
users.map { obj: User -> obj.pk }
)
handleDetailsChangeRequest(data, approveUsersRequest, object : OnSuccessAction {
override fun onSuccess() {
@ -1458,18 +1458,16 @@ class ThreadManager private constructor(
}
private fun pendingUserApproveDenySuccessAction(users: List<User>) {
val pendingRequestsValue = pendingRequests.value ?: return
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())
val filtered = pendingUsers.filter { o: User -> !users.contains(o) }
try {
val clone = pendingRequestsValue.clone() as DirectThreadParticipantRequestsResponse
clone.users = filtered
val totalParticipantRequests = clone.totalParticipantRequests
clone.totalParticipantRequests = if (totalParticipantRequests > 0) totalParticipantRequests - 1 else 0
pendingRequests.postValue(clone)
_pendingRequests.postValue(clone)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "pendingUserApproveDenySuccessAction: ", e)
}

8
app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.kt

@ -6,16 +6,16 @@ import retrofit2.http.*
interface DirectMessagesRepository {
@GET("/api/v1/direct_v2/inbox/")
fun fetchInbox(@QueryMap queryMap: Map<String, String>): Call<DirectInboxResponse?>
suspend fun fetchInbox(@QueryMap queryMap: Map<String, String>): DirectInboxResponse
@GET("/api/v1/direct_v2/pending_inbox/")
fun fetchPendingInbox(@QueryMap queryMap: Map<String, String>): Call<DirectInboxResponse?>
suspend fun fetchPendingInbox(@QueryMap queryMap: Map<String, String>): DirectInboxResponse
@GET("/api/v1/direct_v2/threads/{threadId}/")
fun fetchThread(
suspend fun fetchThread(
@Path("threadId") threadId: String,
@QueryMap queryMap: Map<String, String>,
): Call<DirectThreadFeedResponse?>
): DirectThreadFeedResponse
@GET("/api/v1/direct_v2/get_badge_count/?no_raven=1")
fun fetchUnseenCount(): Call<DirectBadgeCount?>

2
app/src/main/java/awais/instagrabber/services/DMSyncService.java

@ -234,7 +234,7 @@ public class DMSyncService extends LifecycleService {
parseUnread(directInbox);
});
Log.d(TAG, "onStartCommand: refreshing inbox");
inboxManager.refresh();
// inboxManager.refresh();
return START_NOT_STICKY;
}

56
app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java

@ -1,56 +0,0 @@
package awais.instagrabber.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import java.util.List;
import awais.instagrabber.managers.DirectMessagesManager;
import awais.instagrabber.managers.InboxManager;
import awais.instagrabber.models.Resource;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectInbox;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
public class DirectInboxViewModel extends ViewModel {
private static final String TAG = DirectInboxViewModel.class.getSimpleName();
private final InboxManager inboxManager;
public DirectInboxViewModel() {
final DirectMessagesManager messagesManager = DirectMessagesManager.INSTANCE;
inboxManager = messagesManager.getInboxManager();
}
public LiveData<Resource<DirectInbox>> getInbox() {
return inboxManager.getInbox();
}
public LiveData<List<DirectThread>> getThreads() {
return inboxManager.getThreads();
}
public LiveData<Resource<Integer>> getUnseenCount() {
return inboxManager.getUnseenCount();
}
public LiveData<Integer> getPendingRequestsTotal() {
return inboxManager.getPendingRequestsTotal();
}
public User getViewer() {
return inboxManager.getViewer();
}
public void fetchInbox() {
inboxManager.fetchInbox();
}
public void refresh() {
inboxManager.refresh();
}
public void onDestroy() {
inboxManager.onDestroy();
}
}

36
app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.kt

@ -0,0 +1,36 @@
package awais.instagrabber.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.managers.InboxManager
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.DirectInbox
import awais.instagrabber.repositories.responses.directmessages.DirectThread
class DirectInboxViewModel : ViewModel() {
private val inboxManager: InboxManager = DirectMessagesManager.inboxManager
val inbox: LiveData<Resource<DirectInbox?>> = inboxManager.getInbox()
val threads: LiveData<List<DirectThread>> = inboxManager.threads
val unseenCount: LiveData<Resource<Int?>> = inboxManager.getUnseenCount()
val pendingRequestsTotal: LiveData<Int> = inboxManager.getPendingRequestsTotal()
val viewer: User? = inboxManager.viewer
fun fetchInbox() {
inboxManager.fetchInbox(viewModelScope)
}
fun refresh() {
inboxManager.refresh(viewModelScope)
}
fun onDestroy() {
inboxManager.onDestroy()
}
init {
inboxManager.fetchInbox(viewModelScope)
}
}

48
app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java

@ -1,48 +0,0 @@
package awais.instagrabber.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import java.util.List;
import awais.instagrabber.managers.DirectMessagesManager;
import awais.instagrabber.managers.InboxManager;
import awais.instagrabber.models.Resource;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectInbox;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
public class DirectPendingInboxViewModel extends ViewModel {
private static final String TAG = DirectPendingInboxViewModel.class.getSimpleName();
private final InboxManager inboxManager;
public DirectPendingInboxViewModel() {
inboxManager = DirectMessagesManager.INSTANCE.getPendingInboxManager();
inboxManager.fetchInbox();
}
public LiveData<List<DirectThread>> getThreads() {
return inboxManager.getThreads();
}
public LiveData<Resource<DirectInbox>> getInbox() {
return inboxManager.getInbox();
}
public User getViewer() {
return inboxManager.getViewer();
}
public void fetchInbox() {
inboxManager.fetchInbox();
}
public void refresh() {
inboxManager.refresh();
}
public void onDestroy() {
inboxManager.onDestroy();
}
}

34
app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.kt

@ -0,0 +1,34 @@
package awais.instagrabber.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import awais.instagrabber.managers.DirectMessagesManager.pendingInboxManager
import awais.instagrabber.managers.InboxManager
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.DirectInbox
import awais.instagrabber.repositories.responses.directmessages.DirectThread
class DirectPendingInboxViewModel : ViewModel() {
private val inboxManager: InboxManager = pendingInboxManager
val threads: LiveData<List<DirectThread>> = inboxManager.threads
val inbox: LiveData<Resource<DirectInbox?>> = inboxManager.getInbox()
val viewer: User? = inboxManager.viewer
fun fetchInbox() {
inboxManager.fetchInbox(viewModelScope)
}
fun refresh() {
inboxManager.refresh(viewModelScope)
}
fun onDestroy() {
inboxManager.onDestroy()
}
init {
inboxManager.fetchInbox(viewModelScope)
}
}

299
app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java

@ -1,299 +0,0 @@
package awais.instagrabber.viewmodels;
import android.app.Application;
import android.content.ContentResolver;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.util.Pair;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import awais.instagrabber.R;
import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option;
import awais.instagrabber.managers.DirectMessagesManager;
import awais.instagrabber.managers.ThreadManager;
import awais.instagrabber.models.Resource;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.TextUtils;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class DirectSettingsViewModel extends AndroidViewModel {
private static final String TAG = DirectSettingsViewModel.class.getSimpleName();
private static final String ACTION_KICK = "kick";
private static final String ACTION_MAKE_ADMIN = "make_admin";
private static final String ACTION_REMOVE_ADMIN = "remove_admin";
private static final String ACTION_BLOCK = "block";
private static final String ACTION_UNBLOCK = "unblock";
// private static final String ACTION_REPORT = "report";
private static final String ACTION_RESTRICT = "restrict";
private static final String ACTION_UNRESTRICT = "unrestrict";
private final long viewerId;
private final Resources resources;
private final ThreadManager threadManager;
public DirectSettingsViewModel(final Application application,
@NonNull final String threadId,
final boolean pending,
@NonNull final User currentUser) {
super(application);
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!");
}
final ContentResolver contentResolver = application.getContentResolver();
resources = getApplication().getResources();
final DirectMessagesManager messagesManager = DirectMessagesManager.INSTANCE;
threadManager = messagesManager.getThreadManager(threadId, pending, currentUser, contentResolver);
}
@NonNull
public LiveData<DirectThread> getThread() {
return threadManager.getThread();
}
// public void setThread(@NonNull final DirectThread thread) {
// this.thread = thread;
// inputMode.postValue(thread.getInputMode());
// List<User> users = thread.getUsers();
// final ImmutableList.Builder<User> builder = ImmutableList.<User>builder().add(currentUser);
// if (users != null) {
// builder.addAll(users);
// }
// users = builder.build();
// this.users.postValue(new Pair<>(users, thread.getLeftUsers()));
// // setTitle(thread.getThreadTitle());
// final List<Long> adminUserIds = thread.getAdminUserIds();
// this.adminUserIds.postValue(adminUserIds);
// viewerIsAdmin = adminUserIds.contains(viewerId);
// muted.postValue(thread.getMuted());
// mentionsMuted.postValue(thread.isMentionsMuted());
// approvalRequiredToJoin.postValue(thread.isApprovalRequiredForNewMembers());
// isPending.postValue(thread.isPending());
// if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) {
// fetchPendingRequests();
// }
// }
public LiveData<Integer> getInputMode() {
return threadManager.getInputMode();
}
public LiveData<Boolean> isGroup() {
return threadManager.isGroup();
}
public LiveData<List<User>> getUsers() {
return threadManager.getUsersWithCurrent();
}
public LiveData<List<User>> getLeftUsers() {
return threadManager.getLeftUsers();
}
public LiveData<Pair<List<User>, List<User>>> getUsersAndLeftUsers() {
return threadManager.getUsersAndLeftUsers();
}
public LiveData<String> getTitle() {
return threadManager.getThreadTitle();
}
// public void setTitle(final String title) {
// if (title == null) {
// this.title.postValue("");
// return;
// }
// this.title.postValue(title.trim());
// }
public LiveData<List<Long>> getAdminUserIds() {
return threadManager.getAdminUserIds();
}
public LiveData<Boolean> isMuted() {
return threadManager.isMuted();
}
public LiveData<Boolean> getApprovalRequiredToJoin() {
return threadManager.isApprovalRequiredToJoin();
}
public LiveData<DirectThreadParticipantRequestsResponse> getPendingRequests() {
return threadManager.getPendingRequests();
}
public LiveData<Boolean> isPending() {
return threadManager.isPending();
}
public LiveData<Boolean> isViewerAdmin() {
return threadManager.isViewerAdmin();
}
public LiveData<Resource<Object>> updateTitle(final String newTitle) {
return threadManager.updateTitle(newTitle);
}
public LiveData<Resource<Object>> addMembers(final Set<User> users) {
return threadManager.addMembers(users);
}
public LiveData<Resource<Object>> removeMember(final User user) {
return threadManager.removeMember(user);
}
private LiveData<Resource<Object>> makeAdmin(final User user) {
return threadManager.makeAdmin(user);
}
private LiveData<Resource<Object>> removeAdmin(final User user) {
return threadManager.removeAdmin(user);
}
public LiveData<Resource<Object>> mute() {
return threadManager.mute();
}
public LiveData<Resource<Object>> unmute() {
return threadManager.unmute();
}
public LiveData<Resource<Object>> muteMentions() {
return threadManager.muteMentions();
}
public LiveData<Resource<Object>> unmuteMentions() {
return threadManager.unmuteMentions();
}
private LiveData<Resource<Object>> blockUser(final User user) {
return threadManager.blockUser(user);
}
private LiveData<Resource<Object>> unblockUser(final User user) {
return threadManager.unblockUser(user);
}
private LiveData<Resource<Object>> restrictUser(final User user) {
return threadManager.restrictUser(user);
}
private LiveData<Resource<Object>> unRestrictUser(final User user) {
return threadManager.unRestrictUser(user);
}
public LiveData<Resource<Object>> approveUsers(final List<User> users) {
return threadManager.approveUsers(users);
}
public LiveData<Resource<Object>> denyUsers(final List<User> users) {
return threadManager.denyUsers(users);
}
public LiveData<Resource<Object>> approvalRequired() {
return threadManager.approvalRequired();
}
public LiveData<Resource<Object>> approvalNotRequired() {
return threadManager.approvalNotRequired();
}
public LiveData<Resource<Object>> leave() {
return threadManager.leave();
}
public LiveData<Resource<Object>> end() {
return threadManager.end();
}
public ArrayList<Option<String>> createUserOptions(final User user) {
final ArrayList<Option<String>> options = new ArrayList<>();
if (user == null || isSelf(user) || hasLeft(user)) {
return options;
}
final Boolean viewerIsAdmin = threadManager.isViewerAdmin().getValue();
if (viewerIsAdmin != null && viewerIsAdmin) {
options.add(new Option<>(getString(R.string.dms_action_kick), ACTION_KICK));
final boolean isAdmin = threadManager.isAdmin(user);
options.add(new Option<>(
isAdmin ? getString(R.string.dms_action_remove_admin) : getString(R.string.dms_action_make_admin),
isAdmin ? ACTION_REMOVE_ADMIN : ACTION_MAKE_ADMIN
));
}
final boolean blocking = user.getFriendshipStatus().getBlocking();
options.add(new Option<>(
blocking ? getString(R.string.unblock) : getString(R.string.block),
blocking ? ACTION_UNBLOCK : ACTION_BLOCK
));
// options.add(new Option<>(getString(R.string.report), ACTION_REPORT));
final Boolean isGroup = threadManager.isGroup().getValue();
if (isGroup != null && isGroup) {
final boolean restricted = user.getFriendshipStatus().isRestricted();
options.add(new Option<>(
restricted ? getString(R.string.unrestrict) : getString(R.string.restrict),
restricted ? ACTION_UNRESTRICT : ACTION_RESTRICT
));
}
return options;
}
private boolean hasLeft(final User user) {
final List<User> leftUsers = getLeftUsers().getValue();
if (leftUsers == null) return false;
return leftUsers.contains(user);
}
private boolean isSelf(final User user) {
return user.getPk() == viewerId;
}
private String getString(@StringRes final int resId) {
return resources.getString(resId);
}
public LiveData<Resource<Object>> doAction(final User user, final String action) {
if (user == null || action == null) return null;
switch (action) {
case ACTION_KICK:
return removeMember(user);
case ACTION_MAKE_ADMIN:
return makeAdmin(user);
case ACTION_REMOVE_ADMIN:
return removeAdmin(user);
case ACTION_BLOCK:
return blockUser(user);
case ACTION_UNBLOCK:
return unblockUser(user);
// case ACTION_REPORT:
// break;
case ACTION_RESTRICT:
return restrictUser(user);
case ACTION_UNRESTRICT:
return unRestrictUser(user);
default:
return null;
}
}
public LiveData<User> getInviter() {
return threadManager.getInviter();
}
}

201
app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.kt

@ -0,0 +1,201 @@
package awais.instagrabber.viewmodels
import android.app.Application
import androidx.annotation.StringRes
import androidx.core.util.Pair
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import awais.instagrabber.R
import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option
import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.DirectThread
import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.getCsrfTokenFromCookie
import awais.instagrabber.utils.getUserIdFromCookie
class DirectSettingsViewModel(
application: Application,
threadId: String,
pending: Boolean,
currentUser: User,
) : AndroidViewModel(application) {
private val viewerId: Long
private val resources = application.resources
private val threadManager = DirectMessagesManager.getThreadManager(threadId, pending, currentUser, application.contentResolver)
val thread: LiveData<DirectThread?> = threadManager.thread
// public void setThread(@NonNull final DirectThread thread) {
// this.thread = thread;
// inputMode.postValue(thread.getInputMode());
// List<User> users = thread.getUsers();
// final ImmutableList.Builder<User> builder = ImmutableList.<User>builder().add(currentUser);
// if (users != null) {
// builder.addAll(users);
// }
// users = builder.build();
// this.users.postValue(new Pair<>(users, thread.getLeftUsers()));
// // setTitle(thread.getThreadTitle());
// final List<Long> adminUserIds = thread.getAdminUserIds();
// this.adminUserIds.postValue(adminUserIds);
// viewerIsAdmin = adminUserIds.contains(viewerId);
// muted.postValue(thread.getMuted());
// mentionsMuted.postValue(thread.isMentionsMuted());
// approvalRequiredToJoin.postValue(thread.isApprovalRequiredForNewMembers());
// isPending.postValue(thread.isPending());
// if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) {
// fetchPendingRequests();
// }
// }
val inputMode: LiveData<Int> = threadManager.inputMode
fun isGroup(): LiveData<Boolean> = threadManager.isGroup
fun getUsers(): LiveData<List<User>> = threadManager.usersWithCurrent
fun getLeftUsers(): LiveData<List<User>> = threadManager.leftUsers
fun getUsersAndLeftUsers(): LiveData<Pair<List<User>, List<User>>> = threadManager.usersAndLeftUsers
fun getTitle(): LiveData<String?> = threadManager.threadTitle
// public void setTitle(final String title) {
// if (title == null) {
// this.title.postValue("");
// return;
// }
// this.title.postValue(title.trim());
// }
fun getAdminUserIds(): LiveData<List<Long>> = threadManager.adminUserIds
fun isMuted(): LiveData<Boolean> = threadManager.isMuted
fun getApprovalRequiredToJoin(): LiveData<Boolean> = threadManager.isApprovalRequiredToJoin
fun getPendingRequests(): LiveData<DirectThreadParticipantRequestsResponse?> = threadManager.pendingRequests
fun isPending(): LiveData<Boolean> = threadManager.isPending
fun isViewerAdmin(): LiveData<Boolean> = threadManager.isViewerAdmin
fun updateTitle(newTitle: String): LiveData<Resource<Any?>> = threadManager.updateTitle(newTitle)
fun addMembers(users: Set<User>): LiveData<Resource<Any?>> = threadManager.addMembers(users)
fun removeMember(user: User): LiveData<Resource<Any?>> = threadManager.removeMember(user)
private fun makeAdmin(user: User): LiveData<Resource<Any?>> = threadManager.makeAdmin(user)
private fun removeAdmin(user: User): LiveData<Resource<Any?>> = threadManager.removeAdmin(user)
fun mute(): LiveData<Resource<Any?>> = threadManager.mute()
fun unmute(): LiveData<Resource<Any?>> = threadManager.unmute()
fun muteMentions(): LiveData<Resource<Any?>> = threadManager.muteMentions()
fun unmuteMentions(): LiveData<Resource<Any?>> = threadManager.unmuteMentions()
private fun blockUser(user: User): LiveData<Resource<Any?>> = threadManager.blockUser(user, viewModelScope)
private fun unblockUser(user: User): LiveData<Resource<Any?>> = threadManager.unblockUser(user, viewModelScope)
private fun restrictUser(user: User): LiveData<Resource<Any?>> = threadManager.restrictUser(user, viewModelScope)
private fun unRestrictUser(user: User): LiveData<Resource<Any?>> = threadManager.unRestrictUser(user, viewModelScope)
fun approveUsers(users: List<User>): LiveData<Resource<Any?>> = threadManager.approveUsers(users)
fun denyUsers(users: List<User>): LiveData<Resource<Any?>> = threadManager.denyUsers(users)
fun approvalRequired(): LiveData<Resource<Any?>> = threadManager.approvalRequired()
fun approvalNotRequired(): LiveData<Resource<Any?>> = threadManager.approvalNotRequired()
fun leave(): LiveData<Resource<Any?>> = threadManager.leave()
fun end(): LiveData<Resource<Any?>> = threadManager.end()
fun createUserOptions(user: User?): ArrayList<Option<String>> {
val options: ArrayList<Option<String>> = ArrayList()
if (user == null || isSelf(user) || hasLeft(user)) {
return options
}
val viewerIsAdmin: Boolean? = threadManager.isViewerAdmin.value
if (viewerIsAdmin != null && viewerIsAdmin) {
options.add(Option(getString(R.string.dms_action_kick), ACTION_KICK))
val isAdmin: Boolean = threadManager.isAdmin(user)
options.add(Option(
if (isAdmin) getString(R.string.dms_action_remove_admin) else getString(R.string.dms_action_make_admin),
if (isAdmin) ACTION_REMOVE_ADMIN else ACTION_MAKE_ADMIN
))
}
val blocking: Boolean = user.friendshipStatus.blocking
options.add(Option(
if (blocking) getString(R.string.unblock) else getString(R.string.block),
if (blocking) ACTION_UNBLOCK else ACTION_BLOCK
))
// options.add(new Option<>(getString(R.string.report), ACTION_REPORT));
val isGroup: Boolean? = threadManager.isGroup.value
if (isGroup != null && isGroup) {
val restricted: Boolean = user.friendshipStatus.isRestricted
options.add(Option(
if (restricted) getString(R.string.unrestrict) else getString(R.string.restrict),
if (restricted) ACTION_UNRESTRICT else ACTION_RESTRICT
))
}
return options
}
private fun hasLeft(user: User): Boolean {
val leftUsers: List<User> = getLeftUsers().value ?: return false
return leftUsers.contains(user)
}
private fun isSelf(user: User): Boolean = user.pk == viewerId
private fun getString(@StringRes resId: Int): String {
return resources.getString(resId)
}
fun doAction(user: User?, action: String?): LiveData<Resource<Any?>>? {
return if (user == null || action == null) null else when (action) {
ACTION_KICK -> removeMember(user)
ACTION_MAKE_ADMIN -> makeAdmin(user)
ACTION_REMOVE_ADMIN -> removeAdmin(user)
ACTION_BLOCK -> blockUser(user)
ACTION_UNBLOCK -> unblockUser(user)
ACTION_RESTRICT -> restrictUser(user)
ACTION_UNRESTRICT -> unRestrictUser(user)
else -> null
}
}
fun getInviter(): LiveData<User?> = threadManager.inviter
companion object {
private const val ACTION_KICK = "kick"
private const val ACTION_MAKE_ADMIN = "make_admin"
private const val ACTION_REMOVE_ADMIN = "remove_admin"
private const val ACTION_BLOCK = "block"
private const val ACTION_UNBLOCK = "unblock"
// private static final String ACTION_REPORT = "report";
private const val ACTION_RESTRICT = "restrict"
private const val ACTION_UNRESTRICT = "unrestrict"
}
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!" }
}
}

13
app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt

@ -5,10 +5,7 @@ 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 androidx.lifecycle.*
import awais.instagrabber.customviews.emoji.Emoji
import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.managers.DirectMessagesManager.inboxManager
@ -50,13 +47,13 @@ class DirectThreadViewModel(
val items: LiveData<List<DirectItem>> by lazy {
Transformations.map(threadManager.items) { it.filter { thread -> thread.hideInThread == 0 } }
}
val isFetching: LiveData<Resource<Any?>> by lazy { threadManager.isFetching() }
val isFetching: LiveData<Resource<Any?>> by lazy { threadManager.fetching }
val users: LiveData<List<User>> by lazy { threadManager.users }
val leftUsers: LiveData<List<User>> by lazy { threadManager.leftUsers }
val pendingRequestsCount: LiveData<Int> by lazy { threadManager.pendingRequestsCount }
val inputMode: LiveData<Int> by lazy { threadManager.inputMode }
val isPending: LiveData<Boolean> by lazy { threadManager.isPending }
val replyToItem: LiveData<DirectItem?> by lazy { threadManager.getReplyToItem() }
val replyToItem: LiveData<DirectItem?> by lazy { threadManager.replyToItem }
fun moveFromPending() {
val messagesManager = DirectMessagesManager
@ -69,11 +66,11 @@ class DirectThreadViewModel(
}
fun fetchChats() {
threadManager.fetchChats()
threadManager.fetchChats(viewModelScope)
}
fun refreshChats() {
threadManager.refreshChats()
threadManager.refreshChats(viewModelScope)
}
fun sendText(text: String): LiveData<Resource<Any?>> {

506
app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt

@ -1,347 +1,329 @@
package awais.instagrabber.viewmodels;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import awais.instagrabber.R;
import awais.instagrabber.managers.DirectMessagesManager;
import awais.instagrabber.models.Resource;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Caption;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.MediaService;
import awais.instagrabber.webservices.ServiceCallback;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class PostViewV2ViewModel extends ViewModel {
private static final String TAG = PostViewV2ViewModel.class.getSimpleName();
private final MutableLiveData<User> user = new MutableLiveData<>();
private final MutableLiveData<Caption> caption = new MutableLiveData<>();
private final MutableLiveData<Location> location = new MutableLiveData<>();
private final MutableLiveData<String> date = new MutableLiveData<>();
private final MutableLiveData<Long> likeCount = new MutableLiveData<>(0L);
private final MutableLiveData<Long> commentCount = new MutableLiveData<>(0L);
private final MutableLiveData<Long> viewCount = new MutableLiveData<>(0L);
private final MutableLiveData<MediaItemType> type = new MutableLiveData<>();
private final MutableLiveData<Boolean> liked = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> saved = new MutableLiveData<>(false);
private final MutableLiveData<List<Integer>> options = new MutableLiveData<>(new ArrayList<>());
private final MediaService mediaService;
private final long viewerId;
private final boolean isLoggedIn;
private Media media;
private DirectMessagesManager messageManager;
public PostViewV2ViewModel() {
final String cookie = settingsHelper.getString(Constants.COOKIE);
final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID);
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
viewerId = CookieUtils.getUserIdFromCookie(cookie);
mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
}
public void setMedia(final Media media) {
this.media = media;
user.postValue(media.getUser());
caption.postValue(media.getCaption());
location.postValue(media.getLocation());
date.postValue(media.getDate());
likeCount.postValue(media.getLikeCount());
commentCount.postValue(media.getCommentCount());
viewCount.postValue(media.getMediaType() == MediaItemType.MEDIA_TYPE_VIDEO ? media.getViewCount() : null);
type.postValue(media.getMediaType());
liked.postValue(media.getHasLiked());
saved.postValue(media.getHasViewerSaved());
initOptions();
}
private void initOptions() {
final ImmutableList.Builder<Integer> builder = ImmutableList.builder();
if (isLoggedIn && media.getUser() != null && media.getUser().getPk() == viewerId) {
builder.add(R.id.edit_caption);
builder.add(R.id.delete);
package awais.instagrabber.viewmodels
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import awais.instagrabber.R
import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.models.Resource
import awais.instagrabber.models.Resource.Companion.error
import awais.instagrabber.models.Resource.Companion.loading
import awais.instagrabber.models.Resource.Companion.success
import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.repositories.responses.Caption
import awais.instagrabber.repositories.responses.Location
import awais.instagrabber.repositories.responses.Media
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.utils.getCsrfTokenFromCookie
import awais.instagrabber.utils.getUserIdFromCookie
import awais.instagrabber.webservices.MediaService
import awais.instagrabber.webservices.ServiceCallback
import com.google.common.collect.ImmutableList
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.*
class PostViewV2ViewModel : ViewModel() {
private val user = MutableLiveData<User?>()
private val caption = MutableLiveData<Caption?>()
private val location = MutableLiveData<Location?>()
private val date = MutableLiveData<String>()
private val likeCount = MutableLiveData(0L)
private val commentCount = MutableLiveData(0L)
private val viewCount = MutableLiveData(0L)
private val type = MutableLiveData<MediaItemType?>()
private val liked = MutableLiveData(false)
private val saved = MutableLiveData(false)
private val options = MutableLiveData<List<Int>>(ArrayList())
private val viewerId: Long
val isLoggedIn: Boolean
lateinit var media: Media
private set
private var mediaService: MediaService? = null
private var messageManager: DirectMessagesManager? = null
fun setMedia(media: Media) {
this.media = media
user.postValue(media.user)
caption.postValue(media.caption)
location.postValue(media.location)
date.postValue(media.date)
likeCount.postValue(media.likeCount)
commentCount.postValue(media.commentCount)
viewCount.postValue(if (media.mediaType == MediaItemType.MEDIA_TYPE_VIDEO) media.viewCount else null)
type.postValue(media.mediaType)
liked.postValue(media.hasLiked)
saved.postValue(media.hasViewerSaved)
initOptions()
}
private fun initOptions() {
val builder = ImmutableList.builder<Int>()
val user1 = media.user
if (isLoggedIn && user1 != null && user1.pk == viewerId) {
builder.add(R.id.edit_caption)
builder.add(R.id.delete)
}
options.postValue(builder.build());
}
public Media getMedia() {
return media;
}
public boolean isLoggedIn() {
return isLoggedIn;
options.postValue(builder.build())
}
public LiveData<User> getUser() {
return user;
fun getUser(): LiveData<User?> {
return user
}
public LiveData<Caption> getCaption() {
return caption;
fun getCaption(): LiveData<Caption?> {
return caption
}
public LiveData<Location> getLocation() {
return location;
fun getLocation(): LiveData<Location?> {
return location
}
public LiveData<String> getDate() {
return date;
fun getDate(): LiveData<String> {
return date
}
public LiveData<Long> getLikeCount() {
return likeCount;
fun getLikeCount(): LiveData<Long> {
return likeCount
}
public LiveData<Long> getCommentCount() {
return commentCount;
fun getCommentCount(): LiveData<Long> {
return commentCount
}
public LiveData<Long> getViewCount() {
return viewCount;
fun getViewCount(): LiveData<Long?> {
return viewCount
}
public LiveData<MediaItemType> getType() {
return type;
fun getType(): LiveData<MediaItemType?> {
return type
}
public LiveData<Boolean> getLiked() {
return liked;
fun getLiked(): LiveData<Boolean> {
return liked
}
public LiveData<Boolean> getSaved() {
return saved;
fun getSaved(): LiveData<Boolean> {
return saved
}
public LiveData<List<Integer>> getOptions() {
return options;
fun getOptions(): LiveData<List<Int>> {
return options
}
@NonNull
public LiveData<Resource<Object>> toggleLike() {
if (media.getHasLiked()) {
return unlike();
}
return like();
fun toggleLike(): LiveData<Resource<Any?>> {
return if (media.hasLiked) {
unlike()
} else like()
}
public LiveData<Resource<Object>> like() {
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>();
data.postValue(Resource.loading(null));
mediaService.like(media.getPk(), getLikeUnlikeCallback(data));
return data;
fun like(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
mediaService?.like(media.pk, getLikeUnlikeCallback(data))
return data
}
public LiveData<Resource<Object>> unlike() {
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>();
data.postValue(Resource.loading(null));
mediaService.unlike(media.getPk(), getLikeUnlikeCallback(data));
return data;
fun unlike(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
mediaService?.unlike(media.pk, getLikeUnlikeCallback(data))
return data
}
@NonNull
private ServiceCallback<Boolean> getLikeUnlikeCallback(final MutableLiveData<Resource<Object>> data) {
return new ServiceCallback<Boolean>() {
@Override
public void onSuccess(final Boolean result) {
if (!result) {
data.postValue(Resource.error("", null));
return;
private fun getLikeUnlikeCallback(data: MutableLiveData<Resource<Any?>>): ServiceCallback<Boolean?> {
return object : ServiceCallback<Boolean?> {
override fun onSuccess(result: Boolean?) {
if (result != null && !result) {
data.postValue(error("", null))
return
}
data.postValue(Resource.success(true));
final long currentLikesCount = media.getLikeCount();
final long updatedCount;
if (!media.getHasLiked()) {
updatedCount = currentLikesCount + 1;
media.setHasLiked(true);
data.postValue(success(true))
val currentLikesCount = media.likeCount
val updatedCount: Long
if (!media.hasLiked) {
updatedCount = currentLikesCount + 1
media.hasLiked = true
} else {
updatedCount = currentLikesCount - 1;
media.setHasLiked(false);
updatedCount = currentLikesCount - 1
media.hasLiked = false
}
media.setLikeCount(updatedCount);
likeCount.postValue(updatedCount);
liked.postValue(media.getHasLiked());
media.likeCount = updatedCount
likeCount.postValue(updatedCount)
liked.postValue(media.hasLiked)
}
@Override
public void onFailure(final Throwable t) {
data.postValue(Resource.error(t.getMessage(), null));
Log.e(TAG, "Error during like/unlike", t);
override fun onFailure(t: Throwable) {
data.postValue(error(t.message, null))
Log.e(TAG, "Error during like/unlike", t)
}
};
}
}
@NonNull
public LiveData<Resource<Object>> toggleSave() {
if (!media.getHasViewerSaved()) {
return save(null, false);
}
return unsave();
fun toggleSave(): LiveData<Resource<Any?>> {
return if (!media.hasViewerSaved) {
save(null, false)
} else unsave()
}
@NonNull
public LiveData<Resource<Object>> toggleSave(final String collection, final boolean ignoreSaveState) {
return save(collection, ignoreSaveState);
fun toggleSave(collection: String?, ignoreSaveState: Boolean): LiveData<Resource<Any?>> {
return save(collection, ignoreSaveState)
}
public LiveData<Resource<Object>> save(final String collection, final boolean ignoreSaveState) {
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>();
data.postValue(Resource.loading(null));
mediaService.save(media.getPk(), collection, getSaveUnsaveCallback(data, ignoreSaveState));
return data;
fun save(collection: String?, ignoreSaveState: Boolean): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
mediaService?.save(media.pk, collection, getSaveUnsaveCallback(data, ignoreSaveState))
return data
}
public LiveData<Resource<Object>> unsave() {
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>();
data.postValue(Resource.loading(null));
mediaService.unsave(media.getPk(), getSaveUnsaveCallback(data, false));
return data;
fun unsave(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
mediaService?.unsave(media.pk, getSaveUnsaveCallback(data, false))
return data
}
@NonNull
private ServiceCallback<Boolean> getSaveUnsaveCallback(final MutableLiveData<Resource<Object>> data,
final boolean ignoreSaveState) {
return new ServiceCallback<Boolean>() {
@Override
public void onSuccess(final Boolean result) {
if (!result) {
data.postValue(Resource.error("", null));
return;
private fun getSaveUnsaveCallback(
data: MutableLiveData<Resource<Any?>>,
ignoreSaveState: Boolean,
): ServiceCallback<Boolean?> {
return object : ServiceCallback<Boolean?> {
override fun onSuccess(result: Boolean?) {
if (result != null && !result) {
data.postValue(error("", null))
return
}
data.postValue(Resource.success(true));
if (!ignoreSaveState) media.setHasViewerSaved(!media.getHasViewerSaved());
saved.postValue(media.getHasViewerSaved());
data.postValue(success(true))
if (!ignoreSaveState) media.hasViewerSaved = !media.hasViewerSaved
saved.postValue(media.hasViewerSaved)
}
@Override
public void onFailure(final Throwable t) {
data.postValue(Resource.error(t.getMessage(), null));
Log.e(TAG, "Error during save/unsave", t);
override fun onFailure(t: Throwable) {
data.postValue(error(t.message, null))
Log.e(TAG, "Error during save/unsave", t)
}
};
}
}
public LiveData<Resource<Object>> updateCaption(final String caption) {
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>();
data.postValue(Resource.loading(null));
mediaService.editCaption(media.getPk(), caption, new ServiceCallback<Boolean>() {
@Override
public void onSuccess(final Boolean result) {
if (result) {
data.postValue(Resource.success(""));
media.setPostCaption(caption);
PostViewV2ViewModel.this.caption.postValue(media.getCaption());
return;
fun updateCaption(caption: String): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
mediaService?.editCaption(media.pk, caption, object : ServiceCallback<Boolean?> {
override fun onSuccess(result: Boolean?) {
if (result != null && result) {
data.postValue(success(""))
media.setPostCaption(caption)
this@PostViewV2ViewModel.caption.postValue(media.caption)
return
}
data.postValue(Resource.error("", null));
data.postValue(error("", null))
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error editing caption", t);
data.postValue(Resource.error(t.getMessage(), null));
override fun onFailure(t: Throwable) {
Log.e(TAG, "Error editing caption", t)
data.postValue(error(t.message, null))
}
});
return data;
}
public LiveData<Resource<String>> translateCaption() {
final MutableLiveData<Resource<String>> data = new MutableLiveData<>();
data.postValue(Resource.loading(null));
final Caption value = caption.getValue();
if (value == null) return data;
mediaService.translate(value.getPk(), "1", new ServiceCallback<String>() {
@Override
public void onSuccess(final String result) {
if (TextUtils.isEmpty(result)) {
data.postValue(Resource.error("", null));
return;
})
return data
}
fun translateCaption(): LiveData<Resource<String?>> {
val data = MutableLiveData<Resource<String?>>()
data.postValue(loading(null))
val value = caption.value ?: return data
mediaService?.translate(value.pk, "1", object : ServiceCallback<String?> {
override fun onSuccess(result: String?) {
if (result.isNullOrBlank()) {
data.postValue(error("", null))
return
}
data.postValue(Resource.success(result));
data.postValue(success(result))
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error translating comment", t);
data.postValue(Resource.error(t.getMessage(), null));
override fun onFailure(t: Throwable) {
Log.e(TAG, "Error translating comment", t)
data.postValue(error(t.message, null))
}
});
return data;
})
return data
}
public boolean hasPk() {
return media.getPk() != null;
fun hasPk(): Boolean {
return media.pk != null
}
public void setViewCount(final Long viewCount) {
this.viewCount.postValue(viewCount);
fun setViewCount(viewCount: Long?) {
this.viewCount.postValue(viewCount)
}
public LiveData<Resource<Object>> delete() {
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>();
data.postValue(Resource.loading(null));
final Call<String> request = mediaService.delete(media.getId(), media.getMediaType());
fun delete(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
val mediaId = media.id
val mediaType = media.mediaType
if (mediaId == null || mediaType == null) {
data.postValue(error("media id or type is null", null))
return data
}
val request = mediaService?.delete(mediaId, mediaType)
if (request == null) {
data.postValue(Resource.success(new Object()));
return data;
data.postValue(success(Any()))
return data
}
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
if (!response.isSuccessful()) {
data.postValue(Resource.error(R.string.generic_null_response, null));
return;
request.enqueue(object : Callback<String?> {
override fun onResponse(call: Call<String?>, response: Response<String?>) {
if (!response.isSuccessful) {
data.postValue(error(R.string.generic_null_response, null))
return
}
final String body = response.body();
val body = response.body()
if (body == null) {
data.postValue(Resource.error(R.string.generic_null_response, null));
return;
data.postValue(error(R.string.generic_null_response, null))
return
}
data.postValue(Resource.success(new Object()));
data.postValue(success(Any()))
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
Log.e(TAG, "onFailure: ", t);
data.postValue(Resource.error(t.getMessage(), null));
override fun onFailure(call: Call<String?>, t: Throwable) {
Log.e(TAG, "onFailure: ", t)
data.postValue(error(t.message, null))
}
});
return data;
})
return data
}
public void shareDm(@NonNull final RankedRecipient result) {
fun shareDm(result: RankedRecipient) {
if (messageManager == null) {
messageManager = DirectMessagesManager.INSTANCE;
messageManager = DirectMessagesManager
}
messageManager.sendMedia(result, media.getId());
val mediaId = media.id ?: return
messageManager?.sendMedia(result, mediaId, viewModelScope)
}
public void shareDm(@NonNull final Set<RankedRecipient> recipients) {
fun shareDm(recipients: Set<RankedRecipient>) {
if (messageManager == null) {
messageManager = DirectMessagesManager.INSTANCE;
messageManager = DirectMessagesManager
}
val mediaId = media.id ?: return
messageManager?.sendMedia(recipients, mediaId, viewModelScope)
}
init {
val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
val csrfToken: String? = getCsrfTokenFromCookie(cookie)
viewerId = getUserIdFromCookie(cookie)
isLoggedIn = cookie.isNotBlank() && viewerId != 0L
if (!csrfToken.isNullOrBlank()) {
mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId)
}
messageManager.sendMedia(recipients, media.getId());
}
}

10
app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.kt

@ -18,10 +18,10 @@ class DirectMessagesService private constructor(
) : BaseService() {
private val repository: DirectMessagesRepository = RetrofitFactory.retrofit.create(DirectMessagesRepository::class.java)
fun fetchInbox(
suspend fun fetchInbox(
cursor: String?,
seqId: Long,
): Call<DirectInboxResponse?> {
): DirectInboxResponse {
val queryMap = mutableMapOf(
"visual_message_return_type" to "unseen",
"thread_message_limit" to 10.toString(),
@ -38,10 +38,10 @@ class DirectMessagesService private constructor(
return repository.fetchInbox(queryMap)
}
fun fetchThread(
suspend fun fetchThread(
threadId: String,
cursor: String?,
): Call<DirectThreadFeedResponse?> {
): DirectThreadFeedResponse {
val queryMap = mutableMapOf(
"visual_message_return_type" to "unseen",
"limit" to 20.toString(),
@ -409,7 +409,7 @@ class DirectMessagesService private constructor(
return repository.end(threadId, form)
}
fun fetchPendingInbox(cursor: String?, seqId: Long): Call<DirectInboxResponse?> {
suspend fun fetchPendingInbox(cursor: String?, seqId: Long): DirectInboxResponse {
val queryMap = mutableMapOf(
"visual_message_return_type" to "unseen",
"thread_message_limit" to 20.toString(),

Loading…
Cancel
Save