react to given reactions inside message

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2022-11-24 14:43:20 +01:00
parent bcd35ac66c
commit 6b97197c80
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
26 changed files with 849 additions and 581 deletions

View File

@ -3,6 +3,7 @@ package com.nextcloud.talk.adapters.messages
import com.nextcloud.talk.models.json.chat.ChatMessage
interface CommonMessageInterface {
fun onClickReactions(chatMessage: ChatMessage)
fun onLongClickReactions(chatMessage: ChatMessage)
fun onClickReaction(chatMessage: ChatMessage, emoji: String)
fun onOpenMessageActionsDialog(chatMessage: ChatMessage)
}

View File

@ -98,18 +98,21 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) : M
Reaction().showReactions(
message,
::clickOnReaction,
::longClickOnReaction,
binding.reactions,
binding.messageTime.context,
false,
viewThemeUtils
)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
commonMessageInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
commonMessageInterface.onOpenMessageActionsDialog(message)
true
}
}
private fun longClickOnReaction(chatMessage: ChatMessage) {
commonMessageInterface.onLongClickReactions(chatMessage)
}
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {

View File

@ -103,18 +103,21 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
Reaction().showReactions(
message,
::clickOnReaction,
::longClickOnReaction,
binding.reactions,
binding.messageText.context,
false,
viewThemeUtils
)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
commonMessageInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
commonMessageInterface.onOpenMessageActionsDialog(message)
true
}
}
private fun longClickOnReaction(chatMessage: ChatMessage) {
commonMessageInterface.onLongClickReactions(chatMessage)
}
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {

View File

@ -89,18 +89,21 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : MessageH
Reaction().showReactions(
message,
::clickOnReaction,
::longClickOnReaction,
binding.reactions,
binding.messageTime.context,
false,
viewThemeUtils
)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
commonMessageInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
commonMessageInterface.onOpenMessageActionsDialog(message)
true
}
}
private fun longClickOnReaction(chatMessage: ChatMessage) {
commonMessageInterface.onLongClickReactions(chatMessage)
}
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
private fun setPollPreview(message: ChatMessage) {

View File

@ -35,7 +35,7 @@ import com.nextcloud.talk.models.json.chat.ChatMessage;
import androidx.core.content.ContextCompat;
import androidx.emoji.widget.EmojiTextView;
public class IncomingPreviewMessageViewHolder extends MagicPreviewMessageViewHolder {
public class IncomingPreviewMessageViewHolder extends PreviewMessageViewHolder {
private final ItemCustomIncomingPreviewMessageBinding binding;
public IncomingPreviewMessageViewHolder(View itemView, Object payload) {
@ -63,11 +63,6 @@ public class IncomingPreviewMessageViewHolder extends MagicPreviewMessageViewHol
return binding.progressBar;
}
@Override
public SimpleDraweeView getImage() {
return binding.image;
}
@Override
public View getPreviewContainer() {
return binding.previewContainer;
@ -95,4 +90,5 @@ public class IncomingPreviewMessageViewHolder extends MagicPreviewMessageViewHol
@Override
public ReactionsInsideMessageBinding getReactionsBinding(){ return binding.reactions; }
}

View File

@ -147,18 +147,21 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
Reaction().showReactions(
message,
::clickOnReaction,
::longClickOnReaction,
binding.reactions,
binding.messageTime.context,
false,
viewThemeUtils
)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
commonMessageInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
commonMessageInterface.onOpenMessageActionsDialog(message)
true
}
}
private fun longClickOnReaction(chatMessage: ChatMessage) {
commonMessageInterface.onLongClickReactions(chatMessage)
}
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
private fun updateDownloadState(message: ChatMessage) {

View File

@ -119,18 +119,21 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
Reaction().showReactions(
message,
::clickOnReaction,
::longClickOnReaction,
binding.reactions,
binding.messageText.context,
false,
viewThemeUtils
)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
commonMessageInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
commonMessageInterface.onOpenMessageActionsDialog(message)
true
}
}
private fun longClickOnReaction(chatMessage: ChatMessage) {
commonMessageInterface.onLongClickReactions(chatMessage)
}
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
private fun processAuthor(message: ChatMessage) {

View File

@ -121,14 +121,23 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.replyable)
Reaction().showReactions(message, binding.reactions, context, true, viewThemeUtils)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
commonMessageInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
commonMessageInterface.onOpenMessageActionsDialog(message)
true
}
Reaction().showReactions(
message,
::clickOnReaction,
::longClickOnReaction,
binding.reactions,
context,
true,
viewThemeUtils
)
}
private fun longClickOnReaction(chatMessage: ChatMessage) {
commonMessageInterface.onLongClickReactions(chatMessage)
}
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
private fun processParentMessage(message: ChatMessage) {

View File

@ -1,375 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Marcel Hibbe
* @author Andy Scherzinger
* @author Tim Krüger
* Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
* 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.adapters.messages;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Handler;
import android.util.Base64;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import com.facebook.drawee.view.SimpleDraweeView;
import com.google.android.material.card.MaterialCardView;
import com.nextcloud.talk.R;
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.data.user.model.User;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
import com.nextcloud.talk.models.json.chat.ChatMessage;
import com.nextcloud.talk.ui.theme.ViewThemeUtils;
import com.nextcloud.talk.utils.DisplayUtils;
import com.nextcloud.talk.utils.DrawableUtils;
import com.nextcloud.talk.utils.FileViewerUtils;
import com.stfalcon.chatkit.messages.MessageHolders;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.content.ContextCompat;
import androidx.emoji.widget.EmojiTextView;
import autodagger.AutoInjector;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
import static com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback.REPLYABLE_VIEW_TAG;
@AutoInjector(NextcloudTalkApplication.class)
public abstract class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageMessageViewHolder<ChatMessage> {
private static final String TAG = "PreviewMsgViewHolder";
public static final String KEY_CONTACT_NAME = "contact-name";
public static final String KEY_CONTACT_PHOTO = "contact-photo";
public static final String KEY_MIMETYPE = "mimetype";
public static final String KEY_ID = "id";
public static final String KEY_PATH = "path";
public static final String ACTOR_TYPE_BOTS = "bots";
public static final String ACTOR_ID_CHANGELOG = "changelog";
public static final String KEY_NAME = "name";
@Inject
Context context;
@Inject
ViewThemeUtils viewThemeUtils;
@Inject
OkHttpClient okHttpClient;
ProgressBar progressBar;
ReactionsInsideMessageBinding reactionsBinding;
FileViewerUtils fileViewerUtils;
View clickView;
CommonMessageInterface commonMessageInterface;
PreviewMessageInterface previewMessageInterface;
public MagicPreviewMessageViewHolder(View itemView, Object payload) {
super(itemView, payload);
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(ChatMessage message) {
super.onBind(message);
if (userAvatar != null) {
if (message.isGrouped() || message.isOneToOneConversation()) {
if (message.isOneToOneConversation()) {
userAvatar.setVisibility(View.GONE);
} else {
userAvatar.setVisibility(View.INVISIBLE);
}
} else {
userAvatar.setVisibility(View.VISIBLE);
userAvatar.setOnClickListener(v -> {
if (payload instanceof MessagePayload) {
((MessagePayload) payload).getProfileBottomSheet().showFor(message.getActorId(),
v.getContext());
}
});
if (ACTOR_TYPE_BOTS.equals(message.getActorType()) && ACTOR_ID_CHANGELOG.equals(message.getActorId())) {
if (context != null) {
Drawable[] layers = new Drawable[2];
layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background);
layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground);
LayerDrawable layerDrawable = new LayerDrawable(layers);
userAvatar.getHierarchy().setPlaceholderImage(DisplayUtils.getRoundedDrawable(layerDrawable));
}
}
}
}
progressBar = getProgressBar();
viewThemeUtils.platform.colorCircularProgressBar(getProgressBar());
image = getImage();
clickView = getImage();
getMessageText().setVisibility(View.VISIBLE);
if (message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) {
fileViewerUtils = new FileViewerUtils(context, message.getActiveUser());
String fileName = message.getSelectedIndividualHashMap().get(KEY_NAME);
getMessageText().setText(fileName);
if (message.getSelectedIndividualHashMap().containsKey(KEY_CONTACT_NAME)) {
getPreviewContainer().setVisibility(View.GONE);
getPreviewContactName().setText(message.getSelectedIndividualHashMap().get(KEY_CONTACT_NAME));
progressBar = getPreviewContactProgressBar();
getMessageText().setVisibility(View.INVISIBLE);
clickView = getPreviewContactContainer();
viewThemeUtils.talk.colorContactChatItemBackground(getPreviewContactContainer());
viewThemeUtils.talk.colorContactChatItemName(getPreviewContactName());
viewThemeUtils.platform.colorCircularProgressBarOnPrimaryContainer(getPreviewContactProgressBar());
} else {
getPreviewContainer().setVisibility(View.VISIBLE);
getPreviewContactContainer().setVisibility(View.GONE);
}
if (message.getSelectedIndividualHashMap().containsKey(KEY_CONTACT_PHOTO)) {
image = getPreviewContactPhoto();
Drawable drawable = getDrawableFromContactDetails(
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);
int drawableResourceId = DrawableUtils.INSTANCE.getDrawableResourceIdForMimeType(mimetype);
Drawable drawable = ContextCompat.getDrawable(context, drawableResourceId);
if (drawable != null &&
(drawableResourceId == R.drawable.ic_mimetype_folder ||
drawableResourceId == R.drawable.ic_mimetype_package_x_generic)) {
drawable.setColorFilter(viewThemeUtils.getScheme(image.getContext()).getPrimary(),
PorterDuff.Mode.SRC_ATOP);
}
image.getHierarchy().setPlaceholderImage(drawable);
} else {
fetchFileInformation("/" + message.getSelectedIndividualHashMap().get(KEY_PATH),
message.getActiveUser());
}
if (message.getActiveUser() != null &&
message.getActiveUser().getUsername() != null &&
message.getActiveUser().getBaseUrl() != null) {
clickView.setOnClickListener(v ->
fileViewerUtils.openFile(
message,
new FileViewerUtils.ProgressUi(progressBar, getMessageText(), image)
)
);
clickView.setOnLongClickListener(l -> {
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)),
message.getSelectedIndividualHashMap().get(MagicPreviewMessageViewHolder.KEY_MIMETYPE),
new FileViewerUtils.ProgressUi(progressBar, getMessageText(), image));
} else if (message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE) {
getMessageText().setText("GIPHY");
DisplayUtils.setClickableString("GIPHY", "https://giphy.com", getMessageText());
} else if (message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE) {
getMessageText().setText("Tenor");
DisplayUtils.setClickableString("Tenor", "https://tenor.com", getMessageText());
} else {
if (message.getMessageType().equals(ChatMessage.MessageType.SINGLE_LINK_IMAGE_MESSAGE)) {
clickView.setOnClickListener(v -> {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(message.getImageUrl()));
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(browserIntent);
});
} else {
clickView.setOnClickListener(null);
}
getMessageText().setText("");
}
itemView.setTag(REPLYABLE_VIEW_TAG, message.getReplyable());
reactionsBinding = getReactionsBinding();
new Reaction().showReactions(message,
reactionsBinding,
getMessageText().getContext(),
true,
viewThemeUtils);
reactionsBinding.reactionsEmojiWrapper.setOnClickListener(l -> {
commonMessageInterface.onClickReactions(message);
});
reactionsBinding.reactionsEmojiWrapper.setOnLongClickListener(l -> {
commonMessageInterface.onOpenMessageActionsDialog(message);
return true;
});
}
private Drawable getDrawableFromContactDetails(Context context, String base64) {
Drawable drawable = null;
if (!base64.equals("")) {
ByteArrayInputStream inputStream = new ByteArrayInputStream(
Base64.decode(base64.getBytes(), Base64.DEFAULT));
drawable = Drawable.createFromResourceStream(context.getResources(),
null, inputStream, null, null);
try {
inputStream.close();
} catch (IOException e) {
int drawableResourceId = DrawableUtils.INSTANCE.getDrawableResourceIdForMimeType("text/vcard");
drawable = ContextCompat.getDrawable(context, drawableResourceId);
}
}
return drawable;
}
private void onMessageViewLongClick(ChatMessage message) {
if (fileViewerUtils.isSupportedForInternalViewer(message.getSelectedIndividualHashMap().get(KEY_MIMETYPE))) {
previewMessageInterface.onPreviewMessageLongClick(message);
return;
}
Context viewContext;
if (itemView != null && itemView.getContext() != null) {
viewContext = itemView.getContext();
} else {
viewContext = this.context;
}
PopupMenu popupMenu = new PopupMenu(
new ContextThemeWrapper(viewContext, R.style.appActionBarPopupMenu),
itemView,
Gravity.START
);
popupMenu.inflate(R.menu.chat_preview_message_menu);
popupMenu.setOnMenuItemClickListener(item -> {
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();
}
private void fetchFileInformation(String url, User activeUser) {
Single.fromCallable(new Callable<ReadFilesystemOperation>() {
@Override
public ReadFilesystemOperation call() {
return new ReadFilesystemOperation(okHttpClient, activeUser, url, 0);
}
}).observeOn(Schedulers.io())
.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).getMimeType());
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);
}
});
}
public void assignCommonMessageInterface(CommonMessageInterface commonMessageInterface) {
this.commonMessageInterface = commonMessageInterface;
}
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 MaterialCardView getPreviewContactContainer();
public abstract SimpleDraweeView getPreviewContactPhoto();
public abstract EmojiTextView getPreviewContactName();
public abstract ProgressBar getPreviewContactProgressBar();
public abstract ReactionsInsideMessageBinding getReactionsBinding();
}

