diff --git a/app/build.gradle b/app/build.gradle
index 4fa05db26..b8bbb4df1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -332,6 +332,10 @@ dependencies {
gplayImplementation 'com.google.android.gms:play-services-base:18.0.1'
gplayImplementation "com.google.firebase:firebase-messaging:23.0.0"
+
+ // TODO: Define variable for version
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
+ // implementation 'androidx.activity:activity-ktx:1.4.0'
}
task installGitHooks(type: Copy, group: "development") {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index abbf6b73e..aaaeb9430 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -96,6 +96,11 @@
android:name="android.max_aspect"
android:value="10" />
+
+
(KEY_USER_ENTITY)!!
+
+ binding = ActivitySharedItemsBinding.inflate(layoutInflater)
+ setSupportActionBar(binding.sharedItemsToolbar)
+ setContentView(binding.root)
+
+ DisplayUtils.applyColorToStatusBar(
+ this,
+ ResourcesCompat.getColor(
+ resources, R.color.appbar, null
+ )
+ )
+ DisplayUtils.applyColorToNavigationBar(
+ this.window,
+ ResourcesCompat.getColor(resources, R.color.bg_default, null)
+ )
+
+ supportActionBar?.title = conversationName
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ initTabs()
+
+ viewModel = ViewModelProvider(
+ this,
+ SharedItemsViewModel.Factory(userEntity, roomToken, currentTab)
+ ).get(SharedItemsViewModel::class.java)
+
+ viewModel.sharedItems.observe(this) {
+ Log.d(TAG, "Items received: $it")
+
+ if (currentTab == TAB_MEDIA) {
+ val adapter = SharedItemsAdapter()
+ adapter.items = it.items
+ adapter.authHeader = it.authHeader
+ binding.imageRecycler.adapter = adapter
+
+ val layoutManager = GridLayoutManager(this, 4)
+ 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
+ }
+ }
+
+ binding.imageRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ super.onScrollStateChanged(recyclerView, newState)
+ if (!recyclerView.canScrollVertically(1) && newState == RecyclerView.SCROLL_STATE_IDLE) {
+ viewModel.loadNextItems()
+ }
+ }
+ })
+ }
+
+ fun updateItems(type: String) {
+ currentTab = type
+ viewModel.loadItems(type)
+ }
+
+ private fun initTabs() {
+ val tabMedia: TabLayout.Tab = binding.sharedItemsTabs.newTab()
+ tabMedia.tag = TAB_MEDIA
+ tabMedia.setText(R.string.shared_items_media)
+ binding.sharedItemsTabs.addTab(tabMedia)
+
+ val tabFile: TabLayout.Tab = binding.sharedItemsTabs.newTab()
+ tabFile.tag = TAB_FILE
+ tabFile.setText(R.string.shared_items_file)
+ binding.sharedItemsTabs.addTab(tabFile)
+
+ val tabAudio: TabLayout.Tab = binding.sharedItemsTabs.newTab()
+ tabAudio.tag = TAB_AUDIO
+ tabAudio.setText(R.string.shared_items_audio)
+ binding.sharedItemsTabs.addTab(tabAudio)
+
+ val tabVoice: TabLayout.Tab = binding.sharedItemsTabs.newTab()
+ tabVoice.tag = TAB_VOICE
+ tabVoice.setText(R.string.shared_items_voice)
+ binding.sharedItemsTabs.addTab(tabVoice)
+
+ // val tabLocation: TabLayout.Tab = binding.sharedItemsTabs.newTab()
+ // tabLocation.tag = TAB_LOCATION
+ // tabLocation.text = "location"
+ // binding.sharedItemsTabs.addTab(tabLocation)
+
+ // val tabDeckCard: TabLayout.Tab = binding.sharedItemsTabs.newTab()
+ // tabDeckCard.tag = TAB_DECKCARD
+ // tabDeckCard.text = "deckcard"
+ // binding.sharedItemsTabs.addTab(tabDeckCard)
+
+ val tabOther: TabLayout.Tab = binding.sharedItemsTabs.newTab()
+ tabOther.tag = TAB_OTHER
+ tabOther.setText(R.string.shared_items_other)
+ binding.sharedItemsTabs.addTab(tabOther)
+
+ binding.sharedItemsTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+ override fun onTabSelected(tab: TabLayout.Tab) {
+ updateItems(tab.tag as String)
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab) = Unit
+
+ override fun onTabReselected(tab: TabLayout.Tab) = Unit
+ })
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return if (item.itemId == android.R.id.home) {
+ onBackPressed()
+ true
+ } else {
+ super.onOptionsItemSelected(item)
+ }
+ }
+
+ companion object {
+ private val TAG = SharedItemsActivity::class.simpleName
+ const val TAB_AUDIO = "audio"
+ const val TAB_FILE = "file"
+ const val TAB_MEDIA = "media"
+ const val TAB_VOICE = "voice"
+ const val TAB_LOCATION = "location"
+ const val TAB_DECKCARD = "deckcard"
+ const val TAB_OTHER = "other"
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/SharedItemsAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/SharedItemsAdapter.kt
new file mode 100644
index 000000000..c18ec7b61
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/SharedItemsAdapter.kt
@@ -0,0 +1,93 @@
+package com.nextcloud.talk.adapters
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.facebook.drawee.interfaces.DraweeController
+import com.facebook.drawee.view.SimpleDraweeView
+import com.facebook.imagepipeline.common.RotationOptions
+import com.facebook.imagepipeline.request.ImageRequestBuilder
+import com.nextcloud.talk.R
+import com.nextcloud.talk.databinding.AttachmentItemBinding
+import com.nextcloud.talk.repositories.SharedItem
+import com.nextcloud.talk.utils.FileViewerUtils
+
+class SharedItemsAdapter : RecyclerView.Adapter() {
+
+ companion object {
+ private val TAG = SharedItemsAdapter::class.simpleName
+ }
+
+ class ViewHolder(val binding: AttachmentItemBinding, itemView: View) : RecyclerView.ViewHolder(itemView)
+
+ var authHeader: Map = emptyMap()
+ var items: List = emptyList()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val binding = AttachmentItemBinding.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) {
+ val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(currentItem.previewLink))
+ .setProgressiveRenderingEnabled(true)
+ .setRotationOptions(RotationOptions.autoRotate())
+ .disableDiskCache()
+ .setHeaders(authHeader)
+ .build()
+
+ val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
+ .setOldController(holder.binding.image.controller)
+ .setAutoPlayAnimations(true)
+ .setImageRequest(imageRequest)
+ .build()
+ holder.binding.image.controller = draweeController
+ } else {
+ when (currentItem.mimeType) {
+ "video/mp4",
+ "video/quicktime",
+ "video/ogg"
+ -> holder.binding.image.setImageResource(R.drawable.ic_mimetype_video)
+ "audio/mpeg",
+ "audio/wav",
+ "audio/ogg",
+ -> holder.binding.image.setImageResource(R.drawable.ic_mimetype_audio)
+ "image/png",
+ "image/jpeg",
+ "image/gif"
+ -> holder.binding.image.setImageResource(R.drawable.ic_mimetype_image)
+ "text/markdown",
+ "text/plain"
+ -> holder.binding.image.setImageResource(R.drawable.ic_mimetype_text)
+ else
+ -> holder.binding.image.setImageResource(R.drawable.ic_mimetype_file)
+ }
+ }
+ holder.binding.image.setOnClickListener {
+ val fileViewerUtils = FileViewerUtils(it.context, currentItem.userEntity)
+
+ fileViewerUtils.openFile(
+ currentItem.id,
+ currentItem.name,
+ currentItem.fileSize,
+ currentItem.path,
+ currentItem.link,
+ currentItem.mimeType,
+ holder.binding.progressBar,
+ null,
+ it as SimpleDraweeView
+ )
+ }
+ }
+
+ 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
new file mode 100644
index 000000000..7768d59bb
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/SharedItemsListAdapter.kt
@@ -0,0 +1,94 @@
+package com.nextcloud.talk.adapters
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.facebook.drawee.interfaces.DraweeController
+import com.facebook.imagepipeline.common.RotationOptions
+import com.facebook.imagepipeline.request.ImageRequestBuilder
+import com.nextcloud.talk.R
+import com.nextcloud.talk.databinding.AttachmentListItemBinding
+import com.nextcloud.talk.repositories.SharedItem
+import com.nextcloud.talk.utils.FileViewerUtils
+
+class SharedItemsListAdapter : RecyclerView.Adapter() {
+
+ companion object {
+ private val TAG = SharedItemsListAdapter::class.simpleName
+ }
+
+ class ViewHolder(val binding: AttachmentListItemBinding, itemView: View) : RecyclerView.ViewHolder(itemView)
+
+ var authHeader: Map = emptyMap()
+ var items: List = emptyList()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val binding = AttachmentListItemBinding.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
+
+ if (currentItem.previewAvailable) {
+ val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(currentItem.previewLink))
+ .setProgressiveRenderingEnabled(true)
+ .setRotationOptions(RotationOptions.autoRotate())
+ .disableDiskCache()
+ .setHeaders(authHeader)
+ .build()
+
+ val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
+ .setOldController(holder.binding.fileImage.controller)
+ .setAutoPlayAnimations(true)
+ .setImageRequest(imageRequest)
+ .build()
+ holder.binding.fileImage.controller = draweeController
+ } else {
+ when (currentItem.mimeType) {
+ "video/mp4",
+ "video/quicktime",
+ "video/ogg"
+ -> holder.binding.fileImage.setImageResource(R.drawable.ic_mimetype_video)
+ "audio/mpeg",
+ "audio/wav",
+ "audio/ogg",
+ -> holder.binding.fileImage.setImageResource(R.drawable.ic_mimetype_audio)
+ "image/png",
+ "image/jpeg",
+ "image/gif"
+ -> holder.binding.fileImage.setImageResource(R.drawable.ic_mimetype_image)
+ "text/markdown",
+ "text/plain"
+ -> holder.binding.fileImage.setImageResource(R.drawable.ic_mimetype_text)
+ else
+ -> holder.binding.fileImage.setImageResource(R.drawable.ic_mimetype_file)
+ }
+ }
+ holder.binding.fileItem.setOnClickListener {
+ val fileViewerUtils = FileViewerUtils(it.context, currentItem.userEntity)
+
+ fileViewerUtils.openFile(
+ currentItem.id,
+ currentItem.name,
+ currentItem.fileSize,
+ currentItem.path,
+ currentItem.link,
+ currentItem.mimeType,
+ holder.binding.progressBar,
+ null,
+ holder.binding.fileImage
+ )
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return items.size
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
index 36d05ed3f..9d7d40b40 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
@@ -27,13 +27,11 @@
package com.nextcloud.talk.adapters.messages;
import android.annotation.SuppressLint;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
-import android.os.Build;
import android.os.Handler;
import android.util.Base64;
import android.util.Log;
@@ -43,44 +41,31 @@ import android.widget.PopupMenu;
import android.widget.ProgressBar;
import com.facebook.drawee.view.SimpleDraweeView;
-import com.google.common.util.concurrent.ListenableFuture;
import com.nextcloud.talk.R;
-import com.nextcloud.talk.activities.FullScreenImageActivity;
-import com.nextcloud.talk.activities.FullScreenMediaActivity;
-import com.nextcloud.talk.activities.FullScreenTextViewerActivity;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.components.filebrowser.models.BrowserFile;
import com.nextcloud.talk.components.filebrowser.models.DavResponse;
import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
-import com.nextcloud.talk.jobs.DownloadFileToCacheWorker;
-import com.nextcloud.talk.models.database.CapabilitiesUtil;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.models.json.chat.ChatMessage;
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet;
-import com.nextcloud.talk.utils.AccountUtils;
import com.nextcloud.talk.utils.DisplayUtils;
import com.nextcloud.talk.utils.DrawableUtils;
-import com.nextcloud.talk.utils.bundle.BundleKeys;
+import com.nextcloud.talk.utils.FileViewerUtils;
import com.stfalcon.chatkit.messages.MessageHolders;
import java.io.ByteArrayInputStream;
-import java.io.File;
import java.io.IOException;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.content.ContextCompat;
-import androidx.core.content.FileProvider;
import androidx.emoji.widget.EmojiTextView;
-import androidx.work.Data;
-import androidx.work.OneTimeWorkRequest;
-import androidx.work.WorkInfo;
-import androidx.work.WorkManager;
import autodagger.AutoInjector;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
@@ -114,6 +99,8 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
ReactionsInsideMessageBinding reactionsBinding;
+ FileViewerUtils fileViewerUtils;
+
View clickView;
ReactionsInterface reactionsInterface;
@@ -138,7 +125,7 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
} else {
userAvatar.setVisibility(View.VISIBLE);
userAvatar.setOnClickListener(v -> {
- if (payload instanceof ProfileBottomSheet){
+ if (payload instanceof ProfileBottomSheet) {
((ProfileBottomSheet) payload).showFor(message.actorId, v.getContext());
}
});
@@ -163,6 +150,8 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
if (message.getMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) {
+ fileViewerUtils = new FileViewerUtils(context, message.activeUser);
+
String fileName = message.getSelectedIndividualHashMap().get(KEY_NAME);
getMessageText().setText(fileName);
if (message.getSelectedIndividualHashMap().containsKey(KEY_CONTACT_NAME)) {
@@ -179,8 +168,8 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
if (message.getSelectedIndividualHashMap().containsKey(KEY_CONTACT_PHOTO)) {
image = getPreviewContactPhoto();
Drawable drawable = getDrawableFromContactDetails(
- context,
- message.getSelectedIndividualHashMap().get(KEY_CONTACT_PHOTO));
+ context,
+ message.getSelectedIndividualHashMap().get(KEY_CONTACT_PHOTO));
image.getHierarchy().setPlaceholderImage(drawable);
} else if (message.getSelectedIndividualHashMap().containsKey(KEY_MIMETYPE)) {
String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
@@ -191,52 +180,27 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
fetchFileInformation("/" + message.getSelectedIndividualHashMap().get(KEY_PATH), message.activeUser);
}
- if(message.activeUser != null && message.activeUser.getUsername() != null && message.activeUser.getBaseUrl() != null){
- String accountString =
- message.activeUser.getUsername() + "@" +
- message.activeUser.getBaseUrl()
- .replace("https://", "")
- .replace("http://", "");
-
+ if (message.activeUser != null && message.activeUser.getUsername() != null && message.activeUser.getBaseUrl() != null) {
clickView.setOnClickListener(v -> {
- String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
- if (isSupportedForInternalViewer(mimetype) || canBeHandledByExternalApp(mimetype, fileName)) {
- openOrDownloadFile(message);
- } else {
- openFileInFilesApp(message, accountString);
- }
+ fileViewerUtils.openFile(message, progressBar, getMessageText(), image);
});
clickView.setOnLongClickListener(l -> {
- onMessageViewLongClick(message, accountString);
+ onMessageViewLongClick(message);
return true;
});
} else {
Log.e(TAG, "failed to set click listener because activeUser, username or baseUrl were null");
}
+ fileViewerUtils.resumeToUpdateViewsByProgress(
+ Objects.requireNonNull(message.getSelectedIndividualHashMap().get(MagicPreviewMessageViewHolder.KEY_NAME)),
+ Objects.requireNonNull(message.getSelectedIndividualHashMap().get(MagicPreviewMessageViewHolder.KEY_ID)),
+ Objects.requireNonNull(message.getSelectedIndividualHashMap().get(MagicPreviewMessageViewHolder.KEY_MIMETYPE)),
+ progressBar,
+ getMessageText(),
+ image);
- // check if download worker is already running
- String fileId = message.getSelectedIndividualHashMap().get(KEY_ID);
- ListenableFuture> workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId);
-
- try {
- for (WorkInfo workInfo : workers.get()) {
- if (workInfo.getState() == WorkInfo.State.RUNNING ||
- workInfo.getState() == WorkInfo.State.ENQUEUED) {
- progressBar.setVisibility(View.VISIBLE);
-
- String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
-
- WorkManager
- .getInstance(context)
- .getWorkInfoByIdLiveData(workInfo.getId())
- .observeForever(info -> updateViewsByProgress(fileName, mimetype, info));
- }
- }
- } catch (ExecutionException | InterruptedException e) {
- Log.e(TAG, "Error when checking if worker already exists", e);
- }
} else if (message.getMessageType() == ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE) {
getMessageText().setText("GIPHY");
DisplayUtils.setClickableString("GIPHY", "https://giphy.com", getMessageText());
@@ -273,9 +237,9 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
Drawable drawable = null;
if (!base64.equals("")) {
ByteArrayInputStream inputStream = new ByteArrayInputStream(
- Base64.decode(base64.getBytes(), Base64.DEFAULT));
+ Base64.decode(base64.getBytes(), Base64.DEFAULT));
drawable = Drawable.createFromResourceStream(context.getResources(),
- null, inputStream, null, null);
+ null, inputStream, null, null);
try {
inputStream.close();
} catch (IOException e) {
@@ -287,151 +251,8 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
return drawable;
}
- public abstract EmojiTextView getMessageText();
-
- public abstract ProgressBar getProgressBar();
-
- public abstract SimpleDraweeView getImage();
-
- public abstract View getPreviewContainer();
-
- public abstract View getPreviewContactContainer();
-
- public abstract SimpleDraweeView getPreviewContactPhoto();
-
- public abstract EmojiTextView getPreviewContactName();
-
- public abstract ProgressBar getPreviewContactProgressBar();
-
- public abstract ReactionsInsideMessageBinding getReactionsBinding();
-
- private void openOrDownloadFile(ChatMessage message) {
- String filename = message.getSelectedIndividualHashMap().get(KEY_NAME);
- String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
- File file = new File(context.getCacheDir(), filename);
- if (file.exists()) {
- openFile(filename, mimetype);
- } else {
- downloadFileToCache(message);
- }
- }
-
- public boolean isSupportedForInternalViewer(String mimetype){
- switch (mimetype) {
- case "image/png":
- case "image/jpeg":
- case "image/gif":
- case "audio/mpeg":
- case "audio/wav":
- case "audio/ogg":
- case "video/mp4":
- case "video/quicktime":
- case "video/ogg":
- case "text/markdown":
- case "text/plain":
- return true;
- default:
- return false;
- }
- }
-
- private void openFile(String filename, String mimetype) {
- switch (mimetype) {
- case "audio/mpeg":
- case "audio/wav":
- case "audio/ogg":
- case "video/mp4":
- case "video/quicktime":
- case "video/ogg":
- openMediaView(filename, mimetype);
- break;
- case "image/png":
- case "image/jpeg":
- case "image/gif":
- openImageView(filename, mimetype);
- break;
- case "text/markdown":
- case "text/plain":
- openTextView(filename, mimetype);
- break;
- default:
- openFileByExternalApp(filename, mimetype);
- }
- }
-
- private void openFileByExternalApp(String fileName, String mimetype) {
- String path = context.getCacheDir().getAbsolutePath() + "/" + fileName;
- File file = new File(path);
- Intent intent;
- if (Build.VERSION.SDK_INT < 24) {
- intent = new Intent(Intent.ACTION_VIEW);
- intent.setDataAndType(Uri.fromFile(file), mimetype);
- intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
- } else {
- intent = new Intent();
- intent.setAction(Intent.ACTION_VIEW);
- Uri pdfURI = FileProvider.getUriForFile(context, context.getPackageName(), file);
- intent.setDataAndType(pdfURI, mimetype);
- intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- }
-
- try {
- if (intent.resolveActivity(context.getPackageManager()) != null) {
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
- } else {
- Log.e(TAG, "No Application found to open the file. This should have been handled beforehand!");
- }
- } catch (Exception e) {
- Log.e(TAG, "Error while opening file", e);
- }
- }
-
- private boolean canBeHandledByExternalApp(String mimetype, String fileName) {
- String path = context.getCacheDir().getAbsolutePath() + "/" + fileName;
- File file = new File(path);
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setDataAndType(Uri.fromFile(file), mimetype);
- return intent.resolveActivity(context.getPackageManager()) != null;
- }
-
- private void openImageView(String filename, String mimetype) {
- Intent fullScreenImageIntent = new Intent(context, FullScreenImageActivity.class);
- fullScreenImageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- fullScreenImageIntent.putExtra("FILE_NAME", filename);
- fullScreenImageIntent.putExtra("IS_GIF", isGif(mimetype));
- context.startActivity(fullScreenImageIntent);
- }
-
- private void openFileInFilesApp(ChatMessage message, String accountString) {
- if (AccountUtils.INSTANCE.canWeOpenFilesApp(context, accountString)) {
- Intent filesAppIntent = new Intent(Intent.ACTION_VIEW, null);
- final ComponentName componentName = new ComponentName(
- context.getString(R.string.nc_import_accounts_from),
- "com.owncloud.android.ui.activity.FileDisplayActivity"
- );
- filesAppIntent.setComponent(componentName);
- filesAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- filesAppIntent.setPackage(context.getString(R.string.nc_import_accounts_from));
- filesAppIntent.putExtra(BundleKeys.INSTANCE.getKEY_ACCOUNT(), accountString);
- filesAppIntent.putExtra(
- BundleKeys.INSTANCE.getKEY_FILE_ID(),
- message.getSelectedIndividualHashMap().get(KEY_ID)
- );
- context.startActivity(filesAppIntent);
- } else {
- Intent browserIntent = new Intent(
- Intent.ACTION_VIEW,
- Uri.parse(message.getSelectedIndividualHashMap().get("link"))
- );
- browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(browserIntent);
- }
- }
-
- private void onMessageViewLongClick(ChatMessage message, String accountString) {
- if (isSupportedForInternalViewer(message.getSelectedIndividualHashMap().get(KEY_MIMETYPE))) {
+ private void onMessageViewLongClick(ChatMessage message) {
+ if (fileViewerUtils.isSupportedForInternalViewer(message.getSelectedIndividualHashMap().get(KEY_MIMETYPE))) {
previewMessageInterface.onPreviewMessageLongClick(message);
return;
}
@@ -452,132 +273,17 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
popupMenu.inflate(R.menu.chat_preview_message_menu);
popupMenu.setOnMenuItemClickListener(item -> {
- openFileInFilesApp(message, accountString);
+ if (item.getItemId()== R.id.openInFiles){
+ String keyID = message.getSelectedIndividualHashMap().get(KEY_ID);
+ String link = message.getSelectedIndividualHashMap().get("link");
+ fileViewerUtils.openFileInFilesApp(link, keyID);
+ }
return true;
});
popupMenu.show();
}
- @SuppressLint("LongLogTag")
- private void downloadFileToCache(ChatMessage message) {
-
- String baseUrl = message.activeUser.getBaseUrl();
- String userId = message.activeUser.getUserId();
- String attachmentFolder = CapabilitiesUtil.getAttachmentFolder(message.activeUser);
-
- String fileName = message.getSelectedIndividualHashMap().get(KEY_NAME);
- String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
-
- String size = message.getSelectedIndividualHashMap().get("size");
-
- if (size == null) {
- size = "-1";
- }
- Integer fileSize = Integer.valueOf(size);
-
- String fileId = message.getSelectedIndividualHashMap().get(KEY_ID);
- String path = message.getSelectedIndividualHashMap().get(KEY_PATH);
-
- // check if download worker is already running
- ListenableFuture> workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId);
-
- try {
- for (WorkInfo workInfo : workers.get()) {
- if (workInfo.getState() == WorkInfo.State.RUNNING || workInfo.getState() == WorkInfo.State.ENQUEUED) {
- Log.d(TAG, "Download worker for " + fileId + " is already running or " +
- "scheduled");
- return;
- }
- }
- } catch (ExecutionException | InterruptedException e) {
- Log.e(TAG, "Error when checking if worker already exsists", e);
- }
-
- Data data;
- OneTimeWorkRequest downloadWorker;
-
- data = new Data.Builder()
- .putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl)
- .putString(DownloadFileToCacheWorker.KEY_USER_ID, userId)
- .putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder)
- .putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
- .putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
- .putInt(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize)
- .build();
-
- downloadWorker = new OneTimeWorkRequest.Builder(DownloadFileToCacheWorker.class)
- .setInputData(data)
- .addTag(fileId)
- .build();
-
- WorkManager.getInstance().enqueue(downloadWorker);
-
- progressBar.setVisibility(View.VISIBLE);
-
- WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.getId()).observeForever(workInfo -> {
- updateViewsByProgress(fileName, mimetype, workInfo);
- });
- }
-
- private void updateViewsByProgress(String fileName, String mimetype, WorkInfo workInfo) {
- switch (workInfo.getState()) {
- case RUNNING:
- int progress = workInfo.getProgress().getInt(DownloadFileToCacheWorker.PROGRESS, -1);
- if (progress > -1) {
- getMessageText().setText(String.format(context.getResources().getString(R.string.filename_progress), fileName, progress));
- }
- break;
-
- case SUCCEEDED:
- if (image.isShown()) {
- openFile(fileName, mimetype);
- } else {
- Log.d(TAG, "file " + fileName +
- " was downloaded but it's not opened because view is not shown on screen");
- }
- getMessageText().setText(fileName);
- progressBar.setVisibility(View.GONE);
- break;
-
- case FAILED:
- getMessageText().setText(fileName);
- progressBar.setVisibility(View.GONE);
- break;
- default:
- // do nothing
- break;
- }
- }
-
- private void openMediaView(String filename, String mimetype) {
- Intent fullScreenMediaIntent = new Intent(context, FullScreenMediaActivity.class);
- fullScreenMediaIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- fullScreenMediaIntent.putExtra("FILE_NAME", filename);
- fullScreenMediaIntent.putExtra("AUDIO_ONLY", isAudioOnly(mimetype));
- context.startActivity(fullScreenMediaIntent);
- }
-
- private void openTextView(String filename, String mimetype) {
- Intent fullScreenTextViewerIntent = new Intent(context, FullScreenTextViewerActivity.class);
- fullScreenTextViewerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- fullScreenTextViewerIntent.putExtra("FILE_NAME", filename);
- fullScreenTextViewerIntent.putExtra("IS_MARKDOWN", isMarkdown(mimetype));
- context.startActivity(fullScreenTextViewerIntent);
- }
-
- private boolean isGif(String mimetype) {
- return ("image/gif").equals(mimetype);
- }
-
- private boolean isMarkdown(String mimetype) {
- return ("text/markdown").equals(mimetype);
- }
-
- private boolean isAudioOnly(String mimetype) {
- return mimetype.startsWith("audio");
- }
-
private void fetchFileInformation(String url, UserEntity activeUser) {
Single.fromCallable(new Callable() {
@Override
@@ -585,34 +291,34 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
return new ReadFilesystemOperation(okHttpClient, activeUser, url, 0);
}
}).observeOn(Schedulers.io())
- .subscribe(new SingleObserver() {
- @Override
- public void onSubscribe(@NonNull Disposable d) {
- // unused atm
- }
+ .subscribe(new SingleObserver() {
+ @Override
+ public void onSubscribe(@NonNull Disposable d) {
+ // unused atm
+ }
- @Override
- public void onSuccess(@NonNull ReadFilesystemOperation readFilesystemOperation) {
- DavResponse davResponse = readFilesystemOperation.readRemotePath();
- if (davResponse.data != null) {
- List browserFileList = (List) davResponse.data;
- if (!browserFileList.isEmpty()) {
- new Handler(context.getMainLooper()).post(() -> {
- int resourceId = DrawableUtils
- .INSTANCE
- .getDrawableResourceIdForMimeType(browserFileList.get(0).mimeType);
- Drawable drawable = ContextCompat.getDrawable(context, resourceId);
- image.getHierarchy().setPlaceholderImage(drawable);
- });
- }
+ @Override
+ public void onSuccess(@NonNull ReadFilesystemOperation readFilesystemOperation) {
+ DavResponse davResponse = readFilesystemOperation.readRemotePath();
+ if (davResponse.data != null) {
+ List browserFileList = (List) davResponse.data;
+ if (!browserFileList.isEmpty()) {
+ new Handler(context.getMainLooper()).post(() -> {
+ int resourceId = DrawableUtils
+ .INSTANCE
+ .getDrawableResourceIdForMimeType(browserFileList.get(0).mimeType);
+ Drawable drawable = ContextCompat.getDrawable(context, resourceId);
+ image.getHierarchy().setPlaceholderImage(drawable);
+ });
}
}
+ }
- @Override
- public void onError(@NonNull Throwable e) {
- Log.e(TAG, "Error reading file information", e);
- }
- });
+ @Override
+ public void onError(@NonNull Throwable e) {
+ Log.e(TAG, "Error reading file information", e);
+ }
+ });
}
public void assignReactionInterface(ReactionsInterface reactionsInterface) {
@@ -622,4 +328,22 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
public void assignPreviewMessageInterface(PreviewMessageInterface previewMessageInterface) {
this.previewMessageInterface = previewMessageInterface;
}
+
+ public abstract EmojiTextView getMessageText();
+
+ public abstract ProgressBar getProgressBar();
+
+ public abstract SimpleDraweeView getImage();
+
+ public abstract View getPreviewContainer();
+
+ public abstract View getPreviewContactContainer();
+
+ public abstract SimpleDraweeView getPreviewContactPhoto();
+
+ public abstract EmojiTextView getPreviewContactName();
+
+ public abstract ProgressBar getPreviewContactProgressBar();
+
+ public abstract ReactionsInsideMessageBinding getReactionsBinding();
}
diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java
index 8f177a566..4c4133c60 100644
--- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java
+++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java
@@ -27,6 +27,7 @@ package com.nextcloud.talk.api;
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
import com.nextcloud.talk.models.json.chat.ChatOverall;
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage;
+import com.nextcloud.talk.models.json.chat.ChatShareOverall;
import com.nextcloud.talk.models.json.conversations.RoomOverall;
import com.nextcloud.talk.models.json.conversations.RoomsOverall;
import com.nextcloud.talk.models.json.generic.GenericOverall;
@@ -338,6 +339,12 @@ public interface NcApi {
@Field("actorDisplayName") String actorDisplayName,
@Field("replyTo") Integer replyTo);
+ @GET
+ Observable> getSharedItems(@Header("Authorization") String authorization, @Url String url,
+ @Query("objectType") String objectType,
+ @Nullable @Query("lastKnownMessageId") Integer lastKnownMessageId,
+ @Nullable @Query("limit") Integer limit);
+
@GET
Observable getMentionAutocompleteSuggestions(@Header("Authorization") String authorization,
@Url String url, @Query("search") String query,
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 fca1b0f0c..f3e484e15 100644
--- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
+++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
@@ -46,6 +46,7 @@ import android.os.Build
import android.os.Build.VERSION_CODES.O
import android.os.Bundle
import android.os.Handler
+import android.os.Parcelable
import android.os.SystemClock
import android.os.VibrationEffect
import android.os.Vibrator
@@ -99,6 +100,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.activities.TakePhotoActivity
import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
@@ -156,6 +158,7 @@ import com.nextcloud.talk.utils.NotificationUtils
import com.nextcloud.talk.utils.UriUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
@@ -188,9 +191,7 @@ import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
import java.text.SimpleDateFormat
-import java.util.ArrayList
import java.util.Date
-import java.util.HashMap
import java.util.Objects
import java.util.concurrent.ExecutionException
import javax.inject.Inject
@@ -253,6 +254,7 @@ class ChatController(args: Bundle) :
var conversationInfoMenuItem: MenuItem? = null
var conversationVoiceCallMenuItem: MenuItem? = null
var conversationVideoMenuItem: MenuItem? = null
+ var conversationSharedItemsItem: MenuItem? = null
var magicWebSocketInstance: MagicWebSocketInstance? = null
@@ -1464,7 +1466,7 @@ class ChatController(args: Bundle) :
val bundle = Bundle()
bundle.putParcelable(BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap(browserType))
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(conversationUser))
- bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
+ bundle.putString(KEY_ROOM_TOKEN, roomToken)
router.pushController(
RouterTransaction.with(BrowserForSharingController(bundle))
.pushChangeHandler(VerticalChangeHandler())
@@ -1476,7 +1478,7 @@ class ChatController(args: Bundle) :
Log.d(TAG, "showShareLocationScreen")
val bundle = Bundle()
- bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
+ bundle.putString(KEY_ROOM_TOKEN, roomToken)
router.pushController(
RouterTransaction.with(LocationPickerController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
@@ -1487,7 +1489,7 @@ class ChatController(args: Bundle) :
private fun showConversationInfoScreen() {
val bundle = Bundle()
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser)
- bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
+ bundle.putString(KEY_ROOM_TOKEN, roomToken)
bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, inOneToOneCall())
router.pushController(
RouterTransaction.with(ConversationInfoController(bundle))
@@ -2299,6 +2301,7 @@ class ChatController(args: Bundle) :
conversationInfoMenuItem = menu.findItem(R.id.conversation_info)
conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
+ conversationSharedItemsItem = menu.findItem(R.id.shared_items)
loadAvatarForStatusBar()
}
@@ -2337,10 +2340,22 @@ class ChatController(args: Bundle) :
showConversationInfoScreen()
return true
}
+ R.id.shared_items -> {
+ showSharedItems()
+ return true
+ }
else -> return super.onOptionsItemSelected(item)
}
}
+ private fun showSharedItems() {
+ val intent = Intent(activity, SharedItemsActivity::class.java)
+ intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
+ intent.putExtra(KEY_ROOM_TOKEN, roomToken)
+ intent.putExtra(KEY_USER_ENTITY, conversationUser as Parcelable)
+ activity!!.startActivity(intent)
+ }
+
private fun handleSystemMessages(chatMessageList: List): List {
val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
val chatMessageIterator = chatMessageMap.iterator()
@@ -2402,7 +2417,7 @@ class ChatController(args: Bundle) :
bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword)
bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl)
- bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, it.displayName)
+ bundle.putString(KEY_CONVERSATION_NAME, it.displayName)
if (isVoiceOnlyCall) {
bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true)
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 cc35573f6..c853775a4 100644
--- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt
+++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt
@@ -27,9 +27,11 @@
package com.nextcloud.talk.controllers
import android.annotation.SuppressLint
+import android.content.Intent
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
+import android.os.Parcelable
import android.text.TextUtils
import android.util.Log
import android.view.MenuItem
@@ -49,6 +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.adapters.items.ParticipantItem
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
@@ -88,11 +91,8 @@ import io.reactivex.schedulers.Schedulers
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
-import java.util.ArrayList
import java.util.Calendar
import java.util.Collections
-import java.util.Comparator
-import java.util.HashMap
import java.util.Locale
import javax.inject.Inject
@@ -175,10 +175,19 @@ class ConversationInfoController(args: Bundle) :
binding.leaveConversationAction.setOnClickListener { leaveConversation() }
binding.clearConversationHistory.setOnClickListener { showClearHistoryDialog(null) }
binding.addParticipantsAction.setOnClickListener { addParticipants() }
+ binding.showSharedItemsAction.setOnClickListener { showSharedItems() }
fetchRoomInfo()
}
+ private fun showSharedItems() {
+ val intent = Intent(activity, SharedItemsActivity::class.java)
+ intent.putExtra(BundleKeys.KEY_CONVERSATION_NAME, conversation?.displayName)
+ intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, conversationToken)
+ intent.putExtra(BundleKeys.KEY_USER_ENTITY, conversationUser as Parcelable)
+ activity!!.startActivity(intent)
+ }
+
override fun onViewBound(view: View) {
super.onViewBound(view)
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java
index 3057a51ca..e3ac0445b 100644
--- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java
+++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java
@@ -156,6 +156,9 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
if (MessageDigest.isEqual(
Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
("file").getBytes(Charsets.UTF_8))) {
+
+ // TODO: this selectedIndividualHashMap stuff needs to be analyzed and most likely be refactored!
+ // it just feels wrong to fill this here inside getImageUrl()
selectedIndividualHashMap = individualHashMap;
if (!isVoiceMessage()) {
if (getActiveUser() != null && getActiveUser().getBaseUrl() != null) {
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOCS.java b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOCS.java
new file mode 100644
index 000000000..6f0191037
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOCS.java
@@ -0,0 +1,76 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic
+ *
+ * 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.models.json.chat;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+import com.nextcloud.talk.models.json.generic.GenericOCS;
+
+import org.parceler.Parcel;
+
+import java.util.HashMap;
+import java.util.Objects;
+
+@Parcel
+@JsonObject
+public class ChatShareOCS {
+ @JsonField(name = "data")
+ public HashMap data;
+
+ public HashMap getData() {
+ return this.data;
+ }
+
+ public void setData(HashMap data) {
+ this.data = data;
+ }
+
+ public boolean equals(final Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof ChatShareOCS)) {
+ return false;
+ }
+ final ChatShareOCS other = (ChatShareOCS) o;
+ if (!other.canEqual(this)) {
+ return false;
+ }
+ final Object this$data = this.getData();
+ final Object other$data = other.getData();
+
+ return Objects.equals(this$data, other$data);
+ }
+
+ protected boolean canEqual(final Object other) {
+ return other instanceof ChatShareOCS;
+ }
+
+ public int hashCode() {
+ final int PRIME = 59;
+ int result = 1;
+ final Object $data = this.getData();
+ return result * PRIME + ($data == null ? 43 : $data.hashCode());
+ }
+
+ public String toString() {
+ return "ChatShareOCS(data=" + this.getData() + ")";
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverall.java b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverall.java
new file mode 100644
index 000000000..ce97b53e6
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverall.java
@@ -0,0 +1,75 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic
+ *
+ * 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.models.json.chat;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+
+import org.parceler.Parcel;
+
+import java.util.Objects;
+
+@Parcel
+@JsonObject
+public class ChatShareOverall {
+ @JsonField(name = "ocs")
+ public ChatShareOCS ocs;
+
+ public ChatShareOCS getOcs() {
+ return this.ocs;
+ }
+
+ public void setOcs(ChatShareOCS ocs) {
+ this.ocs = ocs;
+ }
+
+ public boolean equals(final Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof ChatShareOverall)) {
+ return false;
+ }
+ final ChatShareOverall other = (ChatShareOverall) o;
+ if (!other.canEqual(this)) {
+ return false;
+ }
+ final Object this$ocs = this.getOcs();
+ final Object other$ocs = other.getOcs();
+
+ return Objects.equals(this$ocs, other$ocs);
+ }
+
+ protected boolean canEqual(final Object other) {
+ return other instanceof ChatShareOverall;
+ }
+
+ public int hashCode() {
+ final int PRIME = 59;
+ int result = 1;
+ final Object $ocs = this.getOcs();
+ return result * PRIME + ($ocs == null ? 43 : $ocs.hashCode());
+ }
+
+ public String toString() {
+ return "ChatShareOverall(ocs=" + this.getOcs() + ")";
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/repositories/SharedItem.kt b/app/src/main/java/com/nextcloud/talk/repositories/SharedItem.kt
new file mode 100644
index 000000000..d620c04a1
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/repositories/SharedItem.kt
@@ -0,0 +1,15 @@
+package com.nextcloud.talk.repositories
+
+import com.nextcloud.talk.models.database.UserEntity
+
+data class SharedItem(
+ val id: String,
+ val name: String,
+ val fileSize: Int,
+ 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/SharedItemsRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/SharedItemsRepository.kt
new file mode 100644
index 000000000..900946724
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/repositories/SharedItemsRepository.kt
@@ -0,0 +1,64 @@
+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.utils.ApiUtils
+import io.reactivex.Observable
+import retrofit2.Response
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class SharedItemsRepository {
+
+ companion object {
+ private val TAG = SharedItemsRepository::class.simpleName
+ }
+
+ var parameters: Parameters? = null
+
+ @Inject
+ lateinit var ncApi: NcApi
+
+ init {
+ sharedApplication!!.componentApplication.inject(this)
+ }
+
+ fun media(type: String): Observable>? {
+ return media(type, null)
+ }
+
+ fun media(type: String, lastKnownMessageId: Int?): Observable>? {
+ val credentials = ApiUtils.getCredentials(parameters!!.userName, parameters!!.userToken)
+
+ return ncApi.getSharedItems(
+ credentials,
+ ApiUtils.getUrlForChatSharedItems(1, parameters!!.baseUrl, parameters!!.roomToken),
+ type, lastKnownMessageId, 28
+ )
+ }
+
+ 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
+ )
+}
diff --git a/app/src/main/java/com/nextcloud/talk/repositories/SharedMediaItems.kt b/app/src/main/java/com/nextcloud/talk/repositories/SharedMediaItems.kt
new file mode 100644
index 000000000..1e3b56bae
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/repositories/SharedMediaItems.kt
@@ -0,0 +1,9 @@
+package com.nextcloud.talk.repositories
+
+class SharedMediaItems(
+ val type: String,
+ val items: MutableList,
+ var lastSeenId: Int?,
+ var moreItemsExisting: Boolean,
+ val authHeader: Map
+)
diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
index 9e3ea3c43..2b6e654c0 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
@@ -260,6 +260,10 @@ public class ApiUtils {
public static String getUrlForChatMessage(int version, String baseUrl, String token, String messageId) {
return getUrlForChat(version, baseUrl, token) + "/" + messageId;
}
+
+ public static String getUrlForChatSharedItems(int version, String baseUrl, String token) {
+ return getUrlForChat(version, baseUrl, token) + "/share";
+ }
public static String getUrlForSignaling(int version, String baseUrl) {
return getUrlForApi(version, baseUrl) + "/signaling";
diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
new file mode 100644
index 000000000..4b3638956
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
@@ -0,0 +1,397 @@
+package com.nextcloud.talk.utils
+
+import android.annotation.SuppressLint
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.util.Log
+import android.view.View
+import android.widget.ProgressBar
+import androidx.core.content.FileProvider
+import androidx.emoji.widget.EmojiTextView
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import com.facebook.drawee.view.SimpleDraweeView
+import com.nextcloud.talk.R
+import com.nextcloud.talk.activities.FullScreenImageActivity
+import com.nextcloud.talk.activities.FullScreenMediaActivity
+import com.nextcloud.talk.activities.FullScreenTextViewerActivity
+import com.nextcloud.talk.adapters.messages.MagicPreviewMessageViewHolder
+import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
+import com.nextcloud.talk.models.database.CapabilitiesUtil
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACCOUNT
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID
+import java.io.File
+import java.util.concurrent.ExecutionException
+
+class FileViewerUtils(private val context: Context, private val userEntity: UserEntity) {
+
+ fun openFile(
+ message: ChatMessage,
+ progressBar: ProgressBar?,
+ messageText: EmojiTextView?,
+ previewImage: SimpleDraweeView
+ ) {
+ val fileName = message.getSelectedIndividualHashMap()[MagicPreviewMessageViewHolder.KEY_NAME]!!
+ val mimetype = message.getSelectedIndividualHashMap()[MagicPreviewMessageViewHolder.KEY_MIMETYPE]!!
+ val link = message.getSelectedIndividualHashMap()["link"]!!
+
+ val fileId = message.getSelectedIndividualHashMap()[MagicPreviewMessageViewHolder.KEY_ID]!!
+ val path = message.getSelectedIndividualHashMap()[MagicPreviewMessageViewHolder.KEY_PATH]!!
+
+ var size = message.getSelectedIndividualHashMap()["size"]
+ if (size == null) {
+ size = "-1"
+ }
+ val fileSize = Integer.valueOf(size)
+
+ openFile(
+ fileId,
+ fileName,
+ fileSize,
+ path,
+ link,
+ mimetype,
+ progressBar,
+ messageText,
+ previewImage
+ )
+ }
+
+ fun openFile(
+ fileId: String,
+ fileName: String,
+ fileSize: Int,
+ path: String,
+ link: String,
+ mimetype: String,
+ progressBar: ProgressBar?,
+ messageText: EmojiTextView?,
+ previewImage: SimpleDraweeView
+ ) {
+ if (isSupportedForInternalViewer(mimetype) || canBeHandledByExternalApp(mimetype, fileName)) {
+ openOrDownloadFile(
+ fileName,
+ fileId,
+ path,
+ fileSize,
+ mimetype,
+ progressBar,
+ messageText,
+ previewImage
+ )
+ } else {
+ openFileInFilesApp(link, fileId)
+ }
+ }
+
+ private fun canBeHandledByExternalApp(mimetype: String, fileName: String): Boolean {
+ val path: String = context.cacheDir.absolutePath + "/" + fileName
+ val file = File(path)
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.setDataAndType(Uri.fromFile(file), mimetype)
+ return intent.resolveActivity(context.packageManager) != null
+ }
+
+ private fun openOrDownloadFile(
+ fileName: String,
+ fileId: String,
+ path: String,
+ fileSize: Int,
+ mimetype: String,
+ progressBar: ProgressBar?,
+ messageText: EmojiTextView?,
+ previewImage: SimpleDraweeView
+ ) {
+ val file = File(context.cacheDir, fileName)
+ if (file.exists()) {
+ openFileByMimetype(fileName!!, mimetype!!)
+ } else {
+ downloadFileToCache(
+ fileName,
+ fileId,
+ path,
+ fileSize,
+ mimetype,
+ progressBar,
+ messageText,
+ previewImage
+ )
+ }
+ }
+
+ private fun openFileByMimetype(filename: String, mimetype: String) {
+ when (mimetype) {
+ "audio/mpeg",
+ "audio/wav",
+ "audio/ogg",
+ "video/mp4",
+ "video/quicktime",
+ "video/ogg"
+ -> openMediaView(filename, mimetype)
+ "image/png",
+ "image/jpeg",
+ "image/gif"
+ -> openImageView(filename, mimetype)
+ "text/markdown",
+ "text/plain"
+ -> openTextView(filename, mimetype)
+ else
+ -> openFileByExternalApp(filename, mimetype)
+ }
+ }
+
+ private fun openFileByExternalApp(fileName: String, mimetype: String) {
+ val path = context.cacheDir.absolutePath + "/" + fileName
+ val file = File(path)
+ val intent: Intent
+ if (Build.VERSION.SDK_INT < 24) {
+ intent = Intent(Intent.ACTION_VIEW)
+ intent.setDataAndType(Uri.fromFile(file), mimetype)
+ intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
+ } else {
+ intent = Intent()
+ intent.action = Intent.ACTION_VIEW
+ val pdfURI = FileProvider.getUriForFile(context, context.packageName, file)
+ intent.setDataAndType(pdfURI, mimetype)
+ intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ try {
+ if (intent.resolveActivity(context.packageManager) != null) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ } else {
+ Log.e(TAG, "No Application found to open the file. This should have been handled beforehand!")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error while opening file", e)
+ }
+ }
+
+ fun openFileInFilesApp(link: String, keyID: String) {
+ val accountString = userEntity.username + "@" +
+ userEntity.baseUrl
+ .replace("https://", "")
+ .replace("http://", "")
+
+ if (canWeOpenFilesApp(context, accountString)) {
+ val filesAppIntent = Intent(Intent.ACTION_VIEW, null)
+ val componentName = ComponentName(
+ context.getString(R.string.nc_import_accounts_from),
+ "com.owncloud.android.ui.activity.FileDisplayActivity"
+ )
+ filesAppIntent.component = componentName
+ filesAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ filesAppIntent.setPackage(context.getString(R.string.nc_import_accounts_from))
+ filesAppIntent.putExtra(KEY_ACCOUNT, accountString)
+ filesAppIntent.putExtra(KEY_FILE_ID, keyID)
+ context.startActivity(filesAppIntent)
+ } else {
+ val browserIntent = Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(link)
+ )
+ browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(browserIntent)
+ }
+ }
+
+ private fun openImageView(filename: String, mimetype: String) {
+ val fullScreenImageIntent = Intent(context, FullScreenImageActivity::class.java)
+ fullScreenImageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ fullScreenImageIntent.putExtra("FILE_NAME", filename)
+ fullScreenImageIntent.putExtra("IS_GIF", isGif(mimetype))
+ context.startActivity(fullScreenImageIntent)
+ }
+
+ private fun openMediaView(filename: String, mimetype: String) {
+ val fullScreenMediaIntent = Intent(context, FullScreenMediaActivity::class.java)
+ fullScreenMediaIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ fullScreenMediaIntent.putExtra("FILE_NAME", filename)
+ fullScreenMediaIntent.putExtra("AUDIO_ONLY", isAudioOnly(mimetype))
+ context.startActivity(fullScreenMediaIntent)
+ }
+
+ private fun openTextView(filename: String, mimetype: String) {
+ val fullScreenTextViewerIntent = Intent(context, FullScreenTextViewerActivity::class.java)
+ fullScreenTextViewerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ fullScreenTextViewerIntent.putExtra("FILE_NAME", filename)
+ fullScreenTextViewerIntent.putExtra("IS_MARKDOWN", isMarkdown(mimetype))
+ context.startActivity(fullScreenTextViewerIntent)
+ }
+
+ fun isSupportedForInternalViewer(mimetype: String?): Boolean {
+ return when (mimetype) {
+ "image/png", "image/jpeg",
+ "image/gif", "audio/mpeg",
+ "audio/wav", "audio/ogg",
+ "video/mp4", "video/quicktime",
+ "video/ogg", "text/markdown",
+ "text/plain" -> true
+ else -> false
+ }
+ }
+
+ private fun isGif(mimetype: String): Boolean {
+ return "image/gif" == mimetype
+ }
+
+ private fun isMarkdown(mimetype: String): Boolean {
+ return "text/markdown" == mimetype
+ }
+
+ private fun isAudioOnly(mimetype: String): Boolean {
+ return mimetype.startsWith("audio")
+ }
+
+ @SuppressLint("LongLogTag")
+ private fun downloadFileToCache(
+ fileName: String,
+ fileId: String,
+ path: String,
+ fileSize: Int,
+ mimetype: String,
+ progressBar: ProgressBar?,
+ messageText: EmojiTextView?,
+ previewImage: SimpleDraweeView
+ ) {
+ // check if download worker is already running
+ val workers = WorkManager.getInstance(context).getWorkInfosByTag(
+ fileId!!
+ )
+ try {
+ for (workInfo in workers.get()) {
+ if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
+ Log.d(TAG, "Download worker for $fileId is already running or scheduled")
+ return
+ }
+ }
+ } catch (e: ExecutionException) {
+ Log.e(TAG, "Error when checking if worker already exsists", e)
+ } catch (e: InterruptedException) {
+ Log.e(TAG, "Error when checking if worker already exsists", e)
+ }
+ val downloadWorker: OneTimeWorkRequest
+ val data: Data = Data.Builder()
+ .putString(DownloadFileToCacheWorker.KEY_BASE_URL, userEntity.baseUrl)
+ .putString(DownloadFileToCacheWorker.KEY_USER_ID, userEntity.userId)
+ .putString(
+ DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER,
+ CapabilitiesUtil.getAttachmentFolder(userEntity)
+ )
+ .putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
+ .putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
+ .putInt(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize)
+ .build()
+ downloadWorker = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java)
+ .setInputData(data)
+ .addTag(fileId)
+ .build()
+ WorkManager.getInstance().enqueue(downloadWorker)
+ progressBar?.visibility = View.VISIBLE
+ WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id)
+ .observeForever { workInfo: WorkInfo? ->
+ updateViewsByProgress(
+ fileName,
+ mimetype,
+ workInfo!!,
+ progressBar,
+ messageText,
+ previewImage
+ )
+ }
+ }
+
+ private fun updateViewsByProgress(
+ fileName: String,
+ mimetype: String,
+ workInfo: WorkInfo,
+ progressBar: ProgressBar?,
+ messageText: EmojiTextView?,
+ previewImage: SimpleDraweeView
+ ) {
+ when (workInfo.state) {
+ WorkInfo.State.RUNNING -> {
+ val progress = workInfo.progress.getInt(DownloadFileToCacheWorker.PROGRESS, -1)
+ if (progress > -1) {
+ messageText?.text = String.format(
+ context.resources.getString(R.string.filename_progress),
+ fileName,
+ progress
+ )
+ }
+ }
+ WorkInfo.State.SUCCEEDED -> {
+ if (previewImage.isShown) {
+ openFileByMimetype(fileName, mimetype)
+ } else {
+ Log.d(
+ TAG,
+ "file " + fileName +
+ " was downloaded but it's not opened because view is not shown on screen"
+ )
+ }
+ messageText?.text = fileName
+ progressBar?.visibility = View.GONE
+ }
+ WorkInfo.State.FAILED -> {
+ messageText?.text = fileName
+ progressBar?.visibility = View.GONE
+ }
+ else -> {
+ }
+ }
+ }
+
+ fun resumeToUpdateViewsByProgress(
+ fileName: String,
+ fileId: String,
+ mimeType: String,
+ progressBar: ProgressBar,
+ messageText: EmojiTextView?,
+ previewImage: SimpleDraweeView
+ ) {
+ val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId)
+
+ try {
+ for (workInfo in workers.get()) {
+ if (workInfo.state == WorkInfo.State.RUNNING ||
+ workInfo.state == WorkInfo.State.ENQUEUED
+ ) {
+ progressBar.visibility = View.VISIBLE
+ WorkManager
+ .getInstance(context)
+ .getWorkInfoByIdLiveData(workInfo.id)
+ .observeForever { info: WorkInfo? ->
+ updateViewsByProgress(
+ fileName,
+ mimeType,
+ info!!,
+ progressBar,
+ messageText,
+ previewImage
+ )
+ }
+ }
+ }
+ } catch (e: ExecutionException) {
+ Log.e(TAG, "Error when checking if worker already exists", e)
+ } catch (e: InterruptedException) {
+ Log.e(TAG, "Error when checking if worker already exists", e)
+ }
+ }
+
+ companion object {
+ private val TAG = FileViewerUtils::class.simpleName
+
+ const val KEY_ID = "id"
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/viewmodels/SharedItemsViewModel.kt b/app/src/main/java/com/nextcloud/talk/viewmodels/SharedItemsViewModel.kt
new file mode 100644
index 000000000..cddf49407
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/viewmodels/SharedItemsViewModel.kt
@@ -0,0 +1,147 @@
+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.repositories.SharedItem
+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: String) :
+ ViewModel() {
+
+ private val _sharedItems: MutableLiveData by lazy {
+ MutableLiveData().also {
+ loadItems(initialType)
+ }
+ }
+
+ 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: String) {
+ repository.media(type)?.subscribeOn(Schedulers.io())
+ ?.observeOn(AndroidSchedulers.mainThread())
+ ?.subscribe(observer(type, true))
+ }
+
+ private fun observer(type: String, 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"]!!.toInt(),
+ fileParameters["path"]!!,
+ fileParameters["link"]!!,
+ fileParameters["mimetype"]!!,
+ previewAvailable,
+ repository.previewLink(fileParameters["id"]),
+ repository.parameters!!.userEntity
+ )
+ } else {
+ Log.w(TAG, "location and deckcard are not yet supported")
+ }
+ }
+ }
+ }
+
+ override fun onError(e: Throwable) {
+ Log.d(TAG, "An error occurred: $e")
+ }
+
+ override fun onComplete() {
+
+ val sortedMutableItems = items.toSortedMap().values.toList().reversed().toMutableList()
+ val moreItemsExisting = items.count() == 28
+
+ if (initModel) {
+ this@SharedItemsViewModel._sharedItems.value =
+ SharedMediaItems(
+ 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()
+ )
+ }
+ }
+ }
+ }
+
+ class Factory(val userEntity: UserEntity, val roomToken: String, private val initialType: String) :
+ 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
+ }
+}
diff --git a/app/src/main/res/drawable/ic_folder_multiple_image.xml b/app/src/main/res/drawable/ic_folder_multiple_image.xml
new file mode 100644
index 000000000..5cbec6e30
--- /dev/null
+++ b/app/src/main/res/drawable/ic_folder_multiple_image.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_shared_items.xml b/app/src/main/res/layout/activity_shared_items.xml
new file mode 100644
index 000000000..d6ababc24
--- /dev/null
+++ b/app/src/main/res/layout/activity_shared_items.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/attachment_item.xml b/app/src/main/res/layout/attachment_item.xml
new file mode 100644
index 000000000..14eba278f
--- /dev/null
+++ b/app/src/main/res/layout/attachment_item.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/attachment_list_item.xml b/app/src/main/res/layout/attachment_list_item.xml
new file mode 100644
index 000000000..3ad85c15a
--- /dev/null
+++ b/app/src/main/res/layout/attachment_list_item.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/controller_conversation_info.xml b/app/src/main/res/layout/controller_conversation_info.xml
index 71d6ef568..37bd3699e 100644
--- a/app/src/main/res/layout/controller_conversation_info.xml
+++ b/app/src/main/res/layout/controller_conversation_info.xml
@@ -129,7 +129,7 @@
android:id="@+id/participants_list_category"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_below="@+id/settings"
+ android:layout_below="@+id/category_shared_items"
android:visibility="gone"
apc:cardBackgroundColor="@color/bg_default"
apc:cardElevation="0dp"
@@ -213,6 +213,29 @@
tools:visibility="visible" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/menu_conversation.xml b/app/src/main/res/menu/menu_conversation.xml
index f9d74ac28..b96a91af2 100644
--- a/app/src/main/res/menu/menu_conversation.xml
+++ b/app/src/main/res/menu/menu_conversation.xml
@@ -37,8 +37,13 @@
+
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index fd5f467b9..a37207455 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -35,6 +35,7 @@
40dp
30dp
96dp
+ 40dp
14sp
6dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 493855699..f2249b863 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -89,7 +89,6 @@
The server version is too old and not supported by this version of the Android app
The server version is very old and will not be supported in the next release!
Warning
- Add
Only current account can be reauthorized
Talk app is not installed on the server you tried to authenticate against
Your already existing account was updated, instead of adding a new one
@@ -118,7 +117,6 @@
Lock %1$s with Android screen lock or supported biometric method
screen_lock
Screen lock inactivity timeout
- None
screen_lock_timeout
Screen security
Prevents screenshots in the recent list and inside the app
@@ -205,8 +203,6 @@
INCOMING
RINGING
Connecting…
- Calling…
- Incoming call from
Guest
New public conversation
Public conversations let you invite people from outside through a specially crafted link.
@@ -338,7 +334,6 @@
Join a conversation or start a new one
Say hi to your friends and colleagues!
- Hello
%s characters limit has been hit
@@ -379,14 +374,6 @@
The meeting will start soon
Not set
-
- No connection
- Bad response
- Timeout
- Empty response
- Unknown error
- Unauthorized
-
Allow guests
Could not leave conversation
You need to promote a new moderator before you can leave %1$s.
@@ -430,6 +417,9 @@
Share contact
Permission to read contacts is required
+
+ Shared items
+
Talk recording from %1$s (%2$s)
Hold to record, release to send.
@@ -498,6 +488,7 @@
Invalid password
Do you want to reauthorize or delete this account?
+
Take a photo
Switch camera
Re-take photo
@@ -507,12 +498,23 @@
Send
Error taking picture
Taking a photo is not possible without permissions
+
+
Bluetooth
Speaker
Phone
Audio output
Wired headset
+
+ Media
+ File
+ Audio
+ Voice
+ Other
+
+ Attachments
+
All
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index a884c7fa2..547896de6 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -257,4 +257,10 @@
- bold
+
+
+