Merge commit 'f5d70dab0c09435c77346415b09f580ebfa30102'

This commit is contained in:
drone 2022-04-28 17:20:06 +00:00
commit b9be4fc195
27 changed files with 1562 additions and 370 deletions

View file

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

View file

@ -96,6 +96,11 @@
android:name="android.max_aspect"
android:value="10" />
<activity
android:name=".activities.SharedItemsActivity"
android:exported="false"
android:theme="@style/AppTheme"/>
<activity
android:name=".activities.MainActivity"
android:label="@string/nc_app_name"

View file

@ -0,0 +1,168 @@
package com.nextcloud.talk.activities
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.tabs.TabLayout
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.SharedItemsAdapter
import com.nextcloud.talk.adapters.SharedItemsListAdapter
import com.nextcloud.talk.databinding.ActivitySharedItemsBinding
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
import com.nextcloud.talk.viewmodels.SharedItemsViewModel
class SharedItemsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySharedItemsBinding
private lateinit var viewModel: SharedItemsViewModel
private lateinit var currentTab: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
currentTab = TAB_MEDIA
val roomToken = intent.getStringExtra(KEY_ROOM_TOKEN)!!
val conversationName = intent.getStringExtra(KEY_CONVERSATION_NAME)
val userEntity = intent.getParcelableExtra<UserEntity>(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"
}
}

View file

@ -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<SharedItemsAdapter.ViewHolder>() {
companion object {
private val TAG = SharedItemsAdapter::class.simpleName
}
class ViewHolder(val binding: AttachmentItemBinding, itemView: View) : RecyclerView.ViewHolder(itemView)
var authHeader: Map<String, String> = emptyMap()
var items: List<SharedItem> = emptyList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = 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
}
}

View file

@ -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<SharedItemsListAdapter.ViewHolder>() {
companion object {
private val TAG = SharedItemsListAdapter::class.simpleName
}
class ViewHolder(val binding: AttachmentListItemBinding, itemView: View) : RecyclerView.ViewHolder(itemView)
var authHeader: Map<String, String> = emptyMap()
var items: List<SharedItem> = emptyList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = 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
}
}

View file

@ -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<List<WorkInfo>> 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<List<WorkInfo>> 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<ReadFilesystemOperation>() {
@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<ReadFilesystemOperation>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
// unused atm
}
.subscribe(new SingleObserver<ReadFilesystemOperation>() {
@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<BrowserFile> browserFileList = (List<BrowserFile>) 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<BrowserFile> browserFileList = (List<BrowserFile>) 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();
}

View file

@ -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<Response<ChatShareOverall>> getSharedItems(@Header("Authorization") String authorization, @Url String url,
@Query("objectType") String objectType,
@Nullable @Query("lastKnownMessageId") Integer lastKnownMessageId,
@Nullable @Query("limit") Integer limit);
@GET
Observable<MentionOverall> getMentionAutocompleteSuggestions(@Header("Authorization") String authorization,
@Url String url, @Query("search") String query,

View file

@ -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<BrowserController.BrowserType>(browserType))
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap<UserEntity>(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<ChatMessage>): List<ChatMessage> {
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)

View file

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

View file

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

View file

@ -0,0 +1,76 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.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<String, ChatMessage> data;
public HashMap<String, ChatMessage> getData() {
return this.data;
}
public void setData(HashMap<String, ChatMessage> 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() + ")";
}
}

View file

@ -0,0 +1,75 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.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() + ")";
}
}

View file

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

View file

@ -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<Response<ChatShareOverall>>? {
return media(type, null)
}
fun media(type: String, lastKnownMessageId: Int?): Observable<Response<ChatShareOverall>>? {
val credentials = ApiUtils.getCredentials(parameters!!.userName, parameters!!.userToken)
return ncApi.getSharedItems(
credentials,
ApiUtils.getUrlForChatSharedItems(1, parameters!!.baseUrl, parameters!!.roomToken),
type, lastKnownMessageId, 28
)
}
fun authHeader(): Map<String, String> {
return mapOf(Pair("Authorization", ApiUtils.getCredentials(parameters!!.userName, parameters!!.userToken)))
}
fun previewLink(fileId: String?): String {
return ApiUtils.getUrlForFilePreviewWithFileId(
parameters!!.baseUrl,
fileId,
sharedApplication!!.resources.getDimensionPixelSize(R.dimen.maximum_file_preview_size)
)
}
data class Parameters(
val userName: String,
val userToken: String,
val baseUrl: String,
val userEntity: UserEntity,
val roomToken: String
)
}