View File

@ -116,18 +116,21 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) :
Reaction().showReactions(
message,
::clickOnReaction,
::longClickOnReaction,
binding.reactions,
binding.messageTime.context,
true,
viewThemeUtils
)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
commonMessageInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
commonMessageInterface.onOpenMessageActionsDialog(message)
true
}
}
private fun longClickOnReaction(chatMessage: ChatMessage) {
commonMessageInterface.onLongClickReactions(chatMessage)
}
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {

View File

@ -122,18 +122,21 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
Reaction().showReactions(
message,
::clickOnReaction,
::longClickOnReaction,
binding.reactions,
binding.messageText.context,
true,
viewThemeUtils
)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
commonMessageInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
commonMessageInterface.onOpenMessageActionsDialog(message)
true
}
}
private fun longClickOnReaction(chatMessage: ChatMessage) {
commonMessageInterface.onLongClickReactions(chatMessage)
}
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
@SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")

View File

@ -108,18 +108,21 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : Messag
Reaction().showReactions(
message,
::clickOnReaction,
::longClickOnReaction,
binding.reactions,
binding.messageTime.context,
true,
viewThemeUtils
)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
commonMessageInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
commonMessageInterface.onOpenMessageActionsDialog(message)
true
}
}
private fun longClickOnReaction(chatMessage: ChatMessage) {
commonMessageInterface.onLongClickReactions(chatMessage)
}
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
private fun setPollPreview(message: ChatMessage) {

View File

@ -35,7 +35,7 @@ import com.nextcloud.talk.models.json.chat.ChatMessage;
import androidx.core.content.ContextCompat;
import androidx.emoji.widget.EmojiTextView;
public class OutcomingPreviewMessageViewHolder extends MagicPreviewMessageViewHolder {
public class OutcomingPreviewMessageViewHolder extends PreviewMessageViewHolder {
private final ItemCustomOutcomingPreviewMessageBinding binding;
@ -64,11 +64,6 @@ public class OutcomingPreviewMessageViewHolder extends MagicPreviewMessageViewHo
return binding.progressBar;
}
@Override
public SimpleDraweeView getImage() {
return binding.image;
}
@Override
public View getPreviewContainer() {
return binding.previewContainer;

View File

@ -140,18 +140,21 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
Reaction().showReactions(
message,
::clickOnReaction,
::longClickOnReaction,
binding.reactions,
binding.messageTime.context,
true,
viewThemeUtils
)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
commonMessageInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
commonMessageInterface.onOpenMessageActionsDialog(message)
true
}
}
private fun longClickOnReaction(chatMessage: ChatMessage) {
commonMessageInterface.onLongClickReactions(chatMessage)
}
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
private fun handleResetVoiceMessageState(message: ChatMessage) {

View File

@ -0,0 +1,341 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Marcel Hibbe
* @author Andy Scherzinger
* @author Tim Krüger
* Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
* 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.adapters.messages
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Handler
import android.util.Base64
import android.util.Log
import android.view.Gravity
import android.view.MenuItem
import android.view.View
import android.widget.PopupMenu
import android.widget.ProgressBar
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.ContextCompat
import androidx.emoji.widget.EmojiTextView
import autodagger.AutoInjector
import com.facebook.drawee.view.SimpleDraweeView
import com.google.android.material.card.MaterialCardView
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.components.filebrowser.models.BrowserFile
import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType
import com.nextcloud.talk.utils.FileViewerUtils
import com.nextcloud.talk.utils.FileViewerUtils.ProgressUi
import com.stfalcon.chatkit.messages.MessageHolders.IncomingImageMessageViewHolder
import io.reactivex.Single
import io.reactivex.SingleObserver
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import okhttp3.OkHttpClient
import java.io.ByteArrayInputStream
import java.io.IOException
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
IncomingImageMessageViewHolder<ChatMessage>(itemView, payload) {
@JvmField
@Inject
var context: Context? = null
@JvmField
@Inject
var viewThemeUtils: ViewThemeUtils? = null
@JvmField
@Inject
var okHttpClient: OkHttpClient? = null
open var progressBar: ProgressBar? = null
open var reactionsBinding: ReactionsInsideMessageBinding? = null
var fileViewerUtils: FileViewerUtils? = null
var clickView: View? = null
lateinit var commonMessageInterface: CommonMessageInterface
var previewMessageInterface: PreviewMessageInterface? = null
init {
sharedApplication!!.componentApplication.inject(this)
}
@SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) {
super.onBind(message)
if (userAvatar != null) {
if (message.isGrouped || message.isOneToOneConversation) {
if (message.isOneToOneConversation) {
userAvatar.visibility = View.GONE
} else {
userAvatar.visibility = View.INVISIBLE
}
} else {
userAvatar.visibility = View.VISIBLE
userAvatar.setOnClickListener { v: View ->
if (payload is MessagePayload) {
(payload as MessagePayload).profileBottomSheet.showFor(
message.actorId!!,
v.context
)
}
}
if (ACTOR_TYPE_BOTS == message.actorType && ACTOR_ID_CHANGELOG == message.actorId) {
if (context != null) {
val layers = arrayOfNulls<Drawable>(2)
layers[0] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_background)
layers[1] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_foreground)
val layerDrawable = LayerDrawable(layers)
userAvatar.hierarchy.setPlaceholderImage(DisplayUtils.getRoundedDrawable(layerDrawable))
}
}
}
}
viewThemeUtils!!.platform.colorCircularProgressBar(progressBar!!)
clickView = image
messageText.visibility = View.VISIBLE
if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) {
fileViewerUtils = FileViewerUtils(context!!, message.activeUser!!)
val fileName = message.selectedIndividualHashMap!![KEY_NAME]
messageText.text = fileName
if (message.selectedIndividualHashMap!!.containsKey(KEY_CONTACT_NAME)) {
previewContainer.visibility = View.GONE
previewContactName.text = message.selectedIndividualHashMap!![KEY_CONTACT_NAME]
progressBar = previewContactProgressBar
messageText.visibility = View.INVISIBLE
clickView = previewContactContainer
viewThemeUtils!!.talk.colorContactChatItemBackground(previewContactContainer)
viewThemeUtils!!.talk.colorContactChatItemName(previewContactName)
viewThemeUtils!!.platform.colorCircularProgressBarOnPrimaryContainer(previewContactProgressBar!!)
} else {
previewContainer.visibility = View.VISIBLE
previewContactContainer.visibility = View.GONE
}
if (message.selectedIndividualHashMap!!.containsKey(KEY_CONTACT_PHOTO)) {
image = previewContactPhoto
val drawable = getDrawableFromContactDetails(
context,
message.selectedIndividualHashMap!![KEY_CONTACT_PHOTO]
)
image.hierarchy.setPlaceholderImage(drawable)
} else if (message.selectedIndividualHashMap!!.containsKey(KEY_MIMETYPE)) {
val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE]
val drawableResourceId = getDrawableResourceIdForMimeType(mimetype)
val drawable = ContextCompat.getDrawable(context!!, drawableResourceId)
if (drawable != null &&
(
drawableResourceId == R.drawable.ic_mimetype_folder ||
drawableResourceId == R.drawable.ic_mimetype_package_x_generic
)
) {
drawable.setColorFilter(
viewThemeUtils!!.getScheme(image.context).primary,
PorterDuff.Mode.SRC_ATOP
)
}
image.hierarchy.setPlaceholderImage(drawable)
} else {
fetchFileInformation(
"/" + message.selectedIndividualHashMap!![KEY_PATH],
message.activeUser
)
}
if (message.activeUser != null &&
message.activeUser!!.username != null &&
message.activeUser!!.baseUrl != null
) {
clickView!!.setOnClickListener { v: View? ->
fileViewerUtils!!.openFile(
message,
ProgressUi(progressBar, messageText, image)
)
}
clickView!!.setOnLongClickListener { l: View? ->
onMessageViewLongClick(message)
true
}
} else {
Log.e(TAG, "failed to set click listener because activeUser, username or baseUrl were null")
}
fileViewerUtils!!.resumeToUpdateViewsByProgress(
message.selectedIndividualHashMap!![KEY_NAME]!!,
message.selectedIndividualHashMap!![KEY_ID]!!,
message.selectedIndividualHashMap!![KEY_MIMETYPE],
ProgressUi(progressBar, messageText, image)
)
} else if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE) {
messageText.text = "GIPHY"
DisplayUtils.setClickableString("GIPHY", "https://giphy.com", messageText)
} else if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE) {
messageText.text = "Tenor"
DisplayUtils.setClickableString("Tenor", "https://tenor.com", messageText)
} else {
if (message.messageType == ChatMessage.MessageType.SINGLE_LINK_IMAGE_MESSAGE.name) {
(clickView as SimpleDraweeView?)?.setOnClickListener {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(message.imageUrl))
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context!!.startActivity(browserIntent)
}
} else {
(clickView as SimpleDraweeView?)?.setOnClickListener(null)
}
messageText.text = ""
}
itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.replyable)
Reaction().showReactions(
message,
::clickOnReaction,
::longClickOnReaction,
reactionsBinding!!,
messageText.context,
true,
viewThemeUtils!!
)
}
private fun longClickOnReaction(chatMessage: ChatMessage) {
commonMessageInterface.onLongClickReactions(chatMessage)
}
private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) {
commonMessageInterface.onClickReaction(chatMessage, emoji)
}
private fun getDrawableFromContactDetails(context: Context?, base64: String?): Drawable? {
var drawable: Drawable? = null
if (base64 != "") {
val inputStream = ByteArrayInputStream(
Base64.decode(base64!!.toByteArray(), Base64.DEFAULT)
)
drawable = Drawable.createFromResourceStream(
context!!.resources,
null, inputStream, null, null
)
try {
inputStream.close()
} catch (e: IOException) {
val drawableResourceId = getDrawableResourceIdForMimeType("text/vcard")
drawable = ContextCompat.getDrawable(context, drawableResourceId)
}
}
return drawable
}
private fun onMessageViewLongClick(message: ChatMessage) {
if (fileViewerUtils!!.isSupportedForInternalViewer(message.selectedIndividualHashMap!![KEY_MIMETYPE])) {
previewMessageInterface!!.onPreviewMessageLongClick(message)
return
}
val viewContext: Context? = if (itemView.context != null) {
itemView.context
} else {
context
}
val popupMenu = PopupMenu(
ContextThemeWrapper(viewContext, R.style.appActionBarPopupMenu),
itemView,
Gravity.START
)
popupMenu.inflate(R.menu.chat_preview_message_menu)
popupMenu.setOnMenuItemClickListener { item: MenuItem ->
if (item.itemId == R.id.openInFiles) {
val keyID = message.selectedIndividualHashMap!![KEY_ID]
val link = message.selectedIndividualHashMap!!["link"]
fileViewerUtils!!.openFileInFilesApp(link!!, keyID!!)
}
true
}
popupMenu.show()
}
private fun fetchFileInformation(url: String, activeUser: User?) {
Single.fromCallable { ReadFilesystemOperation(okHttpClient, activeUser, url, 0) }
.observeOn(Schedulers.io())
.subscribe(object : SingleObserver<ReadFilesystemOperation> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onSuccess(readFilesystemOperation: ReadFilesystemOperation) {
val davResponse = readFilesystemOperation.readRemotePath()
if (davResponse.data != null) {
val browserFileList = davResponse.data as List<BrowserFile>
if (browserFileList.isNotEmpty()) {
Handler(context!!.mainLooper).post {
val resourceId = getDrawableResourceIdForMimeType(browserFileList[0].mimeType)
val drawable = ContextCompat.getDrawable(context!!, resourceId)
image.hierarchy.setPlaceholderImage(drawable)
}
}
}
}
override fun onError(e: Throwable) {
Log.e(TAG, "Error reading file information", e)
}
})
}
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
this.commonMessageInterface = commonMessageInterface
}
fun assignPreviewMessageInterface(previewMessageInterface: PreviewMessageInterface?) {
this.previewMessageInterface = previewMessageInterface
}
abstract val messageText: EmojiTextView
abstract val previewContainer: View
abstract val previewContactContainer: MaterialCardView
abstract val previewContactPhoto: SimpleDraweeView
abstract val previewContactName: EmojiTextView
abstract val previewContactProgressBar: ProgressBar?
companion object {
private const val TAG = "PreviewMsgViewHolder"
const val KEY_CONTACT_NAME = "contact-name"
const val KEY_CONTACT_PHOTO = "contact-photo"
const val KEY_MIMETYPE = "mimetype"
const val KEY_ID = "id"
const val KEY_PATH = "path"
const val ACTOR_TYPE_BOTS = "bots"
const val ACTOR_ID_CHANGELOG = "changelog"
const val KEY_NAME = "name"
}
}

