Merge pull request #2015 from nextcloud/feature/noid/refactor-shared-items

Implement points from "Shared items" review
This commit is contained in:
Tim Krüger 2022-05-23 17:55:25 +02:00 committed by GitHub
commit 36cc5b93bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 984 additions and 580 deletions

View File

@ -169,7 +169,7 @@
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.SharedItemsActivity"
android:name=".shareditems.activities.SharedItemsActivity"
android:theme="@style/AppTheme"/>
<receiver android:name=".receivers.PackageReplacedReceiver">

View File

@ -1,105 +0,0 @@
package com.nextcloud.talk.adapters
import android.net.Uri
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.controller.BaseControllerListener
import com.facebook.drawee.controller.ControllerListener
import com.facebook.drawee.interfaces.DraweeController
import com.facebook.drawee.view.SimpleDraweeView
import com.facebook.imagepipeline.common.RotationOptions
import com.facebook.imagepipeline.image.ImageInfo
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.nextcloud.talk.databinding.SharedItemGridBinding
import com.nextcloud.talk.repositories.SharedItem
import com.nextcloud.talk.utils.DrawableUtils
import com.nextcloud.talk.utils.FileViewerUtils
class SharedItemsGridAdapter : RecyclerView.Adapter<SharedItemsGridAdapter.ViewHolder>() {
companion object {
private val TAG = SharedItemsGridAdapter::class.simpleName
}
class ViewHolder(val binding: SharedItemGridBinding, itemView: View) : RecyclerView.ViewHolder(itemView)
var authHeader: Map<String, String> = emptyMap()
var items: List<SharedItem> = emptyList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = SharedItemGridBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding, binding.root)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentItem = items[position]
if (currentItem.previewAvailable == true) {
val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(currentItem.previewLink))
.setProgressiveRenderingEnabled(true)
.setRotationOptions(RotationOptions.autoRotate())
.disableDiskCache()
.setHeaders(authHeader)
.build()
val listener: ControllerListener<ImageInfo?> = object : BaseControllerListener<ImageInfo?>() {
override fun onFailure(id: String, e: Throwable) {
Log.w(TAG, "Failed to load image. A static mimetype image will be used", e)
setStaticMimetypeImage(currentItem, holder)
}
}
val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
.setOldController(holder.binding.image.controller)
.setAutoPlayAnimations(true)
.setImageRequest(imageRequest)
.setControllerListener(listener)
.build()
holder.binding.image.controller = draweeController
} else {
setStaticMimetypeImage(currentItem, holder)
}
val fileViewerUtils = FileViewerUtils(holder.binding.image.context, currentItem.userEntity)
holder.binding.image.setOnClickListener {
fileViewerUtils.openFile(
FileViewerUtils.FileInfo(currentItem.id, currentItem.name, currentItem.fileSize),
currentItem.path,
currentItem.link,
currentItem.mimeType,
FileViewerUtils.ProgressUi(
holder.binding.progressBar,
null,
it as SimpleDraweeView
)
)
}
fileViewerUtils.resumeToUpdateViewsByProgress(
currentItem.name,
currentItem.id,
currentItem.mimeType,
FileViewerUtils.ProgressUi(holder.binding.progressBar, null, holder.binding.image)
)
}
private fun setStaticMimetypeImage(
currentItem: SharedItem,
holder: ViewHolder
) {
val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(currentItem.mimeType)
val drawable = ContextCompat.getDrawable(holder.binding.image.context, drawableResourceId)
holder.binding.image.hierarchy.setPlaceholderImage(drawable)
}
override fun getItemCount(): Int {
return items.size
}
}

View File

