diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt index 51d0954c..04678cf2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt @@ -3,9 +3,10 @@ package com.keylesspalace.tusky.components.scheduled import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.View import android.view.MenuItem import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.keylesspalace.tusky.BaseActivity @@ -14,8 +15,9 @@ import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.ScheduledStatus -import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.Status import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.uber.autodispose.AutoDispose.autoDisposable @@ -23,13 +25,8 @@ import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_scheduled_toot.* import kotlinx.android.synthetic.main.toolbar_basic.* -import okhttp3.ResponseBody -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response import javax.inject.Inject - class ScheduledTootActivity : BaseActivity(), ScheduledTootAction, Injectable { companion object { @@ -41,10 +38,12 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootAction, Injectable { lateinit var adapter: ScheduledTootAdapter - @Inject - lateinit var mastodonApi: MastodonApi @Inject lateinit var eventHub: EventHub + @Inject + lateinit var viewModelFactory: ViewModelFactory + + lateinit var viewModel: ScheduledTootViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -67,6 +66,8 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootAction, Injectable { adapter = ScheduledTootAdapter(this) scheduledTootList.adapter = adapter + viewModel = ViewModelProvider(this, viewModelFactory)[ScheduledTootViewModel::class.java] + loadStatuses() eventHub.events @@ -90,55 +91,42 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootAction, Injectable { } fun loadStatuses() { - progressBar.visibility = View.VISIBLE - mastodonApi.scheduledStatuses() - .enqueue(object : Callback> { - override fun onResponse(call: Call>, response: Response>) { - progressBar.visibility = View.GONE - if (response.body().isNullOrEmpty()) { - errorMessageView.show() - errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, - null) - } else { - show(response.body()!!) - } - } + viewModel.data.observe(this, Observer { + adapter.submitList(it) + }) - override fun onFailure(call: Call>, t: Throwable) { - progressBar.visibility = View.GONE - errorMessageView.show() - errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { - errorMessageView.hide() - loadStatuses() - } + viewModel.networkState.observe(this, Observer { (status) -> + when(status) { + Status.SUCCESS -> { + progressBar.hide() + swipeRefreshLayout.isRefreshing = false + errorMessageView.hide() + } + Status.RUNNING -> { + errorMessageView.hide() + if(viewModel.data.value?.loadedCount ?: 0 > 0) { + swipeRefreshLayout.isRefreshing = true + } else { + progressBar.show() } - }) + } + Status.FAILED -> { + if(viewModel.data.value?.loadedCount ?: 0 >= 0) { + progressBar.hide() + swipeRefreshLayout.isRefreshing = false + errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { + refreshStatuses() + } + errorMessageView.show() + } + } + } + + }) } private fun refreshStatuses() { - swipeRefreshLayout.isRefreshing = true - mastodonApi.scheduledStatuses() - .enqueue(object : Callback> { - override fun onResponse(call: Call>, response: Response>) { - swipeRefreshLayout.isRefreshing = false - if (response.body().isNullOrEmpty()) { - errorMessageView.show() - errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, - null) - } else { - show(response.body()!!) - } - } - - override fun onFailure(call: Call>, t: Throwable) { - swipeRefreshLayout.isRefreshing = false - } - }) - } - - fun show(statuses: List) { - adapter.setItems(statuses) - adapter.notifyDataSetChanged() + viewModel.reload() } override fun edit(position: Int, item: ScheduledStatus?) { @@ -162,15 +150,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootAction, Injectable { if (item == null) { return } - mastodonApi.deleteScheduledStatus(item.id) - .enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - adapter.removeItem(position) - } - override fun onFailure(call: Call, t: Throwable) { - - } - }) + viewModel.deleteScheduledStatus(item) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt index 4c07ea18..04a36c6e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt @@ -20,6 +20,8 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.TextView +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.ScheduledStatus @@ -31,9 +33,18 @@ interface ScheduledTootAction { class ScheduledTootAdapter( val listener: ScheduledTootAction -) : RecyclerView.Adapter() { +) : PagedListAdapter( + object: DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { + return oldItem.id == newItem.id + } - private var items: MutableList = mutableListOf() + override fun areContentsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { + return oldItem == newItem + } + + } +) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TootViewHolder { val view = LayoutInflater.from(parent.context) @@ -42,25 +53,12 @@ class ScheduledTootAdapter( } override fun onBindViewHolder(viewHolder: TootViewHolder, position: Int) { - viewHolder.bind(items[position]) - } - - override fun getItemCount() = items.size - - fun setItems(newItems: List) { - items = newItems.toMutableList() - notifyDataSetChanged() - } - - fun removeItem(position: Int): ScheduledStatus? { - if (position < 0 || position >= items.size) { - return null + getItem(position)?.let{ + viewHolder.bind(it) } - val toot = items.removeAt(position) - notifyItemRemoved(position) - return toot } + inner class TootViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val text: TextView = view.findViewById(R.id.text) @@ -70,15 +68,15 @@ class ScheduledTootAdapter( fun bind(item: ScheduledStatus) { edit.isEnabled = true delete.isEnabled = true - text.text = item.params.text - edit.setOnClickListener { v: View -> - v.isEnabled = false - listener.edit(adapterPosition, item) - } - delete.setOnClickListener { v: View -> - v.isEnabled = false - listener.delete(adapterPosition, item) - } + text.text = item.params.text + edit.setOnClickListener { v: View -> + v.isEnabled = false + listener.edit(adapterPosition, item) + } + delete.setOnClickListener { v: View -> + v.isEnabled = false + listener.delete(adapterPosition, item) + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt new file mode 100644 index 00000000..78f59084 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt @@ -0,0 +1,87 @@ +package com.keylesspalace.tusky.components.scheduled + +import android.util.Log +import androidx.lifecycle.MutableLiveData +import androidx.paging.DataSource +import androidx.paging.ItemKeyedDataSource +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.NetworkState +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.addTo + +class ScheduledTootDataSourceFactory( + private val mastodonApi: MastodonApi, + private val disposables: CompositeDisposable +): DataSource.Factory() { + + private val scheduledTootsCache = mutableListOf() + + private var dataSource: ScheduledTootDataSource? = null + + val networkState = MutableLiveData() + + override fun create(): DataSource { + return ScheduledTootDataSource(mastodonApi, disposables, scheduledTootsCache, networkState).also { + dataSource = it + } + } + + fun reload() { + scheduledTootsCache.clear() + dataSource?.invalidate() + } + + fun remove(status: ScheduledStatus) { + scheduledTootsCache.remove(status) + dataSource?.invalidate() + } + +} + + +class ScheduledTootDataSource( + private val mastodonApi: MastodonApi, + private val disposables: CompositeDisposable, + private val scheduledTootsCache: MutableList, + private val networkState: MutableLiveData +): ItemKeyedDataSource() { + override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + if(scheduledTootsCache.isNotEmpty()) { + callback.onResult(scheduledTootsCache.toList()) + } else { + networkState.postValue(NetworkState.LOADING) + mastodonApi.scheduledStatuses(limit = params.requestedLoadSize) + .subscribe({ newData -> + scheduledTootsCache.addAll(newData) + callback.onResult(newData) + networkState.postValue(NetworkState.LOADED) + }, { throwable -> + Log.w("ScheduledTootDataSource", "Error loading scheduled statuses", throwable) + networkState.postValue(NetworkState.error(throwable.message)) + }) + .addTo(disposables) + } + } + + override fun loadAfter(params: LoadParams, callback: LoadCallback) { + mastodonApi.scheduledStatuses(limit = params.requestedLoadSize, maxId = params.key) + .subscribe({ newData -> + scheduledTootsCache.addAll(newData) + callback.onResult(newData) + }, { throwable -> + Log.w("ScheduledTootDataSource", "Error loading scheduled statuses", throwable) + networkState.postValue(NetworkState.error(throwable.message)) + }) + .addTo(disposables) + } + + override fun loadBefore(params: LoadParams, callback: LoadCallback) { + // we are always loading from beginning to end + } + + override fun getKey(item: ScheduledStatus): String { + return item.id + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt new file mode 100644 index 00000000..97946de3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt @@ -0,0 +1,46 @@ +package com.keylesspalace.tusky.components.scheduled + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.paging.Config +import androidx.paging.toLiveData +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.network.MastodonApi +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.addTo +import javax.inject.Inject + +class ScheduledTootViewModel @Inject constructor( + val mastodonApi: MastodonApi +): ViewModel() { + + private val disposables = CompositeDisposable() + + private val dataSourceFactory = ScheduledTootDataSourceFactory(mastodonApi, disposables) + + val data = dataSourceFactory.toLiveData( + config = Config(pageSize = 20, initialLoadSizeHint = 20, enablePlaceholders = false) + ) + + val networkState = dataSourceFactory.networkState + + fun reload() { + dataSourceFactory.reload() + } + + fun deleteScheduledStatus(status: ScheduledStatus) { + mastodonApi.deleteScheduledStatus(status.id) + .subscribe({ + dataSourceFactory.remove(status) + },{ throwable -> + Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) + }) + .addTo(disposables) + + } + + override fun onCleared() { + disposables.clear() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index 26309be5..d7f81be7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -77,7 +77,7 @@ class NetworkModule { .apply { addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) if (BuildConfig.DEBUG) { - addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }) + addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) } } .build() diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt index 8381d526..f4929463 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.conversation.ConversationsViewModel import com.keylesspalace.tusky.components.report.ReportViewModel +import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel import com.keylesspalace.tusky.components.search.SearchViewModel import com.keylesspalace.tusky.viewmodel.AccountViewModel import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel @@ -79,5 +80,10 @@ abstract class ViewModelModule { @ViewModelKey(ComposeViewModel::class) internal abstract fun composeViewModel(viewModel: ComposeViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(ScheduledTootViewModel::class) + internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel + //Add more ViewModels here } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 65d096e8..da61cb6a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -201,12 +201,15 @@ interface MastodonApi { ): Single @GET("api/v1/scheduled_statuses") - fun scheduledStatuses(): Call> + fun scheduledStatuses( + @Query("limit") limit: Int? = null, + @Query("max_id") maxId: String? = null + ): Single> @DELETE("api/v1/scheduled_statuses/{id}") fun deleteScheduledStatus( @Path("id") scheduledStatusId: String - ): Call + ): Single @GET("api/v1/accounts/verify_credentials") fun accountVerifyCredentials(): Single diff --git a/app/src/main/res/layout/activity_scheduled_toot.xml b/app/src/main/res/layout/activity_scheduled_toot.xml index e0f1af0c..373a519a 100644 --- a/app/src/main/res/layout/activity_scheduled_toot.xml +++ b/app/src/main/res/layout/activity_scheduled_toot.xml @@ -23,6 +23,18 @@ app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + + + + - - - - - - \ No newline at end of file