Load shared items while scrolling

Signed-off-by: Tim Krüger <t@timkrueger.me>
This commit is contained in:
Tim Krüger 2022-04-28 17:09:26 +02:00
parent d92f5546e9
commit a322a2ad73
No known key found for this signature in database
GPG key ID: FECE3A7222C52A4E
6 changed files with 148 additions and 106 deletions

View file

@ -8,6 +8,7 @@ import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.tabs.TabLayout
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.SharedItemsAdapter
@ -60,7 +61,7 @@ class SharedItemsActivity : AppCompatActivity() {
SharedItemsViewModel.Factory(userEntity, roomToken, currentTab)
).get(SharedItemsViewModel::class.java)
viewModel.media.observe(this) {
viewModel.sharedItems.observe(this) {
Log.d(TAG, "Items received: $it")
if (currentTab == TAB_MEDIA) {
@ -71,8 +72,6 @@ class SharedItemsActivity : AppCompatActivity() {
val layoutManager = GridLayoutManager(this, 4)
binding.imageRecycler.layoutManager = layoutManager
adapter.notifyDataSetChanged()
} else {
val adapter = SharedItemsListAdapter()
adapter.items = it.items
@ -82,15 +81,22 @@ class SharedItemsActivity : AppCompatActivity() {
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = LinearLayoutManager.VERTICAL
binding.imageRecycler.layoutManager = layoutManager
adapter.notifyDataSetChanged()
}
}
binding.imageRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (!recyclerView.canScrollVertically(1) && newState == RecyclerView.SCROLL_STATE_IDLE) {
viewModel.loadNextItems()
}
}
})
}
fun updateItems(type: String) {
currentTab = type
viewModel.loadMediaItems(type)
viewModel.loadItems(type)
}
private fun initTabs() {
@ -134,13 +140,9 @@ class SharedItemsActivity : AppCompatActivity() {
updateItems(tab.tag as String)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
// unused atm
}
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
override fun onTabReselected(tab: TabLayout.Tab) {
// unused atm
}
override fun onTabReselected(tab: TabLayout.Tab) = Unit
})
}

View file

@ -29,12 +29,16 @@ class SharedItemsRepository {
}
fun media(type: String): Observable<Response<ChatShareOverall>>? {
return media(type, null)
}
fun media(type: String, lastKnownMessageId: Int?): Observable<Response<ChatShareOverall>>? {
val credentials = ApiUtils.getCredentials(parameters!!.userName, parameters!!.userToken)
return ncApi.getSharedItems(
credentials,
ApiUtils.getUrlForChatSharedItems(1, parameters!!.baseUrl, parameters!!.roomToken),
type, null, null
type, lastKnownMessageId, 28
)
}

View file

@ -1,7 +1,9 @@
package com.nextcloud.talk.repositories
class SharedMediaItems(
val items: List<SharedItem>,
val lastSeenId: String,
val type: String,
val items: MutableList<SharedItem>,
var lastSeenId: Int?,
var moreItemsExisting: Boolean,
val authHeader: Map<String, String>
)

View file