@ -1,119 +0,0 @@
package com.nextcloud.talk.adapters
import android.net.Uri
import android.text.format.Formatter
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.controller.BaseControllerListener
import com.facebook.drawee.controller.ControllerListener
import com.facebook.drawee.interfaces.DraweeController
import com.facebook.imagepipeline.common.RotationOptions
import com.facebook.imagepipeline.image.ImageInfo
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.nextcloud.talk.databinding.SharedItemListBinding
import com.nextcloud.talk.repositories.SharedItem
import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.DrawableUtils
import com.nextcloud.talk.utils.FileViewerUtils
import com.nextcloud.talk.utils.FileViewerUtils.ProgressUi
class SharedItemsListAdapter : RecyclerView.Adapter<SharedItemsListAdapter.ViewHolder>() {
companion object {
private val TAG = SharedItemsListAdapter::class.simpleName
private const val ONE_SECOND_IN_MILLIS = 1000
}
class ViewHolder(val binding: SharedItemListBinding, itemView: View) : RecyclerView.ViewHolder(itemView)
var authHeader: Map<String, String> = emptyMap()
var items: List<SharedItem> = emptyList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = SharedItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding, binding.root)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentItem = items[position]
holder.binding.fileName.text = currentItem.name
holder.binding.fileSize.text = currentItem.fileSize?.let {
Formatter.formatShortFileSize(
holder.binding.fileSize.context,
it
)
}
holder.binding.fileDate.text = DateUtils.getLocalDateTimeStringFromTimestamp(
currentItem.date * ONE_SECOND_IN_MILLIS
)
if (currentItem.previewAvailable == true) {
val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(currentItem.previewLink))
.setProgressiveRenderingEnabled(true)
.setRotationOptions(RotationOptions.autoRotate())
.disableDiskCache()
.setHeaders(authHeader)
.build()
val listener: ControllerListener<ImageInfo?> = object : BaseControllerListener<ImageInfo?>() {
override fun onFailure(id: String, e: Throwable) {
Log.w(TAG, "Failed to load image. A static mimetype image will be used", e)
setStaticMimetypeImage(currentItem, holder)
}
}
val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
.setOldController(holder.binding.fileImage.controller)
.setAutoPlayAnimations(true)
.setImageRequest(imageRequest)
.setControllerListener(listener)
.build()
holder.binding.fileImage.controller = draweeController
} else {
setStaticMimetypeImage(currentItem, holder)
}
val fileViewerUtils = FileViewerUtils(holder.binding.fileImage.context, currentItem.userEntity)
holder.binding.fileItem.setOnClickListener {
fileViewerUtils.openFile(
FileViewerUtils.FileInfo(currentItem.id, currentItem.name, currentItem.fileSize),
currentItem.path,
currentItem.link,
currentItem.mimeType,
ProgressUi(
holder.binding.progressBar,
null,
holder.binding.fileImage
)
)
}
fileViewerUtils.resumeToUpdateViewsByProgress(
currentItem.name,
currentItem.id,
currentItem.mimeType,
ProgressUi(holder.binding.progressBar, null, holder.binding.fileImage)
)
}
private fun setStaticMimetypeImage(
currentItem: SharedItem,
holder: ViewHolder
) {
val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(currentItem.mimeType)
val drawable = ContextCompat.getDrawable(holder.binding.fileImage.context, drawableResourceId)
holder.binding.fileImage.hierarchy.setPlaceholderImage(drawable)
}
override fun getItemCount(): Int {
return items.size
}
}

View File

@ -55,7 +55,9 @@ import com.nextcloud.talk.components.filebrowser.webdav.DavUtils
import com.nextcloud.talk.dagger.modules.BusModule
import com.nextcloud.talk.dagger.modules.ContextModule
import com.nextcloud.talk.dagger.modules.DatabaseModule
import com.nextcloud.talk.dagger.modules.RepositoryModule
import com.nextcloud.talk.dagger.modules.RestModule
import com.nextcloud.talk.dagger.modules.ViewModelModule
import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.jobs.CapabilitiesWorker
import com.nextcloud.talk.jobs.SignalingSettingsWorker
@ -89,7 +91,9 @@ import javax.inject.Singleton
DatabaseModule::class,
RestModule::class,
UserModule::class,
ArbitraryStorageModule::class
ArbitraryStorageModule::class,
ViewModelModule::class,
RepositoryModule::class
]
)
@Singleton

View File

@ -103,7 +103,7 @@ import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.CallActivity
import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.activities.SharedItemsActivity
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
import com.nextcloud.talk.activities.TakePhotoActivity
import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder

View File

@ -51,7 +51,7 @@ import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.facebook.drawee.backends.pipeline.Fresco
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.SharedItemsActivity
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
import com.nextcloud.talk.adapters.items.ParticipantItem
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication

View File