View File

@ -37,17 +37,22 @@ class Reaction {
fun showReactions(
message: ChatMessage,
clickOnReaction: (message: ChatMessage, emoji: String) -> Unit,
longClickOnReaction: (message: ChatMessage) -> Unit,
binding: ReactionsInsideMessageBinding,
context: Context,
isOutgoingMessage: Boolean,
viewThemeUtils: ViewThemeUtils
) {
binding.reactionsEmojiWrapper.removeAllViews()
if (message.reactions != null && message.reactions!!.isNotEmpty()) {
binding.reactionsEmojiWrapper.visibility = View.VISIBLE
var remainingEmojisToDisplay = MAX_EMOJIS_TO_DISPLAY
val showInfoAboutMoreEmojis = message.reactions!!.size > MAX_EMOJIS_TO_DISPLAY
binding.reactionsEmojiWrapper.setOnLongClickListener {
longClickOnReaction(message)
true
}
val amountParams = getAmountLayoutParams(context)
val wrapperParams = getWrapperLayoutParams(context)
@ -78,13 +83,15 @@ class Reaction {
),
)
binding.reactionsEmojiWrapper.addView(emojiWithAmountWrapper)
remainingEmojisToDisplay--
if (remainingEmojisToDisplay == 0 && showInfoAboutMoreEmojis) {
binding.reactionsEmojiWrapper.addView(getMoreReactionsTextView(context, textColor))
break
emojiWithAmountWrapper.setOnClickListener {
clickOnReaction(message, emoji)
}
emojiWithAmountWrapper.setOnLongClickListener {
longClickOnReaction(message)
false
}
binding.reactionsEmojiWrapper.addView(emojiWithAmountWrapper)
}
} else {
binding.reactionsEmojiWrapper.visibility = View.GONE
@ -132,13 +139,6 @@ class Reaction {
return emojiWithAmountWrapper
}
private fun getMoreReactionsTextView(context: Context, textColor: Int): TextView {
val infoAboutMoreEmojis = TextView(context)
infoAboutMoreEmojis.setTextColor(textColor)
infoAboutMoreEmojis.text = EMOJI_MORE
return infoAboutMoreEmojis
}
private fun getEmojiTextView(context: Context, emoji: String): EmojiTextView {
val reactionEmoji = EmojiTextView(context)
reactionEmoji.text = emoji
@ -202,12 +202,10 @@ class Reaction {
)
companion object {
const val MAX_EMOJIS_TO_DISPLAY = 4
const val AMOUNT_START_MARGIN: Float = 2F
const val EMOJI_END_MARGIN: Float = 6F
const val EMOJI_AND_AMOUNT_PADDING_SIDE: Float = 4F
const val WRAPPER_PADDING_TOP: Float = 2F
const val WRAPPER_PADDING_BOTTOM: Float = 3F
const val EMOJI_MORE = ""
}
}

View File

@ -71,9 +71,9 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
((OutcomingVoiceMessageViewHolder) holder).assignVoiceMessageInterface(chatController);
((OutcomingVoiceMessageViewHolder) holder).assignCommonMessageInterface(chatController);
} else if (holder instanceof MagicPreviewMessageViewHolder) {
((MagicPreviewMessageViewHolder) holder).assignPreviewMessageInterface(chatController);
((MagicPreviewMessageViewHolder) holder).assignCommonMessageInterface(chatController);
} else if (holder instanceof PreviewMessageViewHolder) {
((PreviewMessageViewHolder) holder).assignPreviewMessageInterface(chatController);
((PreviewMessageViewHolder) holder).assignCommonMessageInterface(chatController);
}
}
}

