diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e2f11f867..41ae4ce7f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -169,7 +169,7 @@ android:windowSoftInputMode="stateHidden" /> diff --git a/app/src/main/java/com/nextcloud/talk/adapters/SharedItemsGridAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/SharedItemsGridAdapter.kt deleted file mode 100644 index 240842254..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/SharedItemsGridAdapter.kt +++ /dev/null @@ -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() { - - companion object { - private val TAG = SharedItemsGridAdapter::class.simpleName - } - - class ViewHolder(val binding: SharedItemGridBinding, itemView: View) : RecyclerView.ViewHolder(itemView) - - var authHeader: Map = emptyMap() - var items: List = 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 = object : BaseControllerListener() { - 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 - } -} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/SharedItemsListAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/SharedItemsListAdapter.kt deleted file mode 100644 index 56a084b21..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/SharedItemsListAdapter.kt +++ /dev/null @@ -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() { - - 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 = emptyMap() - var items: List = 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 = object : BaseControllerListener() { - 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 - } -} diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt index b11c8a60b..dc304b8db 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index e4714ea08..cfba72955 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt index bc6c25a1e..7068af0c5 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt new file mode 100644 index 000000000..50da11477 --- /dev/null +++ b/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 . + */ + +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) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt new file mode 100644 index 000000000..d684db1b2 --- /dev/null +++ b/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 . + */ + +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, Provider> +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): 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) + +@Module +abstract class ViewModelModule { + + @Binds + abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory + + @Binds + @IntoMap + @ViewModelKey(SharedItemsViewModel::class) + abstract fun sharedItemsViewModel(viewModel: SharedItemsViewModel): ViewModel +} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/SharedItem.kt b/app/src/main/java/com/nextcloud/talk/repositories/SharedItem.kt deleted file mode 100644 index 38aeef303..000000000 --- a/app/src/main/java/com/nextcloud/talk/repositories/SharedItem.kt +++ /dev/null @@ -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, -) diff --git a/app/src/main/java/com/nextcloud/talk/repositories/SharedItemType.kt b/app/src/main/java/com/nextcloud/talk/repositories/SharedItemType.kt deleted file mode 100644 index e263ae8cc..000000000 --- a/app/src/main/java/com/nextcloud/talk/repositories/SharedItemType.kt +++ /dev/null @@ -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)) - } -} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/SharedItemsRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/SharedItemsRepository.kt deleted file mode 100644 index f34cd5497..000000000 --- a/app/src/main/java/com/nextcloud/talk/repositories/SharedItemsRepository.kt +++ /dev/null @@ -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>? { - return media(type, null) - } - - fun media(type: SharedItemType, lastKnownMessageId: Int?): Observable>? { - 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>? { - val credentials = ApiUtils.getCredentials(parameters!!.userName, parameters!!.userToken) - - return ncApi.getSharedItemsOverview( - credentials, - ApiUtils.getUrlForChatSharedItemsOverview(1, parameters!!.baseUrl, parameters!!.roomToken), - 1 - ) - } - - fun authHeader(): Map { - 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 - } -} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/SharedMediaItems.kt b/app/src/main/java/com/nextcloud/talk/repositories/SharedMediaItems.kt deleted file mode 100644 index 91f5fcf10..000000000 --- a/app/src/main/java/com/nextcloud/talk/repositories/SharedMediaItems.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.nextcloud.talk.repositories - -class SharedMediaItems( - val type: SharedItemType, - val items: MutableList, - var lastSeenId: Int?, - var moreItemsExisting: Boolean, - val authHeader: Map -) diff --git a/app/src/main/java/com/nextcloud/talk/activities/SharedItemsActivity.kt b/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt similarity index 59% rename from app/src/main/java/com/nextcloud/talk/activities/SharedItemsActivity.kt rename to app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt index 76fb75600..a24a3d864 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/SharedItemsActivity.kt +++ b/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 + * + * 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 . + */ + +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) { + 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 diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt new file mode 100644 index 000000000..63cab9b81 --- /dev/null +++ b/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 + * + * 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 . + */ + +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() { + + var items: List = 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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt new file mode 100644 index 000000000..b7bf7be6c --- /dev/null +++ b/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 + * + * 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 . + */ + +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 +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt new file mode 100644 index 000000000..7f5e4cbe7 --- /dev/null +++ b/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 + * + * 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 . + */ + +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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt new file mode 100644 index 000000000..397750dd8 --- /dev/null +++ b/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 + * + * 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 . + */ + +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 = object : BaseControllerListener() { + 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)!! + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItem.kt b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItem.kt new file mode 100644 index 000000000..3a7033fe4 --- /dev/null +++ b/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 + * + * 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 . + */ + +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 +) diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItemType.kt b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItemType.kt new file mode 100644 index 000000000..caedd4c02 --- /dev/null +++ b/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 + * + * 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 . + */ + +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)) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedMediaItems.kt b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedMediaItems.kt new file mode 100644 index 000000000..ef891ffc7 --- /dev/null +++ b/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 + * + * 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 . + */ + +package com.nextcloud.talk.shareditems.model + +class SharedMediaItems( + val items: List, + var lastSeenId: Int?, + var moreItemsExisting: Boolean +) diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt new file mode 100644 index 000000000..a668d672d --- /dev/null +++ b/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 + * + * 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 . + */ + +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? + + fun media( + parameters: Parameters, + type: SharedItemType, + lastKnownMessageId: Int? + ): Observable? + + fun availableTypes(parameters: Parameters): Observable> + + data class Parameters( + val userName: String, + val userToken: String, + val baseUrl: String, + val roomToken: String + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt new file mode 100644 index 000000000..22745b088 --- /dev/null +++ b/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 + * + * 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 . + */ + +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? { + return media(parameters, type, null) + } + + override fun media( + parameters: SharedItemsRepository.Parameters, + type: SharedItemType, + lastKnownMessageId: Int? + ): Observable? { + 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, + parameters: SharedItemsRepository.Parameters + ): SharedMediaItems { + + var chatLastGiven: Int? = null + val items = mutableMapOf() + + 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> { + val credentials = ApiUtils.getCredentials(parameters.userName, parameters.userToken) + + return ncApi.getSharedItemsOverview( + credentials, + ApiUtils.getUrlForChatSharedItemsOverview(1, parameters.baseUrl, parameters.roomToken), + 1 + ).map { + val types = mutableSetOf() + 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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt b/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt new file mode 100644 index 000000000..e26b5cbe6 --- /dev/null +++ b/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 + * + * 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 . + */ + +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, val selectedType: SharedItemType) : ViewState + class LoadingItemsState(types: Set, selectedType: SharedItemType) : + TypesLoadedState(types, selectedType) + + class LoadedState(types: Set, selectedType: SharedItemType, val items: SharedMediaItems) : + TypesLoadedState(types, selectedType) + + private val _viewState: MutableLiveData = MutableLiveData(InitialState) + val viewState: LiveData + 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> { + + var types: Set? = null + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(types: Set) { + 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 = 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 { + + 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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt index f3eaf5b34..03c7fca0f 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt +++ b/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( diff --git a/app/src/main/java/com/nextcloud/talk/viewmodels/SharedItemsViewModel.kt b/app/src/main/java/com/nextcloud/talk/viewmodels/SharedItemsViewModel.kt deleted file mode 100644 index 03210d6cf..000000000 --- a/app/src/main/java/com/nextcloud/talk/viewmodels/SharedItemsViewModel.kt +++ /dev/null @@ -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> by lazy { - MutableLiveData>().also { - availableTypes() - } - } - - private val _sharedItems: MutableLiveData by lazy { - MutableLiveData().also { - loadItems(initialType) - } - } - - val sharedItemType: LiveData> - get() = _sharedItemType - - val sharedItems: LiveData - 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> { - return object : Observer> { - - var chatLastGiven: Int? = null - val items = mutableMapOf() - - override fun onSubscribe(d: Disposable) = Unit - - override fun onNext(response: Response) { - - 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, - chatLastGiven, - moreItemsExisting, - repository.authHeader() - ) - } - } - } - } - - private fun availableTypes() { - repository.availableTypes()?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer> { - - val types = mutableSetOf() - - override fun onSubscribe(d: Disposable) = Unit - - override fun onNext(response: Response) { - 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 create(modelClass: Class): 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 - } -} diff --git a/app/src/main/res/layout/activity_shared_items.xml b/app/src/main/res/layout/activity_shared_items.xml index 5b2b2f6b5..54243b293 100644 --- a/app/src/main/res/layout/activity_shared_items.xml +++ b/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"> + + Shared items Images, files, voice messages… + No shared items Talk recording from %1$s (%2$s)