@ -0,0 +1,36 @@
/*
* Nextcloud Talk application
*
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.dagger.modules
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository
import com.nextcloud.talk.shareditems.repositories.SharedItemsRepositoryImpl
import dagger.Module
import dagger.Provides
@Module
class RepositoryModule {
@Provides
fun provideSharedItemsRepository(ncApi: NcApi): SharedItemsRepository {
return SharedItemsRepositoryImpl(ncApi)
}
}

View File

@ -0,0 +1,56 @@
/*
* Nextcloud Talk application
*
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.dagger.modules
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
import dagger.Binds
import dagger.MapKey
import dagger.Module
import dagger.multibindings.IntoMap
import javax.inject.Inject
import javax.inject.Provider
import kotlin.reflect.KClass
class ViewModelFactory @Inject constructor(
private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModels[modelClass]?.get() as T
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
@Module
abstract class ViewModelModule {
@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
@Binds
@IntoMap
@ViewModelKey(SharedItemsViewModel::class)
abstract fun sharedItemsViewModel(viewModel: SharedItemsViewModel): ViewModel
}

View File

@ -1,16 +0,0 @@
package com.nextcloud.talk.repositories
import com.nextcloud.talk.models.database.UserEntity
data class SharedItem(
val id: String,
val name: String,
val fileSize: Long?,
val date: Long,
val path: String,
val link: String?,
val mimeType: String?,
val previewAvailable: Boolean?,
val previewLink: String,
val userEntity: UserEntity,
)

View File

@ -1,18 +0,0 @@
package com.nextcloud.talk.repositories
import java.util.Locale
enum class SharedItemType {
AUDIO,
FILE,
MEDIA,
VOICE,
LOCATION,
DECKCARD,
OTHER;
companion object {
fun typeFor(name: String) = valueOf(name.uppercase(Locale.ROOT))
}
}

View File

@ -1,78 +0,0 @@
package com.nextcloud.talk.repositories
import autodagger.AutoInjector
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.chat.ChatShareOverall
import com.nextcloud.talk.models.json.chat.ChatShareOverviewOverall
import com.nextcloud.talk.utils.ApiUtils
import io.reactivex.Observable
import retrofit2.Response
import java.util.Locale
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class SharedItemsRepository {
var parameters: Parameters? = null
@Inject
lateinit var ncApi: NcApi
init {
sharedApplication!!.componentApplication.inject(this)
}
fun media(type: SharedItemType): Observable<Response<ChatShareOverall>>? {
return media(type, null)
}
fun media(type: SharedItemType, 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.toString().lowercase(Locale.ROOT),
lastKnownMessageId,
BATCH_SIZE
)
}
fun availableTypes(): Observable<Response<ChatShareOverviewOverall>>? {
val credentials = ApiUtils.getCredentials(parameters!!.userName, parameters!!.userToken)
return ncApi.getSharedItemsOverview(
credentials,
ApiUtils.getUrlForChatSharedItemsOverview(1, parameters!!.baseUrl, parameters!!.roomToken),
1
)
}
fun authHeader(): Map<String, String> {
return mapOf(Pair("Authorization", ApiUtils.getCredentials(parameters!!.userName, parameters!!.userToken)))
}
fun previewLink(fileId: String?): String {
return ApiUtils.getUrlForFilePreviewWithFileId(
parameters!!.baseUrl,
fileId,
sharedApplication!!.resources.getDimensionPixelSize(R.dimen.maximum_file_preview_size)
)
}
data class Parameters(
val userName: String,
val userToken: String,
val baseUrl: String,
val userEntity: UserEntity,
val roomToken: String
)
companion object {
const val BATCH_SIZE: Int = 28
}
}

View File

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

View File

@ -1,37 +1,64 @@
package com.nextcloud.talk.activities
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.shareditems.activities
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
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 autodagger.AutoInjector
import com.google.android.material.tabs.TabLayout
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.SharedItemsGridAdapter
import com.nextcloud.talk.adapters.SharedItemsListAdapter
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.ActivitySharedItemsBinding
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.repositories.SharedItemType
import com.nextcloud.talk.shareditems.adapters.SharedItemsAdapter
import com.nextcloud.talk.shareditems.model.SharedItemType
import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
import com.nextcloud.talk.viewmodels.SharedItemsViewModel
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class SharedItemsActivity : AppCompatActivity() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var binding: ActivitySharedItemsBinding
private lateinit var viewModel: SharedItemsViewModel
private lateinit var currentTab: SharedItemType
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
currentTab = SharedItemType.MEDIA
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
val roomToken = intent.getStringExtra(KEY_ROOM_TOKEN)!!
val conversationName = intent.getStringExtra(KEY_CONVERSATION_NAME)
@ -55,35 +82,37 @@ class SharedItemsActivity : AppCompatActivity() {
supportActionBar?.title = conversationName
supportActionBar?.setDisplayHomeAsUpEnabled(true)
viewModel = ViewModelProvider(
this,
SharedItemsViewModel.Factory(userEntity, roomToken, currentTab)
).get(SharedItemsViewModel::class.java)
viewModel = ViewModelProvider(this, viewModelFactory)[SharedItemsViewModel::class.java]
viewModel.sharedItemType.observe(this) {
initTabs(it)
}
viewModel.viewState.observe(this) { state ->
clearEmptyLoading()
when (state) {
is SharedItemsViewModel.LoadingItemsState, SharedItemsViewModel.InitialState -> {
showLoading()
}
is SharedItemsViewModel.NoSharedItemsState -> {
showEmpty()
}
is SharedItemsViewModel.LoadedState -> {
val sharedMediaItems = state.items
Log.d(TAG, "Items received: $sharedMediaItems")
viewModel.sharedItems.observe(this) {
Log.d(TAG, "Items received: $it")
val showGrid = state.selectedType == SharedItemType.MEDIA
val layoutManager = if (showGrid) {
GridLayoutManager(this, SPAN_COUNT)
} else {
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
}
if (currentTab == SharedItemType.MEDIA) {
val adapter = SharedItemsGridAdapter()
adapter.items = it.items
adapter.authHeader = it.authHeader
binding.imageRecycler.adapter = adapter
val layoutManager = GridLayoutManager(this, SPAN_COUNT)
binding.imageRecycler.layoutManager = layoutManager
} else {
val adapter = SharedItemsListAdapter()
adapter.items = it.items
adapter.authHeader = it.authHeader
binding.imageRecycler.adapter = adapter
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = LinearLayoutManager.VERTICAL
binding.imageRecycler.layoutManager = layoutManager
val adapter = SharedItemsAdapter(showGrid, userEntity).apply {
items = sharedMediaItems.items
}
binding.imageRecycler.adapter = adapter
binding.imageRecycler.layoutManager = layoutManager
}
is SharedItemsViewModel.TypesLoadedState -> {
initTabs(state.types)
}
}
}
@ -95,15 +124,30 @@ class SharedItemsActivity : AppCompatActivity() {
}
}
})
viewModel.initialize(userEntity, roomToken)
}
fun updateItems(type: SharedItemType) {
currentTab = type
viewModel.loadItems(type)
private fun clearEmptyLoading() {
binding.sharedItemsTabs.visibility = View.VISIBLE
binding.emptyContainer.emptyListView.visibility = View.GONE
}
private fun showLoading() {
binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.file_list_loading)
binding.emptyContainer.emptyListView.visibility = View.VISIBLE
}
private fun showEmpty() {
binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.nc_shared_items_empty)
binding.emptyContainer.emptyListView.visibility = View.VISIBLE
binding.sharedItemsTabs.visibility = View.GONE
}
private fun initTabs(sharedItemTypes: Set<SharedItemType>) {
binding.sharedItemsTabs.removeAllTabs()
if (sharedItemTypes.contains(SharedItemType.MEDIA)) {
val tabMedia: TabLayout.Tab = binding.sharedItemsTabs.newTab()
tabMedia.tag = SharedItemType.MEDIA
@ -155,7 +199,7 @@ class SharedItemsActivity : AppCompatActivity() {
binding.sharedItemsTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
updateItems(tab.tag as SharedItemType)
viewModel.initialLoadItems(tab.tag as SharedItemType)
}
override fun onTabUnselected(tab: TabLayout.Tab) = Unit

View File

@ -0,0 +1,70 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.shareditems.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.talk.databinding.SharedItemGridBinding
import com.nextcloud.talk.databinding.SharedItemListBinding
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.shareditems.model.SharedItem
class SharedItemsAdapter(
private val showGrid: Boolean,
private val userEntity: UserEntity
) : RecyclerView.Adapter<SharedItemsViewHolder>() {
var items: List<SharedItem> = emptyList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SharedItemsViewHolder {
return if (showGrid) {
SharedItemsGridViewHolder(
SharedItemGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
userEntity
)
} else {
SharedItemsListViewHolder(
SharedItemListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
userEntity
)
}
}
override fun onBindViewHolder(holder: SharedItemsViewHolder, position: Int) {
holder.onBind(items[position])
}
override fun getItemCount(): Int {
return items.size
}
}

View File

@ -0,0 +1,42 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.shareditems.adapters
import android.view.View
import android.widget.ProgressBar
import com.facebook.drawee.view.SimpleDraweeView
import com.nextcloud.talk.databinding.SharedItemGridBinding
import com.nextcloud.talk.models.database.UserEntity
class SharedItemsGridViewHolder(
override val binding: SharedItemGridBinding,
userEntity: UserEntity
) : SharedItemsViewHolder(binding, userEntity) {
override val image: SimpleDraweeView
get() = binding.image
override val clickTarget: View
get() = binding.image
override val progressBar: ProgressBar
get() = binding.progressBar
}

View File

@ -0,0 +1,65 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.shareditems.adapters
import android.text.format.Formatter
import android.view.View
import android.widget.ProgressBar
import com.facebook.drawee.view.SimpleDraweeView
import com.nextcloud.talk.databinding.SharedItemListBinding
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.shareditems.model.SharedItem
import com.nextcloud.talk.utils.DateUtils
class SharedItemsListViewHolder(
override val binding: SharedItemListBinding,
userEntity: UserEntity
) : SharedItemsViewHolder(binding, userEntity) {
override val image: SimpleDraweeView
get() = binding.fileImage
override val clickTarget: View
get() = binding.fileItem
override val progressBar: ProgressBar
get() = binding.progressBar
override fun onBind(item: SharedItem) {
super.onBind(item)
binding.fileName.text = item.name
binding.fileSize.text = item.fileSize?.let {
Formatter.formatShortFileSize(
binding.fileSize.context,
it
)
}
binding.fileDate.text = DateUtils.getLocalDateTimeStringFromTimestamp(
item.date * ONE_SECOND_IN_MILLIS
)
}
companion object {
private const val ONE_SECOND_IN_MILLIS = 1000
}
}

View File

@ -0,0 +1,133 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.shareditems.adapters
import android.graphics.drawable.Drawable
import android.net.Uri
import android.util.Log
import android.view.View
import android.widget.ProgressBar
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.controller.BaseControllerListener
import com.facebook.drawee.controller.ControllerListener
import com.facebook.drawee.interfaces.DraweeController
import com.facebook.drawee.view.SimpleDraweeView
import com.facebook.imagepipeline.common.RotationOptions
import com.facebook.imagepipeline.image.ImageInfo
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.shareditems.model.SharedItem
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DrawableUtils
import com.nextcloud.talk.utils.FileViewerUtils
abstract class SharedItemsViewHolder(
open val binding: ViewBinding,
private val userEntity: UserEntity
) : RecyclerView.ViewHolder(binding.root) {
companion object {
private val TAG = SharedItemsViewHolder::class.simpleName
}
abstract val image: SimpleDraweeView
abstract val clickTarget: View
abstract val progressBar: ProgressBar
private val authHeader = mapOf(
Pair(
"Authorization",
ApiUtils.getCredentials(userEntity.username, userEntity.token)
)
)
open fun onBind(item: SharedItem) {
image.hierarchy.setPlaceholderImage(staticImage(item.mimeType, image))
if (item.previewAvailable == true) {
image.controller = configurePreview(item)
}
/*
The FileViewerUtils forces us to do things at this points which should be done separated in the activity and
the view model.
This should be done after a refactoring of FileViewerUtils.
*/
val fileViewerUtils = FileViewerUtils(image.context, userEntity)
clickTarget.setOnClickListener {
fileViewerUtils.openFile(
FileViewerUtils.FileInfo(item.id, item.name, item.fileSize),
item.path,
item.link,
item.mimeType,
FileViewerUtils.ProgressUi(
progressBar,
null,
image
)
)
}
fileViewerUtils.resumeToUpdateViewsByProgress(
item.name,
item.id,
item.mimeType,
FileViewerUtils.ProgressUi(progressBar, null, image)
)
}
private fun configurePreview(item: SharedItem): DraweeController {
val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(item.previewLink))
.setProgressiveRenderingEnabled(true)
.setRotationOptions(RotationOptions.autoRotate())
.disableDiskCache()
.setHeaders(authHeader)
.build()
val listener: ControllerListener<ImageInfo?> = object : BaseControllerListener<ImageInfo?>() {
override fun onFailure(id: String, e: Throwable) {
Log.w(TAG, "Failed to load image. A static mimetype image will be used", e)
}
}
return Fresco.newDraweeControllerBuilder()
.setOldController(image.controller)
.setAutoPlayAnimations(true)
.setImageRequest(imageRequest)
.setControllerListener(listener)
.build()
}
private fun staticImage(
mimeType: String?,
image: SimpleDraweeView
): Drawable {
val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(mimeType)
return ContextCompat.getDrawable(image.context, drawableResourceId)!!
}
}