View file

@ -0,0 +1,9 @@
package com.nextcloud.talk.repositories
class SharedMediaItems(
val type: String,
val items: MutableList<SharedItem>,
var lastSeenId: Int?,
var moreItemsExisting: Boolean,
val authHeader: Map<String, String>
)

View file

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

View file

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

View file

@ -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<SharedMediaItems> by lazy {
MutableLiveData<SharedMediaItems>().also {
loadItems(initialType)
}
}
val sharedItems: LiveData<SharedMediaItems>
get() = _sharedItems
fun loadNextItems() {
val currentSharedItems = sharedItems.value!!
if (currentSharedItems.moreItemsExisting) {
repository.media(currentSharedItems.type, currentSharedItems.lastSeenId)?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(observer(currentSharedItems.type, false))
}
}
fun loadItems(type: String) {
repository.media(type)?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(observer(type, true))
}
private fun observer(type: String, initModel: Boolean): Observer<Response<ChatShareOverall>> {
return object : Observer<Response<ChatShareOverall>> {
var chatLastGiven: Int? = null
val items = mutableMapOf<String, SharedItem>()
override fun onSubscribe(d: Disposable) = Unit
override fun onNext(response: Response<ChatShareOverall>) {
if (response.headers()["x-chat-last-given"] != null) {
chatLastGiven = response.headers()["x-chat-last-given"]!!.toInt()
}
val mediaItems = response.body()!!.ocs!!.data
if (mediaItems != null) {
for (it in mediaItems) {
if (it.value.messageParameters.containsKey("file")) {
val fileParameters = it.value.messageParameters["file"]!!
val previewAvailable =
"yes".equals(fileParameters["preview-available"]!!, ignoreCase = true)
items[it.value.id] = SharedItem(
fileParameters["id"]!!,
fileParameters["name"]!!,
fileParameters["size"]!!.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<SharedItem>,
chatLastGiven,
moreItemsExisting,
repository.authHeader()
)
}
}
}
}
class Factory(val userEntity: UserEntity, val roomToken: String, private val initialType: String) :
ViewModelProvider
.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SharedItemsViewModel::class.java)) {
val repository = SharedItemsRepository()
repository.parameters = SharedItemsRepository.Parameters(
userEntity.userId,
userEntity.token,
userEntity.baseUrl,
userEntity,
roomToken
)
return SharedItemsViewModel(repository, initialType) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
companion object {
private val TAG = SharedItemsViewModel::class.simpleName
}
}

View file

