Browse Source

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

Implement points from "Shared items" review
Tim Krüger 3 years ago
parent
commit
36cc5b93bc
27 changed files with 986 additions and 582 deletions
  1. 1 1
      app/src/main/AndroidManifest.xml
  2. 0 105
      app/src/main/java/com/nextcloud/talk/adapters/SharedItemsGridAdapter.kt
  3. 0 119
      app/src/main/java/com/nextcloud/talk/adapters/SharedItemsListAdapter.kt
  4. 5 1
      app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
  5. 1 1
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  6. 1 1
      app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt
  7. 36 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt
  8. 56 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
  9. 0 16
      app/src/main/java/com/nextcloud/talk/repositories/SharedItem.kt
  10. 0 18
      app/src/main/java/com/nextcloud/talk/repositories/SharedItemType.kt
  11. 0 78
      app/src/main/java/com/nextcloud/talk/repositories/SharedItemsRepository.kt
  12. 0 9
      app/src/main/java/com/nextcloud/talk/repositories/SharedMediaItems.kt
  13. 84 40
      app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt
  14. 70 0
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt
  15. 42 0
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt
  16. 65 0
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt
  17. 133 0
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt
  18. 35 0
      app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItem.kt
  19. 40 0
      app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItemType.kt
  20. 29 0
      app/src/main/java/com/nextcloud/talk/shareditems/model/SharedMediaItems.kt
  21. 47 0
      app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt
  22. 148 0
      app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt
  23. 179 0
      app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt
  24. 6 0
      app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
  25. 0 192
      app/src/main/java/com/nextcloud/talk/viewmodels/SharedItemsViewModel.kt
  26. 7 1
      app/src/main/res/layout/activity_shared_items.xml
  27. 1 0
      app/src/main/res/values/strings.xml

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -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">

+ 0 - 105
app/src/main/java/com/nextcloud/talk/adapters/SharedItemsGridAdapter.kt

@@ -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
-    }
-}

+ 0 - 119
app/src/main/java/com/nextcloud/talk/adapters/SharedItemsListAdapter.kt

@@ -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
-    }
-}

+ 5 - 1
app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt

@@ -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

+ 1 - 1
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -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

+ 1 - 1
app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt

@@ -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

+ 36 - 0
app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt

@@ -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)
+    }
+}

+ 56 - 0
app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt

@@ -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
+}

+ 0 - 16
app/src/main/java/com/nextcloud/talk/repositories/SharedItem.kt

@@ -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,
-)

+ 0 - 18
app/src/main/java/com/nextcloud/talk/repositories/SharedItemType.kt

@@ -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))
-    }
-}

+ 0 - 78
app/src/main/java/com/nextcloud/talk/repositories/SharedItemsRepository.kt

@@ -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
-    }
-}

+ 0 - 9
app/src/main/java/com/nextcloud/talk/repositories/SharedMediaItems.kt

@@ -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>
-)

+ 84 - 40
app/src/main/java/com/nextcloud/talk/activities/SharedItemsActivity.kt → app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt

@@ -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.sharedItemType.observe(this) {
-            initTabs(it)
-        }
+        viewModel = ViewModelProvider(this, viewModelFactory)[SharedItemsViewModel::class.java]
 
-        viewModel.sharedItems.observe(this) {
-            Log.d(TAG, "Items received: $it")
-
-            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
+        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")
+
+                    val showGrid = state.selectedType == SharedItemType.MEDIA
+                    val layoutManager = if (showGrid) {
+                        GridLayoutManager(this, SPAN_COUNT)
+                    } else {
+                        LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
+                    }
+
+                    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)
+    }
+
+    private fun clearEmptyLoading() {
+        binding.sharedItemsTabs.visibility = View.VISIBLE
+        binding.emptyContainer.emptyListView.visibility = View.GONE
     }
 
-    fun updateItems(type: SharedItemType) {
-        currentTab = type
-        viewModel.loadItems(type)
+    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

+ 70 - 0
app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt

@@ -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
+    }
+}

+ 42 - 0
app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt

@@ -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
+}

+ 65 - 0
app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt

@@ -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
+    }
+}

+ 133 - 0
app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt

@@ -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)!!
+    }
+}

+ 35 - 0
app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItem.kt

@@ -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
+)

+ 40 - 0
app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItemType.kt

@@ -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))
+    }
+}

+ 29 - 0
app/src/main/java/com/nextcloud/talk/shareditems/model/SharedMediaItems.kt

@@ -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
+)

+ 47 - 0
app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt

@@ -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
+    )
+}

+ 148 - 0
app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt

@@ -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
+    }
+}

+ 179 - 0
app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt

@@ -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
+    }
+}

+ 6 - 0
app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt

@@ -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(

+ 0 - 192
app/src/main/java/com/nextcloud/talk/viewmodels/SharedItemsViewModel.kt

@@ -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
-    }
-}

+ 7 - 1
app/src/main/res/layout/activity_shared_items.xml

@@ -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"

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -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>