View File

@ -0,0 +1,35 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.shareditems.model
data class SharedItem(
val id: String,
val name: String,
val fileSize: Long?,
val date: Long,
val path: String,
val link: String?,
val mimeType: String?,
val previewAvailable: Boolean?,
val previewLink: String
)

View File

@ -0,0 +1,40 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.shareditems.model
import java.util.Locale
enum class SharedItemType {
AUDIO,
FILE,
MEDIA,
VOICE,
LOCATION,
DECKCARD,
OTHER;
companion object {
fun typeFor(name: String) = valueOf(name.uppercase(Locale.ROOT))
}
}

View File

@ -0,0 +1,29 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.shareditems.model
class SharedMediaItems(
val items: List<SharedItem>,
var lastSeenId: Int?,
var moreItemsExisting: Boolean
)

View File

@ -0,0 +1,47 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.shareditems.repositories
import com.nextcloud.talk.shareditems.model.SharedItemType
import com.nextcloud.talk.shareditems.model.SharedMediaItems
import io.reactivex.Observable
interface SharedItemsRepository {
fun media(parameters: Parameters, type: SharedItemType): Observable<SharedMediaItems>?
fun media(
parameters: Parameters,
type: SharedItemType,
lastKnownMessageId: Int?
): Observable<SharedMediaItems>?
fun availableTypes(parameters: Parameters): Observable<Set<SharedItemType>>
data class Parameters(
val userName: String,
val userToken: String,
val baseUrl: String,
val roomToken: String
)
}