@ -0,0 +1,25 @@
<!--
@author Google LLC
Copyright (C) 2022 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M7,15L11.5,9L15,13.5L17.5,10.5L21,15M22,4H14L12,2H6A2,2 0 0,0 4,4V16A2,2 0 0,0 6,18H22A2,2 0 0,0 24,16V6A2,2 0 0,0 22,4M2,6H0V11H0V20A2,2 0 0,0 2,22H20V20H2V6Z" />
</vector>

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Tim Krüger
~ @author Andy Scherzinger
~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
~ Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_default"
tools:context=".activities.SharedItemsActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/shared_items_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/appbar"
android:theme="?attr/actionBarPopupTheme"
app:layout_constraintTop_toTopOf="parent"
app:layout_scrollFlags="enterAlwaysCollapsed|noScroll"
app:navigationIconTint="@color/fontAppbar"
app:popupTheme="@style/appActionBarPopupMenu"
app:titleTextColor="@color/fontAppbar"
tools:title="@string/nc_app_product_name" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/shared_items_tabs"
android:layout_width="wrap_content"
android:layout_height="@dimen/min_size_clickable_area"
android:layout_marginBottom="8dp"
android:background="@color/appbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/shared_items_toolbar"
app:tabGravity="fill"
app:tabMode="scrollable"
app:tabTextAppearance="@style/TextAppearanceTab" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/image_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/shared_items_tabs"
tools:listitem="@layout/attachment_item" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Tim Krüger
~ @author Marcel Hibbe
~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/preview_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:adjustViewBounds="true"
app:layout_alignSelf="flex_start"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_flexGrow="1"
app:layout_wrapBefore="true">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp"
android:src="@drawable/ic_mimetype_file"
app:placeholderImageScaleType="fitCenter"
fresco:actualImageScaleType="centerCrop"
fresco:failureImage="@drawable/ic_mimetype_file"
fresco:placeholderImage="@drawable/ic_mimetype_file"
fresco:roundedCornerRadius="4dp" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Tim Krüger
~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/file_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_half_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_half_margin"
android:orientation="vertical">
<FrameLayout
android:id="@+id/preview_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:adjustViewBounds="true"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true">
<com.facebook.drawee.view.SimpleDraweeView
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:id="@+id/file_image"
android:layout_width="@dimen/file_icon_size"
android:layout_height="@dimen/file_icon_size"
android:padding="4dp"
android:src="@drawable/ic_mimetype_file"
app:layout_constraintTop_toTopOf="parent"
app:placeholderImageScaleType="fitCenter"
fresco:actualImageScaleType="centerCrop"
fresco:failureImage="@drawable/ic_mimetype_file"
fresco:placeholderImage="@drawable/ic_mimetype_file"
fresco:roundedCornerRadius="4dp"
tools:src="@drawable/ic_call_black_24dp"/>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/file_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/preview_container"
android:ellipsize="end"
android:lines="1"
android:textAlignment="viewStart"
android:textAppearance="@style/ListItem"
tools:text="Filename" />
</RelativeLayout>

View file

@ -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" />
</LinearLayout>
<com.yarolegovich.mp.MaterialPreferenceCategory
android:id="@+id/category_shared_items"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/settings"
android:animateLayoutChanges="true"
apc:cardBackgroundColor="@color/bg_default"
apc:cardElevation="0dp"
apc:mpc_title="Shared Items">
<com.yarolegovich.mp.MaterialStandardPreference
android:id="@+id/show_shared_items_action"
android:layout_width="match_parent"
android:layout_height="wrap_content"
apc:mp_icon="@drawable/ic_folder_multiple_image"
apc:mp_icon_tint="@color/grey_600"
apc:mp_summary="See all shared photos, voice messages, files, etc."
apc:mp_title="Shared Items" />
</com.yarolegovich.mp.MaterialPreferenceCategory>
</RelativeLayout>
</ScrollView>
</RelativeLayout>

View file

@ -37,8 +37,13 @@
<item
android:id="@+id/conversation_info"
android:icon="@drawable/ic_info_white_24dp"
android:orderInCategory="1"
android:title="@string/nc_conversation_menu_conversation_info"
app:showAsAction="never" />
<item
android:id="@+id/shared_items"
android:orderInCategory="1"
android:title="Shared Items"
app:showAsAction="never" />
</menu>

View file

@ -35,6 +35,7 @@
<dimen name="avatar_size">40dp</dimen>
<dimen name="avatar_size_app_bar">30dp</dimen>
<dimen name="avatar_size_big">96dp</dimen>
<dimen name="file_icon_size">40dp</dimen>
<dimen name="chat_text_size">14sp</dimen>
<dimen name="message_bubble_corners_radius">6dp</dimen>

View file