@ -16,76 +16,109 @@ import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import retrofit2.Response
class SharedItemsViewModel(private val repository: SharedItemsRepository, private val initialType: String) : ViewModel() {
class SharedItemsViewModel(private val repository: SharedItemsRepository, private val initialType: String) :
ViewModel() {
private val _media: MutableLiveData<SharedMediaItems> by lazy {
private val _sharedItems: MutableLiveData<SharedMediaItems> by lazy {
MutableLiveData<SharedMediaItems>().also {
loadMediaItems(initialType)
loadItems(initialType)
}
}
val media: LiveData<SharedMediaItems>
get() = _media
val sharedItems: LiveData<SharedMediaItems>
get() = _sharedItems
fun loadMediaItems(type: String) {
fun loadNextItems() {
val currentSharedItems = sharedItems.value!!
if (currentSharedItems.moreItemsExisting) {
repository.media(currentSharedItems.type, currentSharedItems.lastSeenId)?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(observer(currentSharedItems.type, false))
}
}
fun loadItems(type: String) {
repository.media(type)?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<Response<ChatShareOverall>> {
?.subscribe(observer(type, true))
}
var chatLastGiven: String = ""
val items = mutableMapOf<String, SharedItem>()
private fun observer(type: String, initModel: Boolean): Observer<Response<ChatShareOverall>> {
return object : Observer<Response<ChatShareOverall>> {
override fun onSubscribe(d: Disposable) = Unit
var chatLastGiven: Int? = null
val items = mutableMapOf<String, SharedItem>()
override fun onNext(response: Response<ChatShareOverall>) {
override fun onSubscribe(d: Disposable) = Unit
if (response.headers()["x-chat-last-given"] != null) {
chatLastGiven = response.headers()["x-chat-last-given"]!!
}
override fun onNext(response: Response<ChatShareOverall>) {
val mediaItems = response.body()!!.ocs!!.data
mediaItems?.forEach {
if (it.value.messageParameters.containsKey("file")) {
val fileParameters = it.value.messageParameters["file"]!!
val previewAvailable = "yes".equals(fileParameters["preview-available"]!!, ignoreCase = true)
items[it.value.id] = SharedItem(
fileParameters["id"]!!,
fileParameters["name"]!!,
fileParameters["size"]!!.toInt(),
fileParameters["path"]!!,
fileParameters["link"]!!,
fileParameters["mimetype"]!!,
previewAvailable,
repository.previewLink(fileParameters["id"]),
repository.parameters!!.userEntity
)
} else {
Log.w(TAG, "location and deckcard are not yet supported")
}
}
if (response.headers()["x-chat-last-given"] != null) {
chatLastGiven = response.headers()["x-chat-last-given"]!!.toInt()
}
override fun onError(e: Throwable) {
Log.d(TAG, "An error occurred: $e")
}
val mediaItems = response.body()!!.ocs!!.data
mediaItems?.forEach {
if (it.value.messageParameters.containsKey("file")) {
val fileParameters = it.value.messageParameters["file"]!!
override fun onComplete() {
this@SharedItemsViewModel._media.value =
val previewAvailable = "yes".equals(fileParameters["preview-available"]!!, ignoreCase = true)
items[it.value.id] = SharedItem(
fileParameters["id"]!!,
fileParameters["name"]!!,
fileParameters["size"]!!.toInt(),
fileParameters["path"]!!,
fileParameters["link"]!!,
fileParameters["mimetype"]!!,
previewAvailable,
repository.previewLink(fileParameters["id"]),
repository.parameters!!.userEntity
)
} else {
Log.w(TAG, "location and deckcard are not yet supported")
}
}
}
override fun onError(e: Throwable) {
Log.d(TAG, "An error occurred: $e")
}
override fun onComplete() {
val sortedMutableItems = items.toSortedMap().values.toList().reversed().toMutableList()
val moreItemsExisting = items.count() == 28
if (initModel) {
this@SharedItemsViewModel._sharedItems.value =
SharedMediaItems(
items.toSortedMap().values.toList().reversed(),
type,
sortedMutableItems,
chatLastGiven,
moreItemsExisting,
repository.authHeader()
)
} else {
val oldItems = this@SharedItemsViewModel._sharedItems.value!!.items
this@SharedItemsViewModel._sharedItems.value =
SharedMediaItems(
type,
(oldItems.toMutableList() + sortedMutableItems) as MutableList<SharedItem>,
chatLastGiven,
moreItemsExisting,
repository.authHeader()
)
}
})
}
}
}
class Factory(val userEntity: UserEntity, val roomToken: String, private val initialType: String) : ViewModelProvider
.Factory {
class Factory(val userEntity: UserEntity, val roomToken: String, private val initialType: String) :
ViewModelProvider
.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SharedItemsViewModel::class.java)) {

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Tim Krüger
@ -34,12 +33,12 @@
android:layout_height="?attr/actionBarSize"
android:background="@color/appbar"
android:theme="?attr/actionBarPopupTheme"
app:layout_constraintTop_toTopOf="parent"
app:layout_scrollFlags="enterAlwaysCollapsed|noScroll"
app:navigationIconTint="@color/fontAppbar"
app:popupTheme="@style/appActionBarPopupMenu"
app:titleTextColor="@color/fontAppbar"
tools:title="@string/nc_app_product_name"
app:layout_constraintTop_toTopOf="parent" />
tools:title="@string/nc_app_product_name" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/shared_items_tabs"
@ -54,18 +53,15 @@
app:tabMode="scrollable"
app:tabTextAppearance="@style/TextAppearanceTab" />
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/shared_items_tabs">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/image_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/attachment_item" />
</androidx.core.widget.NestedScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/image_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/shared_items_tabs"
tools:listitem="@layout/attachment_item" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -20,40 +20,45 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/preview_container"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:adjustViewBounds="true"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true">
android:layout_height="wrap_content">
<com.facebook.drawee.view.SimpleDraweeView
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:id="@+id/image"
<FrameLayout
android:id="@+id/preview_container"
android:layout_width="match_parent"
android:layout_height="100dp"
android:padding="4dp"
android:src="@drawable/ic_mimetype_file"
android:layout_height="0dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:adjustViewBounds="true"
app:layout_alignSelf="flex_start"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:placeholderImageScaleType="fitCenter"
fresco:actualImageScaleType="centerCrop"
fresco:failureImage="@drawable/ic_mimetype_file"
fresco:placeholderImage="@drawable/ic_mimetype_file"
fresco:roundedCornerRadius="4dp"
tools:src="@drawable/ic_call_black_24dp"/>
app:layout_flexGrow="1"
app:layout_wrapBefore="true">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp"
android:src="@drawable/ic_mimetype_file"
app:placeholderImageScaleType="fitCenter"
fresco:actualImageScaleType="centerCrop"
fresco:failureImage="@drawable/ic_mimetype_file"
fresco:placeholderImage="@drawable/ic_mimetype_file"
fresco:roundedCornerRadius="4dp" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>