diff --git a/CHANGELOG.md b/CHANGELOG.md
index fcbf9c874..6090d60f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ Types of changes can be: Added/Changed/Deprecated/Removed/Fixed/Security
## [UNRELEASED]
### Added
+- open files inside app (jpg, .png, .gif, .mp3, .mp4, .mov, .wav, .txt, .md)
- edit profile information and privacy settings
### Changed
diff --git a/app/build.gradle b/app/build.gradle
index 072d9f7ce..0a3a6ab32 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -128,7 +128,8 @@ android {
ext {
daggerVersion = "2.34.1"
powermockVersion = "2.0.9"
- workVersion = "1.0.1"
+ workVersion = "2.3.0"
+ markwonVersion = "4.6.2"
}
@@ -147,10 +148,10 @@ dependencies {
implementation 'com.github.vanniktech:Emoji:0.6.0'
implementation group: 'androidx.emoji', name: 'emoji-bundled', version: '1.1.0'
implementation 'org.michaelevans.colorart:library:0.0.3'
- implementation "android.arch.work:work-runtime:${workVersion}"
- implementation "android.arch.work:work-rxjava2:${workVersion}"
+ implementation "androidx.work:work-runtime:${workVersion}"
+ implementation "androidx.work:work-rxjava2:${workVersion}"
+ androidTestImplementation "androidx.work:work-testing:${workVersion}"
implementation 'com.google.android:flexbox:1.1.1'
- androidTestImplementation "android.arch.work:work-testing:${workVersion}"
implementation ('com.gitlab.bitfireAT:dav4jvm:f2078bc846', {
exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser
})
@@ -242,6 +243,12 @@ dependencies {
implementation 'com.afollestad.material-dialogs:lifecycle:3.1.0'
implementation 'com.google.code.gson:gson:2.8.6'
+ implementation 'com.google.android.exoplayer:exoplayer:2.13.3'
+
+ implementation 'com.github.chrisbanes:PhotoView:2.0.0'
+ implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.23'
+
+ implementation "io.noties.markwon:core:$markwonVersion"
//implementation 'com.github.dhaval2404:imagepicker:1.8'
implementation 'com.github.tobiaskaminsky:ImagePicker:extraFile-SNAPSHOT'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 51bfa3869..8ceb5add2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -54,7 +54,7 @@
-
+
@@ -108,6 +108,24 @@
android:configChanges="orientation|screenSize"
android:launchMode="singleTask" />
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt
new file mode 100644
index 000000000..55c4537e9
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt
@@ -0,0 +1,138 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * @author Dariusz Olszewski
+ * Copyright (C) 2021 Marcel Hibbe
+ * Copyright (C) 2021 Dariusz Olszewski
+ *
+ * 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.activities
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.FrameLayout
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.FileProvider
+import com.github.chrisbanes.photoview.PhotoView
+import com.nextcloud.talk.BuildConfig
+import com.nextcloud.talk.R
+import pl.droidsonroids.gif.GifDrawable
+import pl.droidsonroids.gif.GifImageView
+import java.io.File
+
+
+class FullScreenImageActivity : AppCompatActivity() {
+
+ private lateinit var path: String
+ private lateinit var imageWrapperView: FrameLayout
+ private lateinit var photoView: PhotoView
+ private lateinit var gifView: GifImageView
+
+ private var showFullscreen = false
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_preview, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return if (item.itemId == R.id.share) {
+ val shareUri = FileProvider.getUriForFile(this,
+ BuildConfig.APPLICATION_ID,
+ File(path))
+
+ val shareIntent: Intent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_STREAM, shareUri)
+ type = "image/*"
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to)))
+
+ true
+ } else {
+ super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.activity_full_screen_image)
+ setSupportActionBar(findViewById(R.id.imageview_toolbar))
+ supportActionBar?.setDisplayShowTitleEnabled(false);
+
+ imageWrapperView = findViewById(R.id.image_wrapper_view)
+ photoView = findViewById(R.id.photo_view)
+ gifView = findViewById(R.id.gif_view)
+
+ photoView.setOnPhotoTapListener{ view, x, y ->
+ toggleFullscreen()
+ }
+ photoView.setOnOutsidePhotoTapListener{
+ toggleFullscreen()
+ }
+ gifView.setOnClickListener{
+ toggleFullscreen()
+ }
+
+ val fileName = intent.getStringExtra("FILE_NAME")
+ val isGif = intent.getBooleanExtra("IS_GIF", false)
+
+ path = applicationContext.cacheDir.absolutePath + "/" + fileName
+ if (isGif) {
+ photoView.visibility = View.INVISIBLE
+ gifView.visibility = View.VISIBLE
+ val gifFromUri = GifDrawable(path)
+ gifView.setImageDrawable(gifFromUri)
+ } else {
+ gifView.visibility = View.INVISIBLE
+ photoView.visibility = View.VISIBLE
+ photoView.setImageURI(Uri.parse(path))
+ }
+ }
+
+ private fun toggleFullscreen(){
+ showFullscreen = !showFullscreen;
+ if (showFullscreen){
+ hideSystemUI()
+ supportActionBar?.hide()
+ } else{
+ showSystemUI()
+ supportActionBar?.show()
+ }
+ }
+
+ private fun hideSystemUI() {
+ window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
+ or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_FULLSCREEN)
+ }
+
+ private fun showSystemUI() {
+ window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt
new file mode 100644
index 000000000..640f581e6
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt
@@ -0,0 +1,147 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe
+ *
+ * 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.activities
+
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.WindowManager
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.FileProvider
+import autodagger.AutoInjector
+import com.google.android.exoplayer2.MediaItem
+import com.google.android.exoplayer2.Player
+import com.google.android.exoplayer2.SimpleExoPlayer
+import com.google.android.exoplayer2.ui.PlayerControlView
+import com.google.android.exoplayer2.ui.StyledPlayerView
+import com.nextcloud.talk.BuildConfig
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import java.io.File
+
+@AutoInjector(NextcloudTalkApplication::class)
+class FullScreenMediaActivity : AppCompatActivity(), Player.EventListener {
+
+ private lateinit var path: String
+ private lateinit var playerView: StyledPlayerView
+ private lateinit var player: SimpleExoPlayer
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_preview, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return if (item.itemId == R.id.share) {
+ val shareUri = FileProvider.getUriForFile(this,
+ BuildConfig.APPLICATION_ID,
+ File(path))
+
+ val shareIntent: Intent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_STREAM, shareUri)
+ type = "video/*"
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to)))
+
+ true
+ } else {
+ super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val fileName = intent.getStringExtra("FILE_NAME")
+ val isAudioOnly = intent.getBooleanExtra("AUDIO_ONLY", false)
+
+ path = applicationContext.cacheDir.absolutePath + "/" + fileName
+
+ setContentView(R.layout.activity_full_screen_media)
+ setSupportActionBar(findViewById(R.id.mediaview_toolbar))
+ supportActionBar?.setDisplayShowTitleEnabled(false);
+
+ playerView = findViewById(R.id.player_view)
+
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ playerView.showController()
+ if (isAudioOnly) {
+ playerView.controllerShowTimeoutMs = 0
+ }
+
+ playerView.setControllerVisibilityListener { v ->
+ if (v != 0) {
+ hideSystemUI()
+ supportActionBar?.hide()
+ } else {
+ showSystemUI()
+ supportActionBar?.show()
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ initializePlayer()
+
+ val mediaItem: MediaItem = MediaItem.fromUri(path)
+ player.setMediaItem(mediaItem)
+ player.prepare()
+ player.play()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ releasePlayer()
+ }
+
+ private fun initializePlayer() {
+ player = SimpleExoPlayer.Builder(applicationContext).build()
+ playerView.player = player;
+ player.playWhenReady = true
+ player.addListener(this)
+ }
+
+ private fun releasePlayer() {
+ player.release()
+ }
+
+ private fun hideSystemUI() {
+ window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
+ or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_FULLSCREEN)
+ }
+
+ private fun showSystemUI() {
+ window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt
new file mode 100644
index 000000000..c550c49dc
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt
@@ -0,0 +1,93 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe
+ *
+ * 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.activities
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.FileProvider
+import autodagger.AutoInjector
+import com.nextcloud.talk.BuildConfig
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import io.noties.markwon.Markwon
+import java.io.File
+
+
+@AutoInjector(NextcloudTalkApplication::class)
+class FullScreenTextViewerActivity : AppCompatActivity() {
+
+ private lateinit var path: String
+ private lateinit var textView: TextView
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_preview, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return if (item.itemId == R.id.share) {
+ val shareUri = FileProvider.getUriForFile(this,
+ BuildConfig.APPLICATION_ID,
+ File(path))
+
+ val shareIntent: Intent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_STREAM, shareUri)
+ type = "text/*"
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to)))
+
+ true
+ } else {
+ super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.activity_full_screen_text)
+ setSupportActionBar(findViewById(R.id.textview_toolbar))
+ supportActionBar?.setDisplayShowTitleEnabled(false);
+
+ textView = findViewById(R.id.text_view)
+
+ val fileName = intent.getStringExtra("FILE_NAME")
+ val isMarkdown = intent.getBooleanExtra("IS_MARKDOWN", false)
+ path = applicationContext.cacheDir.absolutePath + "/" + fileName
+ var text = readFile(path)
+
+ if (isMarkdown) {
+ val markwon = Markwon.create(applicationContext);
+ markwon.setMarkdown(textView, text);
+ } else {
+ textView.text = text
+ }
+ }
+
+ private fun readFile(fileName: String) = File(fileName).inputStream().readBytes().toString(Charsets.UTF_8)
+
+}
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 826611432..7e5eb51b1 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
@@ -2,7 +2,9 @@
* Nextcloud Talk application
*
* @author Mario Danic
+ * @author Marcel Hibbe
* Copyright (C) 2017-2018 Mario Danic
+ * Copyright (C) 2021 Marcel Hibbe
*
* 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
@@ -28,19 +30,21 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Handler;
+import android.util.Log;
+import android.view.Gravity;
import android.view.View;
-import android.widget.TextView;
+import android.widget.PopupMenu;
-import androidx.emoji.widget.EmojiTextView;
-
-import autodagger.AutoInjector;
-import butterknife.BindView;
-import butterknife.ButterKnife;
+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.jobs.DownloadFileToCacheWorker;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.models.json.chat.ChatMessage;
import com.nextcloud.talk.utils.AccountUtils;
@@ -48,22 +52,39 @@ import com.nextcloud.talk.utils.DisplayUtils;
import com.nextcloud.talk.utils.DrawableUtils;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.stfalcon.chatkit.messages.MessageHolders;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+
+import javax.inject.Inject;
+
+import androidx.core.content.ContextCompat;
+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 butterknife.BindView;
+import butterknife.ButterKnife;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
-import javax.inject.Inject;
-import java.util.List;
-import java.util.concurrent.Callable;
-
@AutoInjector(NextcloudTalkApplication.class)
public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageMessageViewHolder {
+ private static String TAG = "MagicPreviewMessageViewHolder";
+
@BindView(R.id.messageText)
EmojiTextView messageText;
+ View progressBar;
+
@Inject
Context context;
@@ -73,6 +94,7 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
public MagicPreviewMessageViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
+ progressBar = itemView.findViewById(R.id.progress_bar);
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
}
@@ -102,35 +124,54 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
}
if (message.getMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) {
- // it's a preview for a Nextcloud share
- messageText.setText(message.getSelectedIndividualHashMap().get("name"));
- DisplayUtils.setClickableString(message.getSelectedIndividualHashMap().get("name"), message.getSelectedIndividualHashMap().get("link"), messageText);
+ String fileName = message.getSelectedIndividualHashMap().get("name");
+ messageText.setText(fileName);
if (message.getSelectedIndividualHashMap().containsKey("mimetype")) {
- image.getHierarchy().setPlaceholderImage(context.getDrawable(DrawableUtils.INSTANCE.getDrawableResourceIdForMimeType(message.getSelectedIndividualHashMap().get("mimetype"))));
+ String mimetype = message.getSelectedIndividualHashMap().get("mimetype");
+ int drawableResourceId = DrawableUtils.INSTANCE.getDrawableResourceIdForMimeType(mimetype);
+ Drawable drawable = ContextCompat.getDrawable(context, drawableResourceId);
+ image.getHierarchy().setPlaceholderImage(drawable);
} else {
fetchFileInformation("/" + message.getSelectedIndividualHashMap().get("path"), message.activeUser);
}
+ String accountString =
+ message.activeUser.getUsername() + "@" + message.activeUser.getBaseUrl().replace("https://", "").replace("http://", "");
+
image.setOnClickListener(v -> {
-
- String accountString =
- message.activeUser.getUsername() + "@" + message.activeUser.getBaseUrl().replace("https://", "").replace("http://", "");
-
- 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("id"));
- context.startActivity(filesAppIntent);
+ String mimetype = message.getSelectedIndividualHashMap().get("mimetype");
+ if (isSupportedMimetype(mimetype)) {
+ openOrDownloadFile(message);
} else {
- Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(message.getSelectedIndividualHashMap().get("link")));
- browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(browserIntent);
+ openFileInFilesApp(message, accountString);
}
});
+
+ image.setOnLongClickListener(l -> {
+ onMessageViewLongClick(message, accountString);
+ return true;
+ });
+
+ // check if download worker is already running
+ String fileId = message.getSelectedIndividualHashMap().get("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("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) {
messageText.setText("GIPHY");
DisplayUtils.setClickableString("GIPHY", "https://giphy.com", messageText);
@@ -151,6 +192,222 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
}
}
+ public boolean isSupportedMimetype(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 openOrDownloadFile(ChatMessage message) {
+ String filename = message.getSelectedIndividualHashMap().get("name");
+ String mimetype = message.getSelectedIndividualHashMap().get("mimetype");
+ File file = new File(context.getCacheDir(), filename);
+ if (file.exists()) {
+ openFile(filename, mimetype);
+ } else {
+ String size = message.getSelectedIndividualHashMap().get("size");
+
+ if (size == null) {
+ size = "-1";
+ }
+ Integer fileSize = Integer.valueOf(size);
+
+ String fileId = message.getSelectedIndividualHashMap().get("id");
+ String path = message.getSelectedIndividualHashMap().get("path");
+ downloadFileToCache(
+ message.activeUser.getBaseUrl(),
+ message.activeUser.getUserId(),
+ message.activeUser.getAttachmentFolder(),
+ filename,
+ path,
+ mimetype,
+ fileSize,
+ fileId
+ );
+ }
+ }
+
+ 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:
+ Log.w(TAG, "no method defined for mimetype: " + mimetype);
+ }
+ }
+
+ 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("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 (isSupportedMimetype(message.getSelectedIndividualHashMap().get("mimetype"))) {
+ return;
+ }
+
+ PopupMenu popupMenu = new PopupMenu(this.context, itemView, Gravity.START);
+ popupMenu.inflate(R.menu.chat_preview_message_menu);
+
+ popupMenu.setOnMenuItemClickListener(item -> {
+ openFileInFilesApp(message, accountString);
+ return true;
+ });
+
+ popupMenu.show();
+ }
+
+ private void downloadFileToCache(String baseUrl,
+ String userId,
+ String attachmentFolder,
+ String fileName,
+ String path,
+ String mimetype,
+ Integer size,
+ String fileId) {
+
+ // 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("Download", "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, size)
+ .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) {
+ messageText.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, "image " + fileName + " was downloaded but it's not opened (view is not shown)");
+ }
+ messageText.setText(fileName);
+ progressBar.setVisibility(View.GONE);
+ break;
+
+ case FAILED:
+ messageText.setText(fileName);
+ progressBar.setVisibility(View.GONE);
+ 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
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 821a02a6d..8600ed610 100644
--- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java
+++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java
@@ -3,7 +3,9 @@
* Nextcloud Talk application
*
* @author Mario Danic
+ * @author Marcel Hibbe
* Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
+ * Copyright (C) 2021 Marcel Hibbe
*
* 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
@@ -374,6 +376,10 @@ public interface NcApi {
@Url String url,
@Body RequestBody body);
+ @GET
+ Call downloadFile(@Header("Authorization") String authorization,
+ @Url String url);
+
@DELETE
Observable deleteChatMessage(@Header("Authorization") String authorization,
@Url String url);
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 07ea1c994..5c601918a 100644
--- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
+++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
@@ -2,7 +2,9 @@
* Nextcloud Talk application
*
* @author Mario Danic
+ * @author Marcel Hibbe
* Copyright (C) 2017-2019 Mario Danic
+ * Copyright (C) 2021 Marcel Hibbe
*
* 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
@@ -435,8 +437,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
if (newMessagesCount != 0 && layoutManager != null) {
- if (layoutManager!!.findFirstCompletelyVisibleItemPosition() <
- newMessagesCount) {
+ if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < newMessagesCount) {
newMessagesCount = 0
if (popupBubble != null && popupBubble!!.isShown) {
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt
new file mode 100644
index 000000000..a01046c6e
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt
@@ -0,0 +1,157 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe
+ *
+ * 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.jobs
+
+import android.content.Context
+import android.util.Log
+import androidx.work.Data
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import autodagger.AutoInjector
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.database.user.UserUtils
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import okhttp3.ResponseBody
+import java.io.*
+import javax.inject.Inject
+
+
+@AutoInjector(NextcloudTalkApplication::class)
+class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerParameters) :
+ Worker(context, workerParameters) {
+
+ private var totalFileSize: Int = -1
+
+ @Inject
+ lateinit var ncApi: NcApi
+
+ @Inject
+ lateinit var userUtils: UserUtils
+
+ @Inject
+ lateinit var appPreferences: AppPreferences
+
+ override fun doWork(): Result {
+ NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+
+ if (totalFileSize > -1) {
+ setProgressAsync(Data.Builder().putInt(PROGRESS, 0).build())
+ }
+
+ try {
+ val currentUser = userUtils.currentUser
+ val baseUrl = inputData.getString(KEY_BASE_URL)
+ val userId = inputData.getString(KEY_USER_ID)
+ val attachmentFolder = inputData.getString(KEY_ATTACHMENT_FOLDER)
+ val fileName = inputData.getString(KEY_FILE_NAME)
+ val remotePath = inputData.getString(KEY_FILE_PATH)
+ totalFileSize = (inputData.getInt(KEY_FILE_SIZE, -1))
+
+ checkNotNull(currentUser)
+ checkNotNull(baseUrl)
+ checkNotNull(userId)
+ checkNotNull(attachmentFolder)
+ checkNotNull(fileName)
+ checkNotNull(remotePath)
+
+ val url = ApiUtils.getUrlForFileDownload(baseUrl, userId, remotePath)
+
+ return downloadFile(currentUser, url, fileName)
+ } catch (e: IllegalStateException) {
+ Log.e(javaClass.simpleName, "Something went wrong when trying to download file", e)
+ return Result.failure()
+ }
+ }
+
+ private fun downloadFile(currentUser: UserEntity, url: String, fileName: String): Result {
+ val downloadCall = ncApi.downloadFile(
+ ApiUtils.getCredentials(currentUser.username, currentUser.token),
+ url)
+
+ return executeDownload(downloadCall.execute().body(), fileName)
+ }
+
+ private fun executeDownload(body: ResponseBody?, fileName: String): Result {
+ if (body == null) {
+ Log.e(TAG, "Response body when downloading $fileName is null!")
+ return Result.failure()
+ }
+
+ var count: Int
+ val data = ByteArray(1024 * 4)
+ val bis: InputStream = BufferedInputStream(body.byteStream(), 1024 * 8)
+ val outputFile = File(context.cacheDir, fileName + "_")
+ val output: OutputStream = FileOutputStream(outputFile)
+ var total: Long = 0
+ val startTime = System.currentTimeMillis()
+ var timeCount = 1
+
+ count = bis.read(data)
+
+ while (count != -1) {
+ if (totalFileSize > -1) {
+ total += count.toLong()
+ val progress = (total * 100 / totalFileSize).toInt()
+ val currentTime = System.currentTimeMillis() - startTime
+ if (currentTime > 50 * timeCount) {
+ setProgressAsync(Data.Builder().putInt(PROGRESS, progress).build())
+ timeCount++
+ }
+ }
+ output.write(data, 0, count)
+ count = bis.read(data)
+ }
+
+ output.flush()
+ output.close()
+ bis.close()
+
+ return onDownloadComplete(fileName)
+ }
+
+ private fun onDownloadComplete(fileName: String): Result {
+ val tempFile = File(context.cacheDir, fileName + "_")
+ val targetFile = File(context.cacheDir, fileName)
+
+ return if (tempFile.renameTo(targetFile)) {
+ setProgressAsync(Data.Builder().putBoolean(SUCCESS, true).build())
+ Result.success()
+ } else {
+ Result.failure()
+ }
+ }
+
+ companion object {
+ const val TAG = "DownloadFileToCache"
+ const val KEY_BASE_URL = "KEY_BASE_URL"
+ const val KEY_USER_ID = "KEY_USER_ID"
+ const val KEY_ATTACHMENT_FOLDER = "KEY_ATTACHMENT_FOLDER"
+ const val KEY_FILE_NAME = "KEY_FILE_NAME"
+ const val KEY_FILE_PATH = "KEY_FILE_PATH"
+ const val KEY_FILE_SIZE = "KEY_FILE_SIZE"
+ const val PROGRESS = "PROGRESS"
+ const val SUCCESS = "SUCCESS"
+
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
index bbc13c299..33a97451a 100644
--- a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
+++ b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
@@ -21,9 +21,7 @@
package com.nextcloud.talk.jobs
import android.content.Context
-import android.database.Cursor
import android.net.Uri
-import android.provider.OpenableColumns
import android.util.Log
import androidx.work.*
import autodagger.AutoInjector
@@ -45,6 +43,8 @@ import io.reactivex.schedulers.Schedulers
import okhttp3.MediaType
import okhttp3.RequestBody
import retrofit2.Response
+import java.io.File
+import java.io.FileOutputStream
import java.io.InputStream
import java.util.*
import javax.inject.Inject
@@ -80,9 +80,9 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
for (index in sourcefiles.indices) {
val sourcefileUri = Uri.parse(sourcefiles[index])
- var filename = UriUtils.getFileName(sourcefileUri, context)
+ val filename = UriUtils.getFileName(sourcefileUri, context)
val requestBody = createRequestBody(sourcefileUri)
- uploadFile(currentUser, ncTargetpath, filename, roomToken, requestBody)
+ uploadFile(currentUser, ncTargetpath, filename, roomToken, requestBody, sourcefileUri)
}
} catch (e: IllegalStateException) {
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
@@ -107,7 +107,8 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
return requestBody
}
- private fun uploadFile(currentUser: UserEntity, ncTargetpath: String?, filename: String?, roomToken: String?, requestBody: RequestBody?) {
+ private fun uploadFile(currentUser: UserEntity, ncTargetpath: String?, filename: String, roomToken: String?,
+ requestBody: RequestBody?, sourcefileUri: Uri) {
ncApi.uploadFile(
ApiUtils.getCredentials(currentUser.username, currentUser.token),
ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, ncTargetpath, filename),
@@ -128,10 +129,23 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
override fun onComplete() {
shareFile(roomToken, currentUser, ncTargetpath, filename)
+ copyFileToCache(sourcefileUri, filename)
}
})
}
+ private fun copyFileToCache(sourceFileUri: Uri, filename: String) {
+ val cachedFile = File(context.cacheDir, filename)
+ val outputStream = FileOutputStream(cachedFile)
+ val inputStream: InputStream = context.contentResolver.openInputStream(sourceFileUri)!!
+
+ inputStream.use { input ->
+ outputStream.use { output ->
+ input.copyTo(output)
+ }
+ }
+ }
+
private fun shareFile(roomToken: String?, currentUser: UserEntity, ncTargetpath: String?, filename: String?) {
val paths: MutableList = ArrayList()
paths.add("$ncTargetpath/$filename")
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 d7f7bd18f..14eecdf4a 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
@@ -299,6 +299,10 @@ public class ApiUtils {
return baseUrl + "/remote.php/dav/files/" + user + attachmentFolder + "/" + filename;
}
+ public static String getUrlForFileDownload(String baseUrl, String user, String remotePath) {
+ return baseUrl + "/remote.php/dav/files/" + user + "/" + remotePath;
+ }
+
public static String getUrlForMessageDeletion(String baseUrl, String token, String messageId) {
return baseUrl + ocsApiVersion + spreedApiVersion + "/chat/" + token + "/" + messageId;
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt
index 5191036d0..a4db61cf3 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt
@@ -42,7 +42,7 @@ object UriUtils {
}
}
if (filename == null) {
- Log.e(UploadAndShareFilesWorker.TAG, "failed to get DISPLAY_NAME from uri. using fallback.")
+ Log.e("UriUtils", "failed to get DISPLAY_NAME from uri. using fallback.")
filename = uri.path
val lastIndexOfSlash = filename!!.lastIndexOf('/')
if (lastIndexOfSlash != -1) {
diff --git a/app/src/main/res/layout/activity_full_screen_image.xml b/app/src/main/res/layout/activity_full_screen_image.xml
new file mode 100644
index 000000000..47d2d59d9
--- /dev/null
+++ b/app/src/main/res/layout/activity_full_screen_image.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_full_screen_media.xml b/app/src/main/res/layout/activity_full_screen_media.xml
new file mode 100644
index 000000000..44935af75
--- /dev/null
+++ b/app/src/main/res/layout/activity_full_screen_media.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_full_screen_text.xml b/app/src/main/res/layout/activity_full_screen_text.xml
new file mode 100644
index 000000000..7e728f9e3
--- /dev/null
+++ b/app/src/main/res/layout/activity_full_screen_text.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_custom_incoming_preview_message.xml b/app/src/main/res/layout/item_custom_incoming_preview_message.xml
index d3f0b4c16..031d78c7d 100644
--- a/app/src/main/res/layout/item_custom_incoming_preview_message.xml
+++ b/app/src/main/res/layout/item_custom_incoming_preview_message.xml
@@ -2,7 +2,9 @@
~ Nextcloud Talk application
~
~ @author Mario Danic
+ ~ @author Marcel Hibbe
~ Copyright (C) 2017-2018 Mario Danic
+ ~ Copyright (C) 2021 Marcel Hibbe
~
~ 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
@@ -48,16 +50,28 @@
app:flexWrap="wrap"
app:justifyContent="flex_end">
-
+ android:adjustViewBounds="true">
+
+
+
+
+
+ ~ Copyright (C) 2021 Marcel Hibbe
~
~ 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
@@ -40,17 +42,28 @@
app:flexWrap="wrap"
app:justifyContent="flex_end">
-
+ android:adjustViewBounds="true">
+
+
+
+
+
+ app:layout_wrapBefore="true"
+ tools:text="Message" />
+ app:layout_alignSelf="center"
+ tools:text="12:34:56" />
diff --git a/app/src/main/res/menu/chat_preview_message_menu.xml b/app/src/main/res/menu/chat_preview_message_menu.xml
new file mode 100644
index 000000000..89df47bd4
--- /dev/null
+++ b/app/src/main/res/menu/chat_preview_message_menu.xml
@@ -0,0 +1,25 @@
+
+
+
diff --git a/app/src/main/res/menu/menu_preview.xml b/app/src/main/res/menu/menu_preview.xml
new file mode 100644
index 000000000..5388e67c1
--- /dev/null
+++ b/app/src/main/res/menu/menu_preview.xml
@@ -0,0 +1,25 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c6b24ee83..8633770d7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -341,6 +341,10 @@
Delete
Message deleted successfully, but it might have been leaked to other services
+ Share
+ Send to
+ Open in Files app
+
Upload local file
Share from %1$s
@@ -393,7 +397,7 @@
Published
Synchronize to trusted servers and the global and public address book
Scope toggle
-
+
Search in %s
@@ -403,4 +407,5 @@
Open main menu
Failed to save %1$s
selected
+ %1$s (%2$d)
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 76653f6c7..36b21b75c 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -133,6 +133,26 @@
- @color/white
+
+
+
+
+
+