View File

@ -0,0 +1,148 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.shareditems.repositories
import android.util.Log
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.models.json.chat.ChatShareOverall
import com.nextcloud.talk.shareditems.model.SharedItem
import com.nextcloud.talk.shareditems.model.SharedItemType
import com.nextcloud.talk.shareditems.model.SharedMediaItems
import com.nextcloud.talk.utils.ApiUtils
import io.reactivex.Observable
import retrofit2.Response
import java.util.Locale
import javax.inject.Inject
class SharedItemsRepositoryImpl @Inject constructor(private val ncApi: NcApi) : SharedItemsRepository {
override fun media(
parameters: SharedItemsRepository.Parameters,
type: SharedItemType
): Observable<SharedMediaItems>? {
return media(parameters, type, null)
}
override fun media(
parameters: SharedItemsRepository.Parameters,
type: SharedItemType,
lastKnownMessageId: Int?
): Observable<SharedMediaItems>? {
val credentials = ApiUtils.getCredentials(parameters.userName, parameters.userToken)
return ncApi.getSharedItems(
credentials,
ApiUtils.getUrlForChatSharedItems(1, parameters.baseUrl, parameters.roomToken),
type.toString().lowercase(Locale.ROOT),
lastKnownMessageId,
BATCH_SIZE
).map { map(it, parameters) }
}
private fun map(
response: Response<ChatShareOverall>,
parameters: SharedItemsRepository.Parameters
): SharedMediaItems {
var chatLastGiven: Int? = null
val items = mutableMapOf<String, SharedItem>()
if (response.headers()["x-chat-last-given"] != null) {
chatLastGiven = response.headers()["x-chat-last-given"]!!.toInt()
}
val mediaItems = response.body()!!.ocs!!.data
if (mediaItems != null) {
for (it in mediaItems) {
if (it.value.messageParameters?.containsKey("file") == true) {
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"]!!.toLong(),
it.value.timestamp,
fileParameters["path"]!!,
fileParameters["link"]!!,
fileParameters["mimetype"]!!,
previewAvailable,
previewLink(fileParameters["id"], parameters.baseUrl)
)
} else {
Log.w(TAG, "location and deckcard are not yet supported")
}
}
}
val sortedMutableItems = items.toSortedMap().values.toList().reversed().toMutableList()
val moreItemsExisting = items.count() == BATCH_SIZE
return SharedMediaItems(
sortedMutableItems,
chatLastGiven,
moreItemsExisting
)
}
override fun availableTypes(parameters: SharedItemsRepository.Parameters): Observable<Set<SharedItemType>> {
val credentials = ApiUtils.getCredentials(parameters.userName, parameters.userToken)
return ncApi.getSharedItemsOverview(
credentials,
ApiUtils.getUrlForChatSharedItemsOverview(1, parameters.baseUrl, parameters.roomToken),
1
).map {
val types = mutableSetOf<SharedItemType>()
val typeMap = it.body()!!.ocs!!.data!!
for (t in typeMap) {
if (t.value.isNotEmpty()) {
try {
types += SharedItemType.typeFor(t.key)
} catch (e: IllegalArgumentException) {
Log.w(TAG, "Server responds an unknown shared item type: ${t.key}")
}
}
}
types.toSet()
}
}
private fun previewLink(fileId: String?, baseUrl: String): String {
return ApiUtils.getUrlForFilePreviewWithFileId(
baseUrl,
fileId,
sharedApplication!!.resources.getDimensionPixelSize(R.dimen.maximum_file_preview_size)
)
}
companion object {
const val BATCH_SIZE: Int = 28
private val TAG = SharedItemsRepositoryImpl::class.simpleName
}
}