@ -89,7 +89,6 @@
<string name="nc_settings_server_eol">The server version is too old and not supported by this version of the Android app</string>
<string name="nc_settings_server_almost_eol">The server version is very old and will not be supported in the next release!</string>
<string name="nc_settings_warning">Warning</string>
<string name="nc_add">Add</string>
<string name="nc_settings_wrong_account">Only current account can be reauthorized</string>
<string name="nc_settings_no_talk_installed">Talk app is not installed on the server you tried to authenticate against</string>
<string name="nc_settings_account_updated">Your already existing account was updated, instead of adding a new one</string>
@ -118,7 +117,6 @@
<string name="nc_settings_screen_lock_desc">Lock %1$s with Android screen lock or supported biometric method</string>
<string name="nc_settings_screen_lock_key" translatable="false">screen_lock</string>
<string name="nc_settings_screen_lock_timeout_title">Screen lock inactivity timeout</string>
<string name="nc_none">None</string>
<string name="nc_settings_screen_lock_timeout_key" translatable="false">screen_lock_timeout</string>
<string name="nc_settings_screen_security_title">Screen security</string>
<string name="nc_settings_screen_security_desc">Prevents screenshots in the recent list and inside the app</string>
@ -205,8 +203,6 @@
<string name="nc_call_incoming">INCOMING</string>
<string name="nc_call_ringing">RINGING</string>
<string name="nc_connecting_call">Connecting…</string>
<string name="nc_calling">Calling…</string>
<string name="nc_incoming_call">Incoming call from</string>
<string name="nc_nick_guest">Guest</string>
<string name="nc_public_call">New public conversation</string>
<string name="nc_public_call_explanation">Public conversations let you invite people from outside through a specially crafted link.</string>
@ -338,7 +334,6 @@
<!-- Empty states -->
<string name="nc_conversations_empty">Join a conversation or start a new one</string>
<string name="nc_conversations_empty_details">Say hi to your friends and colleagues!</string>
<string name="nc_hello">Hello</string>
<!-- Other -->
<string name="nc_limit_hit">%s characters limit has been hit</string>
@ -379,14 +374,6 @@
<string name="nc_lobby_start_soon">The meeting will start soon</string>
<string name="nc_manual">Not set</string>
<!-- Errors -->
<string name="nc_no_connection_error">No connection</string>
<string name="nc_bad_response_error">Bad response</string>
<string name="nc_timeout_error">Timeout</string>
<string name="nc_empty_response_error">Empty response</string>
<string name="nc_not_defined_error">Unknown error</string>
<string name="nc_unauthorized_error">Unauthorized</string>
<string name="nc_allow_guests">Allow guests</string>
<string name="nc_last_moderator_title">Could not leave conversation</string>
<string name="nc_last_moderator">You need to promote a new moderator before you can leave %1$s.</string>
@ -430,6 +417,9 @@
<string name="nc_share_contact">Share contact</string>
<string name="nc_share_contact_permission">Permission to read contacts is required</string>
<!-- shared items -->
<string name="nc_shared_items">Shared items</string>
<!-- voice messages -->
<string name="nc_voice_message_filename">Talk recording from %1$s (%2$s)</string>
<string name="nc_voice_message_hold_to_record_info">Hold to record, release to send.</string>
@ -498,6 +488,7 @@
<string name="nc_dialog_invalid_password">Invalid password</string>
<string name="nc_dialog_reauth_or_delete">Do you want to reauthorize or delete this account?</string>
<!-- Take photo -->
<string name="take_photo">Take a photo</string>
<string name="take_photo_switch_camera">Switch camera</string>
<string name="take_photo_retake_photo">Re-take photo</string>
@ -507,12 +498,23 @@
<string name="take_photo_send">Send</string>
<string name="take_photo_error_deleting_picture">Error taking picture</string>
<string name="take_photo_permission">Taking a photo is not possible without permissions</string>
<!-- Audio selection -->
<string name="audio_output_bluetooth">Bluetooth</string>
<string name="audio_output_speaker">Speaker</string>
<string name="audio_output_phone">Phone</string>
<string name="audio_output_dialog_headline">Audio output</string>
<string name="audio_output_wired_headset">Wired headset</string>
<!-- Shared items -->
<string name="shared_items_media">Media</string>
<string name="shared_items_file">File</string>
<string name="shared_items_audio">Audio</string>
<string name="shared_items_voice">Voice</string>
<string name="shared_items_other">Other</string>
<string name="title_attachments">Attachments</string>
<string name="reactions_tab_all">All</string>
</resources>

View file

@ -257,4 +257,10 @@
<item name="android:textStyle">bold</item>
</style>
<style name="TextAppearanceTab" parent="TextAppearance.Design.Tab">
<item name="textAllCaps">false</item>
<item name="android:textAllCaps">false</item>
</style>
</resources>