View File

@ -7,7 +7,7 @@
* @author Tim Krüger
* Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2021-2022 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
@ -138,6 +138,8 @@ import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
import com.nextcloud.talk.jobs.ShareOperationWorker
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
import com.nextcloud.talk.messagesearch.MessageSearchActivity
import com.nextcloud.talk.models.domain.ReactionAddedModel
import com.nextcloud.talk.models.domain.ReactionDeletedModel
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.chat.ChatOverall
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
@ -150,6 +152,7 @@ import com.nextcloud.talk.models.json.mention.Mention
import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
import com.nextcloud.talk.presenters.MentionAutocompletePresenter
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
import com.nextcloud.talk.ui.dialog.AttachmentDialog
@ -235,6 +238,9 @@ class ChatController(args: Bundle) :
@Inject
lateinit var eventBus: EventBus
@Inject
lateinit var reactionsRepository: ReactionsRepository
@Inject
lateinit var permissionUtil: PlatformPermissionUtil
@ -1225,7 +1231,7 @@ class ChatController(args: Bundle) :
}
}
fun vibrate() {
private fun vibrate() {
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
if (Build.VERSION.SDK_INT >= O) {
vibrator.vibrate(VibrationEffect.createOneShot(SHORT_VIBRATE, VibrationEffect.DEFAULT_AMPLITUDE))
@ -2788,7 +2794,22 @@ class ChatController(args: Bundle) :
}
}
override fun onClickReactions(chatMessage: ChatMessage) {
override fun onClickReaction(chatMessage: ChatMessage, emoji: String) {
vibrate()
if (chatMessage.reactionsSelf?.contains(emoji) == true) {
reactionsRepository.deleteReaction(currentConversation!!, chatMessage, emoji)
.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(ReactionDeletedObserver())
} else {
reactionsRepository.addReaction(currentConversation!!, chatMessage, emoji)
.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(ReactionAddedObserver())
}
}
override fun onLongClickReactions(chatMessage: ChatMessage) {
activity?.let {
ShowReactionsDialog(
activity!!,
@ -2801,6 +2822,52 @@ class ChatController(args: Bundle) :
}
}
inner class ReactionAddedObserver : Observer<ReactionAddedModel> {
override fun onSubscribe(d: Disposable) {
}
override fun onNext(reactionAddedModel: ReactionAddedModel) {
Log.d(TAG, "onNext")
if (reactionAddedModel.success) {
updateUiToAddReaction(
reactionAddedModel.chatMessage,
reactionAddedModel.emoji
)
}
}
override fun onError(e: Throwable) {
Log.d(TAG, "onError")
}
override fun onComplete() {
Log.d(TAG, "onComplete")
}
}
inner class ReactionDeletedObserver : Observer<ReactionDeletedModel> {
override fun onSubscribe(d: Disposable) {
}
override fun onNext(reactionDeletedModel: ReactionDeletedModel) {
Log.d(TAG, "onNext")
if (reactionDeletedModel.success) {
updateUiToDeleteReaction(
reactionDeletedModel.chatMessage,
reactionDeletedModel.emoji
)
}
}
override fun onError(e: Throwable) {
Log.d(TAG, "onError")
}
override fun onComplete() {
Log.d(TAG, "onComplete")
}
}
override fun onOpenMessageActionsDialog(chatMessage: ChatMessage) {
openMessageActionsDialog(chatMessage)
}
@ -2823,8 +2890,7 @@ class ChatController(args: Bundle) :
conversationUser,
currentConversation,
isShowMessageDeletionButton(message),
participantPermissions.hasChatPermission(),
ncApi
participantPermissions.hasChatPermission()
).show()
}
}
@ -3126,7 +3192,7 @@ class ChatController(args: Bundle) :
adapter?.update(messageTemp)
}
fun updateAdapterAfterSendReaction(message: ChatMessage, emoji: String) {
fun updateUiToAddReaction(message: ChatMessage, emoji: String) {
if (message.reactions == null) {
message.reactions = LinkedHashMap()
}
@ -3144,6 +3210,24 @@ class ChatController(args: Bundle) :
adapter?.update(message)
}
fun updateUiToDeleteReaction(message: ChatMessage, emoji: String) {
if (message.reactions == null) {
message.reactions = LinkedHashMap()
}
if (message.reactionsSelf == null) {
message.reactionsSelf = ArrayList<String>()
}
var amount = message.reactions!![emoji]
if (amount == null) {
amount = 0
}
message.reactions!![emoji] = amount - 1
message.reactionsSelf!!.remove(emoji)
adapter?.update(message)
}
private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
if (conversationUser == null) return false

View File

@ -3,6 +3,8 @@
*
* @author Álvaro Brey
* @author Andy Scherzinger
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Nextcloud GmbH
@ -35,6 +37,8 @@ import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsR
import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepositoryImpl
import com.nextcloud.talk.repositories.conversations.ConversationsRepository
import com.nextcloud.talk.repositories.conversations.ConversationsRepositoryImpl
import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.repositories.reactions.ReactionsRepositoryImpl
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepositoryImpl
import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository
@ -82,4 +86,9 @@ class RepositoryModule {
fun provideArbitraryStoragesRepository(database: TalkDatabase): ArbitraryStoragesRepository {
return ArbitraryStoragesRepositoryImpl(database.arbitraryStoragesDao())
}
@Provides
fun provideReactionsRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ReactionsRepository {
return ReactionsRepositoryImpl(ncApi, userProvider)
}
}

View File

@ -0,0 +1,29 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* 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/>.
*/
package com.nextcloud.talk.models.domain
import com.nextcloud.talk.models.json.chat.ChatMessage
data class ReactionAddedModel(
var chatMessage: ChatMessage,
var emoji: String,
var success: Boolean
)

View File

@ -0,0 +1,29 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* 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/>.
*/
package com.nextcloud.talk.models.domain
import com.nextcloud.talk.models.json.chat.ChatMessage
data class ReactionDeletedModel(
var chatMessage: ChatMessage,
var emoji: String,
var success: Boolean
)

View File

@ -0,0 +1,42 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* 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 <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.repositories.reactions
import com.nextcloud.talk.models.domain.ReactionAddedModel
import com.nextcloud.talk.models.domain.ReactionDeletedModel
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation
import io.reactivex.Observable
interface ReactionsRepository {
fun addReaction(
currentConversation: Conversation,
message: ChatMessage,
emoji: String
): Observable<ReactionAddedModel>
fun deleteReaction(
currentConversation: Conversation,
message: ChatMessage,
emoji: String
): Observable<ReactionDeletedModel>
}

View File

@ -0,0 +1,102 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* 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 <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.repositories.reactions
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ReactionAddedModel
import com.nextcloud.talk.models.domain.ReactionDeletedModel
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.generic.GenericMeta
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import io.reactivex.Observable
class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: CurrentUserProviderNew) :
ReactionsRepository {
val currentUser: User = currentUserProvider.currentUser.blockingGet()
val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
override fun addReaction(
currentConversation: Conversation,
message: ChatMessage,
emoji: String
): Observable<ReactionAddedModel> {
return ncApi.sendReaction(
credentials,
ApiUtils.getUrlForMessageReaction(
currentUser.baseUrl,
currentConversation.token,
message.id
),
emoji
).map { mapToReactionAddedModel(message, emoji, it.ocs?.meta!!) }
}
override fun deleteReaction(
currentConversation: Conversation,
message: ChatMessage,
emoji: String
): Observable<ReactionDeletedModel> {
return ncApi.deleteReaction(
credentials,
ApiUtils.getUrlForMessageReaction(
currentUser.baseUrl,
currentConversation.token,
message.id
),
emoji
).map { mapToReactionDeletedModel(message, emoji, it.ocs?.meta!!) }
}
private fun mapToReactionAddedModel(
message: ChatMessage,
emoji: String,
reactionResponse: GenericMeta
): ReactionAddedModel {
val success = reactionResponse.statusCode == HTTP_CREATED
return ReactionAddedModel(
message,
emoji,
success
)
}
private fun mapToReactionDeletedModel(
message: ChatMessage,
emoji: String,
reactionResponse: GenericMeta
): ReactionDeletedModel {
val success = reactionResponse.statusCode == HTTP_OK
return ReactionDeletedModel(
message,
emoji,
success
)
}
companion object {
private const val HTTP_OK: Int = 200
private const val HTTP_CREATED: Int = 201
}
}