View File

@ -0,0 +1,179 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.shareditems.viewmodels
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.shareditems.model.SharedItemType
import com.nextcloud.talk.shareditems.model.SharedMediaItems
import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
class SharedItemsViewModel @Inject constructor(
private val repository: SharedItemsRepository
) :
ViewModel() {
private lateinit var repositoryParameters: SharedItemsRepository.Parameters
sealed interface ViewState
object InitialState : ViewState
object NoSharedItemsState : ViewState
open class TypesLoadedState(val types: Set<SharedItemType>, val selectedType: SharedItemType) : ViewState
class LoadingItemsState(types: Set<SharedItemType>, selectedType: SharedItemType) :
TypesLoadedState(types, selectedType)
class LoadedState(types: Set<SharedItemType>, selectedType: SharedItemType, val items: SharedMediaItems) :
TypesLoadedState(types, selectedType)
private val _viewState: MutableLiveData<ViewState> = MutableLiveData(InitialState)
val viewState: LiveData<ViewState>
get() = _viewState
fun initialize(userEntity: UserEntity, roomToken: String) {
repositoryParameters = SharedItemsRepository.Parameters(
userEntity.userId,
userEntity.token,
userEntity.baseUrl,
roomToken
)
loadAvailableTypes()
}
private fun loadAvailableTypes() {
repository.availableTypes(repositoryParameters).subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<Set<SharedItemType>> {
var types: Set<SharedItemType>? = null
override fun onSubscribe(d: Disposable) = Unit
override fun onNext(types: Set<SharedItemType>) {
this.types = types
}
override fun onError(e: Throwable) {
Log.d(TAG, "An error occurred: $e")
}
override fun onComplete() {
val newTypes = this.types
if (newTypes.isNullOrEmpty()) {
this@SharedItemsViewModel._viewState.value = NoSharedItemsState
} else {
val selectedType = chooseInitialType(newTypes)
this@SharedItemsViewModel._viewState.value =
TypesLoadedState(newTypes, selectedType)
initialLoadItems(selectedType)
}
}
})
}
private fun chooseInitialType(newTypes: Set<SharedItemType>): SharedItemType = when {
newTypes.contains(SharedItemType.MEDIA) -> SharedItemType.MEDIA
else -> newTypes.toList().first()
}
fun initialLoadItems(type: SharedItemType) {
val state = _viewState.value
if (state is TypesLoadedState) {
_viewState.value = LoadingItemsState(state.types, type)
repository.media(repositoryParameters, type)?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(SharedMediaItemsObserver())
}
}
fun loadNextItems() {
when (val currentState = _viewState.value) {
is LoadedState -> {
val currentSharedItems = currentState.items
if (currentSharedItems.moreItemsExisting) {
repository.media(repositoryParameters, currentState.selectedType, currentSharedItems.lastSeenId)
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(SharedMediaItemsObserver())
}
}
else -> return
}
}
inner class SharedMediaItemsObserver : Observer<SharedMediaItems> {
var newSharedItems: SharedMediaItems? = null
override fun onSubscribe(d: Disposable) = Unit
override fun onNext(response: SharedMediaItems) {
newSharedItems = response
}
override fun onError(e: Throwable) {
Log.d(TAG, "An error occurred: $e")
}
override fun onComplete() {
val items = newSharedItems!!
val state = this@SharedItemsViewModel._viewState.value
if (state is LoadedState) {
val oldItems = state.items.items
val newItems =
SharedMediaItems(
oldItems + newSharedItems!!.items,
newSharedItems!!.lastSeenId,
newSharedItems!!.moreItemsExisting
)
setCurrentState(newItems)
} else {
setCurrentState(items)
}
}
private fun setCurrentState(items: SharedMediaItems) {
when (val state = this@SharedItemsViewModel._viewState.value) {
is TypesLoadedState -> {
this@SharedItemsViewModel._viewState.value = LoadedState(
state.types,
state.selectedType,
items
)
}
else -> return
}
}
}
companion object {
private val TAG = SharedItemsViewModel::class.simpleName
}
}

