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