View File

@ -2,6 +2,8 @@
* Nextcloud Talk application
*
* @author Andy Scherzinger
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
*
* This program is free software: you can redistribute it and/or modify
@ -25,7 +27,6 @@ import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
@ -35,16 +36,16 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.controllers.ChatController
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.DialogMessageActionsBinding
import com.nextcloud.talk.models.domain.ReactionAddedModel
import com.nextcloud.talk.models.domain.ReactionDeletedModel
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
import com.vanniktech.emoji.EmojiPopup
import com.vanniktech.emoji.EmojiTextView
@ -63,13 +64,15 @@ class MessageActionsDialog(
private val user: User?,
private val currentConversation: Conversation?,
private val showMessageDeletionButton: Boolean,
private val hasChatPermission: Boolean,
private val ncApi: NcApi
private val hasChatPermission: Boolean
) : BottomSheetDialog(chatController.activity!!) {
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
@Inject
lateinit var reactionsRepository: ReactionsRepository
private lateinit var dialogMessageActionsBinding: DialogMessageActionsBinding
private lateinit var popup: EmojiPopup
@ -138,7 +141,7 @@ class MessageActionsDialog(
},
onEmojiClickListener = {
popup.dismiss()
sendReaction(message, it.unicode)
clickOnEmoji(message, it.unicode)
},
onEmojiPopupDismissListener = {
dialogMessageActionsBinding.emojiMore.clearFocus()
@ -180,27 +183,27 @@ class MessageActionsDialog(
) {
checkAndSetEmojiSelfReaction(dialogMessageActionsBinding.emojiThumbsUp)
dialogMessageActionsBinding.emojiThumbsUp.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiThumbsUp.text.toString())
clickOnEmoji(message, dialogMessageActionsBinding.emojiThumbsUp.text.toString())
}
checkAndSetEmojiSelfReaction(dialogMessageActionsBinding.emojiThumbsDown)
dialogMessageActionsBinding.emojiThumbsDown.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiThumbsDown.text.toString())
clickOnEmoji(message, dialogMessageActionsBinding.emojiThumbsDown.text.toString())
}
checkAndSetEmojiSelfReaction(dialogMessageActionsBinding.emojiLaugh)
dialogMessageActionsBinding.emojiLaugh.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiLaugh.text.toString())
clickOnEmoji(message, dialogMessageActionsBinding.emojiLaugh.text.toString())
}
checkAndSetEmojiSelfReaction(dialogMessageActionsBinding.emojiHeart)
dialogMessageActionsBinding.emojiHeart.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiHeart.text.toString())
clickOnEmoji(message, dialogMessageActionsBinding.emojiHeart.text.toString())
}
checkAndSetEmojiSelfReaction(dialogMessageActionsBinding.emojiConfused)
dialogMessageActionsBinding.emojiConfused.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiConfused.text.toString())
clickOnEmoji(message, dialogMessageActionsBinding.emojiConfused.text.toString())
}
checkAndSetEmojiSelfReaction(dialogMessageActionsBinding.emojiSad)
dialogMessageActionsBinding.emojiSad.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiSad.text.toString())
clickOnEmoji(message, dialogMessageActionsBinding.emojiSad.text.toString())
}
dialogMessageActionsBinding.emojiMore.setOnClickListener {
@ -302,88 +305,66 @@ class MessageActionsDialog(
}
}
private fun sendReaction(message: ChatMessage, emoji: String) {
private fun clickOnEmoji(message: ChatMessage, emoji: String) {
if (message.reactionsSelf?.contains(emoji) == true) {
deleteReaction(message, emoji)
reactionsRepository.deleteReaction(currentConversation!!, message, emoji)
.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(ReactionDeletedObserver())
} else {
addReaction(message, emoji)
reactionsRepository.addReaction(currentConversation!!, message, emoji)
.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(ReactionAddedObserver())
}
}
private fun addReaction(message: ChatMessage, emoji: String) {
val credentials = ApiUtils.getCredentials(user?.username, user?.token)
inner class ReactionAddedObserver : Observer<ReactionAddedModel> {
override fun onSubscribe(d: Disposable) {
}
ncApi.sendReaction(
credentials,
ApiUtils.getUrlForMessageReaction(
user?.baseUrl,
currentConversation!!.token,
message.id
),
emoji
)
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(reactionAddedModel: ReactionAddedModel) {
if (reactionAddedModel.success) {
chatController.updateUiToAddReaction(
reactionAddedModel.chatMessage,
reactionAddedModel.emoji
)
}
}
override fun onNext(genericOverall: GenericOverall) {
val statusCode = genericOverall.ocs?.meta?.statusCode
if (statusCode == HTTP_CREATED) {
chatController.updateAdapterAfterSendReaction(message, emoji)
}
}
override fun onError(e: Throwable) {
}
override fun onError(e: Throwable) {
Log.e(TAG, "error while sending reaction: $emoji")
}
override fun onComplete() {
dismiss()
}
})
override fun onComplete() {
dismiss()
}
}
private fun deleteReaction(message: ChatMessage, emoji: String) {
val credentials = ApiUtils.getCredentials(user?.username, user?.token)
inner class ReactionDeletedObserver : Observer<ReactionDeletedModel> {
override fun onSubscribe(d: Disposable) {
}
ncApi.deleteReaction(
credentials,
ApiUtils.getUrlForMessageReaction(
user?.baseUrl,
currentConversation!!.token,
message.id
),
emoji
)
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(reactionDeletedModel: ReactionDeletedModel) {
if (reactionDeletedModel.success) {
chatController.updateUiToDeleteReaction(
reactionDeletedModel.chatMessage,
reactionDeletedModel.emoji
)
}
}
override fun onNext(genericOverall: GenericOverall) {
Log.d(TAG, "deleted reaction: $emoji")
}
override fun onError(e: Throwable) {
}
override fun onError(e: Throwable) {
Log.e(TAG, "error while deleting reaction: $emoji")
}
override fun onComplete() {
dismiss()
}
})
override fun onComplete() {
dismiss()
}
}
companion object {
private const val TAG = "MessageActionsDialog"
private val TAG = MessageActionsDialog::class.java.simpleName
private const val ACTOR_LENGTH = 6
private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
private const val HTTP_CREATED: Int = 201
private const val DELAY: Long = 200
}
}