View File

@ -52,6 +52,12 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID
import java.io.File
import java.util.concurrent.ExecutionException
/*
Usage of this class forces us to do things at one location which should be separated in a activity and view model.
Example:
- SharedItemsViewHolder
*/
class FileViewerUtils(private val context: Context, private val userEntity: UserEntity) {
fun openFile(

View File

@ -1,192 +0,0 @@
package com.nextcloud.talk.viewmodels
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.chat.ChatShareOverall
import com.nextcloud.talk.models.json.chat.ChatShareOverviewOverall
import com.nextcloud.talk.repositories.SharedItem
import com.nextcloud.talk.repositories.SharedItemType
import com.nextcloud.talk.repositories.SharedItemsRepository
import com.nextcloud.talk.repositories.SharedMediaItems
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import retrofit2.Response
class SharedItemsViewModel(private val repository: SharedItemsRepository, private val initialType: SharedItemType) :
ViewModel() {
private val _sharedItemType: MutableLiveData<Set<SharedItemType>> by lazy {
MutableLiveData<Set<SharedItemType>>().also {
availableTypes()
}
}
private val _sharedItems: MutableLiveData<SharedMediaItems> by lazy {
MutableLiveData<SharedMediaItems>().also {
loadItems(initialType)
}
}
val sharedItemType: LiveData<Set<SharedItemType>>
get() = _sharedItemType
val sharedItems: LiveData<SharedMediaItems>
get() = _sharedItems
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: SharedItemType) {
repository.media(type)?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(observer(type, true))
}
private fun observer(type: SharedItemType, initModel: Boolean): Observer<Response<ChatShareOverall>> {
return object : Observer<Response<ChatShareOverall>> {
var chatLastGiven: Int? = null
val items = mutableMapOf<String, SharedItem>()
override fun onSubscribe(d: Disposable) = Unit
override fun onNext(response: Response<ChatShareOverall>) {
if (response.headers()["x-chat-last-given"] != null) {
chatLastGiven = response.headers()["x-chat-last-given"]!!.toInt()
}
val mediaItems = response.body()!!.ocs!!.data
if (mediaItems != null) {
for (it in mediaItems) {
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"]?.toLong(),
it.value.timestamp,
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() == BATCH_SIZE
if (initModel) {
this@SharedItemsViewModel._sharedItems.value =
SharedMediaItems(
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()
)
}
}
}
}
private fun availableTypes() {
repository.availableTypes()?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<Response<ChatShareOverviewOverall>> {
val types = mutableSetOf<SharedItemType>()
override fun onSubscribe(d: Disposable) = Unit
override fun onNext(response: Response<ChatShareOverviewOverall>) {
val typeMap = response.body()!!.ocs!!.data!!
for (it in typeMap) {
if (it.value.size > 0) {
try {
types += SharedItemType.typeFor(it.key)
} catch (e: IllegalArgumentException) {
Log.w(TAG, "Server responds an unknown shared item type: ${it.key}")
}
}
}
}
override fun onError(e: Throwable) {
Log.d(TAG, "An error occurred: $e")
}
override fun onComplete() {
this@SharedItemsViewModel._sharedItemType.value = types
}
})
}
class Factory(val userEntity: UserEntity, val roomToken: String, private val initialType: SharedItemType) :
ViewModelProvider
.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SharedItemsViewModel::class.java)) {
val repository = SharedItemsRepository()
repository.parameters = SharedItemsRepository.Parameters(
userEntity.userId,
userEntity.token,
userEntity.baseUrl,
userEntity,
roomToken
)
return SharedItemsViewModel(repository, initialType) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
companion object {
private val TAG = SharedItemsViewModel::class.simpleName
const val BATCH_SIZE: Int = 28
}
}

View File

@ -25,7 +25,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_default"
tools:context=".activities.SharedItemsActivity">
tools:context=".shareditems.activities.SharedItemsActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/shared_items_appbar"
@ -56,6 +56,12 @@
</com.google.android.material.appbar.AppBarLayout>
<include
android:id="@+id/emptyContainer"
layout="@layout/empty_list"
android:visibility="gone"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/image_recycler"
android:layout_width="match_parent"

View File

@ -423,6 +423,7 @@
<!-- shared items -->
<string name="nc_shared_items">Shared items</string>
<string name="nc_shared_items_description">Images, files, voice messages…</string>
<string name="nc_shared_items_empty">No shared items</string>
<!-- voice messages -->
<string name="nc_voice_message_filename">Talk recording from %1$s (%2$s)</string>