Explorar o código

react to given reactions inside message

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe %!s(int64=2) %!d(string=hai) anos
pai
achega
6b97197c80
Modificáronse 26 ficheiros con 855 adicións e 587 borrados
  1. 2 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt
  2. 10 7
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt
  3. 10 7
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
  4. 10 7
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt
  5. 2 6
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java
  6. 10 7
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
  7. 10 7
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt
  8. 17 8
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt
  9. 0 375
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
  10. 10 7
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt
  11. 10 7
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt
  12. 10 7
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt
  13. 1 6
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java
  14. 10 7
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt
  15. 341 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt
  16. 15 17
      app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt
  17. 3 3
      app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java
  18. 90 6
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  19. 9 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt
  20. 29 0
      app/src/main/java/com/nextcloud/talk/models/domain/ReactionAddedModel.kt
  21. 29 0
      app/src/main/java/com/nextcloud/talk/models/domain/ReactionDeletedModel.kt
  22. 42 0
      app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt
  23. 102 0
      app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt
  24. 64 83
      app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt
  25. 5 5
      app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
  26. 14 14
      app/src/main/res/layout/reactions_inside_message.xml

+ 2 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt

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

+ 10 - 7
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt

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

+ 10 - 7
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt

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

+ 10 - 7
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt

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

+ 2 - 6
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java

@@ -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; }
+
 }

+ 10 - 7
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt

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

+ 10 - 7
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt

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

+ 17 - 8
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt

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

+ 0 - 375
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java

@@ -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();
-}

+ 10 - 7
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt

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

+ 10 - 7
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt

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

+ 10 - 7
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt

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

+ 1 - 6
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java

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

+ 10 - 7
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt

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

+ 341 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt

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

+ 15 - 17
app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt

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

+ 3 - 3
app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java

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

+ 90 - 6
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

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

+ 9 - 0
app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt

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

+ 29 - 0
app/src/main/java/com/nextcloud/talk/models/domain/ReactionAddedModel.kt

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

+ 29 - 0
app/src/main/java/com/nextcloud/talk/models/domain/ReactionDeletedModel.kt

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

+ 42 - 0
app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt

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

+ 102 - 0
app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt

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

+ 64 - 83
app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt

@@ -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)
-
-        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(genericOverall: GenericOverall) {
-                    val statusCode = genericOverall.ocs?.meta?.statusCode
-                    if (statusCode == HTTP_CREATED) {
-                        chatController.updateAdapterAfterSendReaction(message, emoji)
-                    }
-                }
-
-                override fun onError(e: Throwable) {
-                    Log.e(TAG, "error while sending reaction: $emoji")
-                }
-
-                override fun onComplete() {
-                    dismiss()
-                }
-            })
+    inner class ReactionAddedObserver : Observer<ReactionAddedModel> {
+        override fun onSubscribe(d: Disposable) {
+        }
+
+        override fun onNext(reactionAddedModel: ReactionAddedModel) {
+            if (reactionAddedModel.success) {
+                chatController.updateUiToAddReaction(
+                    reactionAddedModel.chatMessage,
+                    reactionAddedModel.emoji
+                )
+            }
+        }
+
+        override fun onError(e: Throwable) {
+        }
+
+        override fun onComplete() {
+            dismiss()
+        }
     }
 
-    private fun deleteReaction(message: ChatMessage, emoji: String) {
-        val credentials = ApiUtils.getCredentials(user?.username, user?.token)
-
-        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(genericOverall: GenericOverall) {
-                    Log.d(TAG, "deleted reaction: $emoji")
-                }
-
-                override fun onError(e: Throwable) {
-                    Log.e(TAG, "error while deleting reaction: $emoji")
-                }
-
-                override fun onComplete() {
-                    dismiss()
-                }
-            })
+    inner class ReactionDeletedObserver : Observer<ReactionDeletedModel> {
+        override fun onSubscribe(d: Disposable) {
+        }
+
+        override fun onNext(reactionDeletedModel: ReactionDeletedModel) {
+            if (reactionDeletedModel.success) {
+                chatController.updateUiToDeleteReaction(
+                    reactionDeletedModel.chatMessage,
+                    reactionDeletedModel.emoji
+                )
+            }
+        }
+
+        override fun onError(e: Throwable) {
+        }
+
+        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
     }
 }

+ 5 - 5
app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt

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

+ 14 - 14
app/src/main/res/layout/reactions_inside_message.xml

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