View File

@ -41,7 +41,7 @@ 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.adapters.messages.PreviewMessageViewHolder
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
import com.nextcloud.talk.models.json.chat.ChatMessage
@ -78,12 +78,12 @@ class FileViewerUtils(private val context: Context, private val user: User) {
message: ChatMessage,
progressUi: ProgressUi
) {
val fileName = message.selectedIndividualHashMap!![MagicPreviewMessageViewHolder.KEY_NAME]!!
val mimetype = message.selectedIndividualHashMap!![MagicPreviewMessageViewHolder.KEY_MIMETYPE]!!
val fileName = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_NAME]!!
val mimetype = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_MIMETYPE]!!
val link = message.selectedIndividualHashMap!!["link"]!!
val fileId = message.selectedIndividualHashMap!![MagicPreviewMessageViewHolder.KEY_ID]!!
val path = message.selectedIndividualHashMap!![MagicPreviewMessageViewHolder.KEY_PATH]!!
val fileId = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]!!
val path = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_PATH]!!
var size = message.selectedIndividualHashMap!!["size"]
if (size == null) {

View File

@ -17,20 +17,20 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout 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/reactions_emoji_wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true">
<TextView
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/reactions_emoji_horizontal_scroller"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:fillViewport="true"
android:measureAllChildren="false"
android:scrollbars="none" >
<LinearLayout
android:id="@+id/reactions_emoji_wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="emojis">
</TextView>
</LinearLayout>
android:gravity="center_vertical"
android:orientation="horizontal" >
</LinearLayout>
</HorizontalScrollView>