浏览代码

Merge pull request #519 from nextcloud/chips-magic

Adds mention chips
Mario Đanić 6 年之前
父节点
当前提交
f35c4b486c
共有 26 个文件被更改,包括 442 次插入67 次删除
  1. 5 6
      app/build.gradle
  2. 21 11
      app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java
  3. 0 2
      app/src/main/java/com/nextcloud/talk/adapters/items/MenuItem.java
  4. 16 8
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.java
  5. 19 5
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.java
  6. 25 3
      app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java
  7. 0 1
      app/src/main/java/com/nextcloud/talk/controllers/CallController.java
  8. 10 5
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.java
  9. 2 2
      app/src/main/java/com/nextcloud/talk/controllers/LockedController.java
  10. 4 2
      app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java
  11. 4 8
      app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.java
  12. 2 1
      app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.java
  13. 4 3
      app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java
  14. 103 4
      app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
  15. 7 0
      app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java
  16. 7 5
      app/src/main/java/com/nextcloud/talk/utils/text/Spans.java
  17. 71 0
      app/src/main/java/com/nextcloud/talk/utils/text/TextAlignedImageSpan.java
  18. 26 0
      app/src/main/res/drawable/accent_circle.xml
  19. 26 0
      app/src/main/res/drawable/white_circle.xml
  20. 2 1
      app/src/main/res/layout/item_custom_incoming_text_message.xml
  21. 1 0
      app/src/main/res/layout/item_custom_outcoming_text_message.xml
  22. 1 0
      app/src/main/res/layout/view_message_input.xml
  23. 8 0
      app/src/main/res/values/styles.xml
  24. 26 0
      app/src/main/res/xml/chip_accent_background.xml
  25. 26 0
      app/src/main/res/xml/chip_outgoing_own_mention.xml
  26. 26 0
      app/src/main/res/xml/chip_simple_background.xml

+ 5 - 6
app/build.gradle

@@ -17,8 +17,8 @@ android {
         targetSdkVersion 28
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
 
-        versionCode 90
-        versionName "6.0.0beta2"
+        versionCode 91
+        versionName "6.0.0beta3"
 
         flavorDimensions "default"
         renderscriptTargetApi 19
@@ -100,7 +100,6 @@ android {
 }
 
 ext {
-    supportLibraryVersion = '28.0.0'
     workVersion = "1.0.0"
 }
 
@@ -115,17 +114,17 @@ dependencies {
     implementation fileTree(dir: 'libs', include: ['*.jar'])
     implementation 'androidx.appcompat:appcompat:1.0.2'
     implementation 'com.google.android.material:material:1.0.0'
-    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3'
+    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4'
     implementation 'com.github.vanniktech:Emoji:746caa4623'
     implementation 'org.michaelevans.colorart:library:0.0.3'
     implementation "android.arch.work:work-runtime:${workVersion}"
     implementation "android.arch.work:work-rxjava2:${workVersion}"
     androidTestImplementation "android.arch.work:work-testing:${workVersion}"
 
-    implementation 'androidx.biometric:biometric:1.0.0-alpha03'
+    implementation 'androidx.biometric:biometric:1.0.0-alpha04'
     implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"
 
-    implementation 'androidx.multidex:multidex:2.0.0'
+    implementation 'androidx.multidex:multidex:2.0.1'
 
     implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
     implementation "io.reactivex.rxjava2:rxjava:2.2.7"

+ 21 - 11
app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java

@@ -45,18 +45,28 @@ import java.util.List;
 public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
         implements IFilterable<String> {
 
-    private String userId;
+    private String objectId;
     private String displayName;
+    private String source;
     private UserEntity currentUser;
 
-    public MentionAutocompleteItem(String userId, String displayName, UserEntity currentUser) {
-        this.userId = userId;
+    public MentionAutocompleteItem(String objectId, String displayName, String source, UserEntity currentUser) {
+        this.objectId = objectId;
         this.displayName = displayName;
+        this.source = source;
         this.currentUser = currentUser;
     }
 
-    public String getUserId() {
-        return userId;
+    public String getSource() {
+        return source;
+    }
+
+    public void setSource(String source) {
+        this.source = source;
+    }
+
+    public String getObjectId() {
+        return objectId;
     }
 
     public String getDisplayName() {
@@ -67,7 +77,7 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
     public boolean equals(Object o) {
         if (o instanceof MentionAutocompleteItem) {
             MentionAutocompleteItem inItem = (MentionAutocompleteItem) o;
-            return (userId.equals(inItem.userId) && displayName.equals(inItem.displayName));
+            return (objectId.equals(inItem.objectId) && displayName.equals(inItem.displayName));
         }
 
         return false;
@@ -93,22 +103,22 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
                     String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.getSharedApplication()
                             .getResources().getColor(R.color.colorPrimary));
             if (holder.contactMentionId != null) {
-                FlexibleUtils.highlightText(holder.contactMentionId, "@" + userId,
+                FlexibleUtils.highlightText(holder.contactMentionId, "@" + objectId,
                         String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.getSharedApplication()
                                 .getResources().getColor(R.color.colorPrimary));
             }
         } else {
             holder.contactDisplayName.setText(displayName);
             if (holder.contactMentionId != null) {
-                holder.contactMentionId.setText("@" + userId);
+                holder.contactMentionId.setText("@" + objectId);
             }
         }
 
-        if (userId.equals("all")) {
+        if (source.equals("calls")) {
             holder.avatarFlipView.setFrontImageBitmap(DisplayUtils.getRoundedBitmapFromVectorDrawableResource(NextcloudTalkApplication.getSharedApplication().getResources(), R.drawable.ic_people_group_white_24px));
         } else {
             GlideUrl glideUrl = new GlideUrl(ApiUtils.getUrlForAvatarWithName(currentUser.getBaseUrl(),
-                    userId, R.dimen.avatar_size), new LazyHeaders.Builder()
+                    objectId, R.dimen.avatar_size), new LazyHeaders.Builder()
                     .setHeader("Accept", "image/*")
                     .setHeader("User-Agent", ApiUtils.getUserAgent())
                     .build());
@@ -129,7 +139,7 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
 
     @Override
     public boolean filter(String constraint) {
-        return userId != null && StringUtils.containsIgnoreCase(userId, constraint) ||
+        return objectId != null && StringUtils.containsIgnoreCase(objectId, constraint) ||
                 displayName != null && StringUtils.containsIgnoreCase(displayName, constraint);
 
     }

+ 0 - 2
app/src/main/java/com/nextcloud/talk/adapters/items/MenuItem.java

@@ -28,14 +28,12 @@ import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.style.ForegroundColorSpan;
 import android.view.View;
-import android.widget.TextView;
 import butterknife.BindView;
 import butterknife.ButterKnife;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.utils.DisplayUtils;
 import com.vanniktech.emoji.EmojiTextView;
-
 import eu.davidea.flexibleadapter.FlexibleAdapter;
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
 import eu.davidea.viewholders.FlexibleViewHolder;

+ 16 - 8
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.java

@@ -145,18 +145,26 @@ public class MagicIncomingTextMessageViewHolder
                 Map<String, String> individualHashMap = message.getMessageParameters().get(key);
                 if (individualHashMap != null) {
                     if (individualHashMap.get("type").equals("user") || individualHashMap.get("type").equals("guest") || individualHashMap.get("type").equals("call")) {
-                        int color;
-
                         if (individualHashMap.get("id").equals(message.getActiveUserId())) {
-                            color = NextcloudTalkApplication.getSharedApplication().getResources().getColor(R.color
-                                    .nc_incoming_text_mention_you);
+                            messageString =
+                                    DisplayUtils.searchAndReplaceWithMentionSpan(messageText.getContext(),
+                                            messageString,
+                                            individualHashMap.get("id"),
+                                            individualHashMap.get("name"),
+                                            individualHashMap.get("type"),
+                                            userUtils.getUserById(message.getActiveUserId()),
+                                            R.xml.chip_simple_background);
                         } else {
-                            color = NextcloudTalkApplication.getSharedApplication().getResources().getColor(R.color
-                                    .nc_incoming_text_mention_others);
+                            messageString =
+                                    DisplayUtils.searchAndReplaceWithMentionSpan(messageText.getContext(),
+                                            messageString,
+                                            individualHashMap.get("id"),
+                                            individualHashMap.get("name"),
+                                            individualHashMap.get("type"),
+                                            userUtils.getUserById(message.getActiveUserId()),
+                                            R.xml.chip_accent_background);
                         }
 
-                        messageString = DisplayUtils.searchAndColor(messageString,
-                                "@" + individualHashMap.get("name"), color);
                     } else if (individualHashMap.get("type").equals("file")) {
                         itemView.setOnClickListener(v -> {
                             Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap.get("link")));

+ 19 - 5
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.java

@@ -58,6 +58,9 @@ public class MagicOutcomingTextMessageViewHolder extends MessageHolders.Outcomin
     @Inject
     UserUtils userUtils;
 
+    @Inject
+    Context context;
+
     private View itemView;
 
     public MagicOutcomingTextMessageViewHolder(View itemView) {
@@ -76,7 +79,6 @@ public class MagicOutcomingTextMessageViewHolder extends MessageHolders.Outcomin
 
         Spannable messageString = new SpannableString(message.getText());
 
-        Context context = NextcloudTalkApplication.getSharedApplication().getApplicationContext();
         itemView.setSelected(false);
         messageTimeView.setTextColor(context.getResources().getColor(R.color.white60));
 
@@ -92,11 +94,23 @@ public class MagicOutcomingTextMessageViewHolder extends MessageHolders.Outcomin
                     if (individualHashMap.get("type").equals("user") || individualHashMap.get("type").equals("guest") || individualHashMap.get("type").equals("call")) {
                         if (!individualHashMap.get("id").equals(message.getActiveUserId())) {
                             messageString =
-                                    DisplayUtils.searchAndColor(messageString,
-                                            "@" + individualHashMap.get("name"), NextcloudTalkApplication
-                                            .getSharedApplication().getResources().getColor(R.color.nc_outcoming_text_default));
+                                    DisplayUtils.searchAndReplaceWithMentionSpan(messageText.getContext(),
+                                            messageString,
+                                            individualHashMap.get("id"),
+                                            individualHashMap.get("name"),
+                                            individualHashMap.get("type"),
+                                            userUtils.getUserById(message.getActiveUserId()),
+                                            R.xml.chip_simple_background);
+                        } else {
+                            messageString =
+                                    DisplayUtils.searchAndReplaceWithMentionSpan(messageText.getContext(),
+                                            messageString,
+                                            individualHashMap.get("id"),
+                                            individualHashMap.get("name"),
+                                            individualHashMap.get("type"),
+                                            userUtils.getUserById(message.getActiveUserId()),
+                                            R.xml.chip_outgoing_own_mention);
                         }
-
                     } else if (individualHashMap.get("type").equals("file")) {
                         itemView.setOnClickListener(v -> {
                             Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap.get("link")));

+ 25 - 3
app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java

@@ -20,15 +20,31 @@
 
 package com.nextcloud.talk.callbacks;
 
-import android.graphics.Typeface;
+import android.content.Context;
 import android.text.Editable;
 import android.text.Spanned;
+import android.text.style.DynamicDrawableSpan;
+import com.nextcloud.talk.R;
+import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.mention.Mention;
+import com.nextcloud.talk.utils.DisplayUtils;
 import com.nextcloud.talk.utils.MagicCharPolicy;
 import com.nextcloud.talk.utils.text.Spans;
 import com.otaliastudios.autocomplete.AutocompleteCallback;
+import com.vanniktech.emoji.EmojiEditText;
 
 public class MentionAutocompleteCallback implements AutocompleteCallback<Mention> {
+    private Context context;
+    private UserEntity conversationUser;
+    private EmojiEditText emojiEditText;
+
+    public MentionAutocompleteCallback(Context context, UserEntity conversationUser,
+                                       EmojiEditText emojiEditText) {
+        this.context = context;
+        this.conversationUser = conversationUser;
+        this.emojiEditText = emojiEditText;
+    }
+
     @Override
     public boolean onPopupItemClicked(Editable editable, Mention item) {
         int[] range = MagicCharPolicy.getQueryRange(editable);
@@ -37,8 +53,14 @@ public class MentionAutocompleteCallback implements AutocompleteCallback<Mention
         int end = range[1];
         String replacement = item.getLabel();
         editable.replace(start, end, replacement + " ");
-        Spans.MentionSpan mentionSpan = new Spans.MentionSpan(Typeface.BOLD, item.getId(), item.getLabel());
-        editable.setSpan(mentionSpan, start, start + item.getLabel().length() , Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        Spans.MentionChipSpan mentionChipSpan =
+                new Spans.MentionChipSpan(DisplayUtils.getDrawableForMentionChipSpan(context,
+                        item.getId(), item.getLabel(), conversationUser, item.getSource(),
+                        R.xml.chip_accent_background, emojiEditText),
+                        DynamicDrawableSpan.ALIGN_BASELINE,
+                        item.getId(), item.getLabel());
+        editable.setSpan(mentionChipSpan, start, start + item.getLabel().length(),
+                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
         return true;
     }
 

+ 0 - 1
app/src/main/java/com/nextcloud/talk/controllers/CallController.java

@@ -95,7 +95,6 @@ import pub.devrel.easypermissions.AfterPermissionGranted;
 
 import javax.inject.Inject;
 import java.io.IOException;
-import java.lang.reflect.Field;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 

+ 10 - 5
app/src/main/java/com/nextcloud/talk/controllers/ChatController.java

@@ -118,6 +118,8 @@ public class ChatController extends BaseController implements MessagesListAdapte
     UserUtils userUtils;
     @Inject
     AppPreferences appPreferences;
+    @Inject
+    Context context;
     @BindView(R.id.messagesListView)
     MessagesList messagesListView;
     @BindView(R.id.messageInputView)
@@ -385,8 +387,9 @@ public class ChatController extends BaseController implements MessagesListAdapte
                 }
 
                 Editable editable = messageInput.getEditableText();
-                Spans.MentionSpan[] mentionSpans = editable.getSpans(0, messageInput.length(), Spans.MentionSpan.class);
-                Spans.MentionSpan mentionSpan;
+                Spans.MentionChipSpan[] mentionSpans = editable.getSpans(0, messageInput.length(),
+                        Spans.MentionChipSpan.class);
+                Spans.MentionChipSpan mentionSpan;
                 for (int i = 0; i < mentionSpans.length; i++) {
                     mentionSpan = mentionSpans[i];
                     if (start >= editable.getSpanStart(mentionSpan) && start < editable.getSpanEnd(mentionSpan)) {
@@ -482,7 +485,8 @@ public class ChatController extends BaseController implements MessagesListAdapte
         float elevation = 6f;
         Drawable backgroundDrawable = new ColorDrawable(Color.WHITE);
         AutocompletePresenter<Mention> presenter = new MentionAutocompletePresenter(getApplicationContext(), roomToken);
-        AutocompleteCallback<Mention> callback = new MentionAutocompleteCallback();
+        AutocompleteCallback<Mention> callback = new MentionAutocompleteCallback(getActivity(),
+                conversationUser, messageInput);
 
         if (mentionAutocomplete == null && messageInput != null) {
             mentionAutocomplete = Autocomplete.<Mention>on(messageInput)
@@ -728,8 +732,9 @@ public class ChatController extends BaseController implements MessagesListAdapte
 
     private void submitMessage() {
         final Editable editable = messageInput.getEditableText();
-        Spans.MentionSpan mentionSpans[] = editable.getSpans(0, editable.length(), Spans.MentionSpan.class);
-        Spans.MentionSpan mentionSpan;
+        Spans.MentionChipSpan mentionSpans[] = editable.getSpans(0, editable.length(),
+                Spans.MentionChipSpan.class);
+        Spans.MentionChipSpan mentionSpan;
         for (int i = 0; i < mentionSpans.length; i++) {
             mentionSpan = mentionSpans[i];
             editable.replace(editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@" + mentionSpan.getId());

+ 2 - 2
app/src/main/java/com/nextcloud/talk/controllers/LockedController.java

@@ -32,10 +32,10 @@ import android.view.ViewGroup;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.biometric.BiometricPrompt;
+import androidx.fragment.app.FragmentActivity;
 import autodagger.AutoInjector;
 import butterknife.OnClick;
 import com.nextcloud.talk.R;
-import com.nextcloud.talk.activities.MainActivity;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.controllers.base.BaseController;
 import com.nextcloud.talk.utils.SecurityUtils;
@@ -88,7 +88,7 @@ public class LockedController extends BaseController {
 
             Executor executor = Executors.newSingleThreadExecutor();
 
-            final BiometricPrompt biometricPrompt = new BiometricPrompt((MainActivity) context, executor,
+            final BiometricPrompt biometricPrompt = new BiometricPrompt((FragmentActivity) context, executor,
                     new BiometricPrompt.AuthenticationCallback() {
                         @Override
                         public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {

+ 4 - 2
app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java

@@ -192,8 +192,10 @@ public class CallMenuController extends BaseController implements FlexibleAdapte
 
 
             if (conversation.canLeave(currentUser)) {
-                menuItems.add(new MenuItem(getResources().getString(R.string.nc_leave), 1, getResources().getDrawable(R.drawable
-                        .ic_close_grey600_24dp)));
+                menuItems.add(new MenuItem(getResources().getString(R.string.nc_leave), 1,
+                        DisplayUtils.getTintedDrawable(getResources(),
+                                R.drawable.ic_exit_to_app_black_24dp, R.color.grey_600)
+                ));
             }
         } else if (menuType.equals(MenuType.SHARE)) {
             prepareIntent();

+ 4 - 8
app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.java

@@ -22,7 +22,6 @@ package com.nextcloud.talk.controllers.bottomsheet;
 
 import android.content.ComponentName;
 import android.content.Intent;
-import android.content.res.ColorStateList;
 import android.os.Bundle;
 import android.text.Editable;
 import android.text.InputType;
@@ -33,7 +32,10 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.view.inputmethod.EditorInfo;
 import android.widget.Button;
-
+import androidx.annotation.NonNull;
+import autodagger.AutoInjector;
+import butterknife.BindView;
+import butterknife.OnClick;
 import com.bluelinelabs.conductor.RouterTransaction;
 import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
 import com.google.android.material.textfield.TextInputEditText;
@@ -47,17 +49,11 @@ import com.nextcloud.talk.utils.ShareUtils;
 import com.nextcloud.talk.utils.bundle.BundleKeys;
 import com.nextcloud.talk.utils.database.user.UserUtils;
 import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder;
-
 import org.greenrobot.eventbus.EventBus;
 import org.parceler.Parcels;
 
 import javax.inject.Inject;
 
-import androidx.annotation.NonNull;
-import autodagger.AutoInjector;
-import butterknife.BindView;
-import butterknife.OnClick;
-
 @AutoInjector(NextcloudTalkApplication.class)
 public class EntryMenuController extends BaseController {
 

+ 2 - 1
app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.java

@@ -20,6 +20,7 @@
 package com.nextcloud.talk.models.json.mention;
 
 import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonIgnore;
 import com.bluelinelabs.logansquare.annotation.JsonObject;
 import lombok.Data;
 import org.parceler.Parcel;
@@ -34,7 +35,7 @@ public class Mention {
     @JsonField(name = "label")
     String label;
 
-    // type of user (guests or users)
+    // type of user (guests or users or calls)
     @JsonField(name = "source")
     String source;
 }

+ 4 - 3
app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java

@@ -113,7 +113,8 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
                             List<AbstractFlexibleItem> internalAbstractFlexibleItemList = new ArrayList<>();
                             for (Mention mention : mentionsList) {
                                 internalAbstractFlexibleItemList.add(
-                                        new MentionAutocompleteItem(mention.getId(), mention.getLabel(),
+                                        new MentionAutocompleteItem(mention.getId(),
+                                                mention.getLabel(), mention.getSource(),
                                                 currentUser));
                             }
 
@@ -143,9 +144,9 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
         Mention mention = new Mention();
         MentionAutocompleteItem mentionAutocompleteItem = (MentionAutocompleteItem) adapter.getItem(position);
         if (mentionAutocompleteItem != null) {
-            mention.setId(mentionAutocompleteItem.getUserId());
+            mention.setId(mentionAutocompleteItem.getObjectId());
             mention.setLabel(mentionAutocompleteItem.getDisplayName());
-            mention.setSource("users");
+            mention.setSource(mentionAutocompleteItem.getSource());
             dispatchClick(mention);
         }
         return true;

+ 103 - 4
app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java

@@ -24,6 +24,7 @@ import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.ColorStateList;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -37,10 +38,7 @@ import android.net.Uri;
 import android.os.Build;
 import android.text.*;
 import android.text.method.LinkMovementMethod;
-import android.text.style.AbsoluteSizeSpan;
-import android.text.style.ClickableSpan;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.StyleSpan;
+import android.text.style.*;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.View;
@@ -50,15 +48,26 @@ import androidx.annotation.*;
 import androidx.appcompat.widget.AppCompatDrawableManager;
 import androidx.core.content.ContextCompat;
 import androidx.core.graphics.drawable.DrawableCompat;
+import com.facebook.common.executors.UiThreadImmediateExecutorService;
+import com.facebook.common.references.CloseableReference;
+import com.facebook.datasource.DataSource;
+import com.facebook.drawee.backends.pipeline.Fresco;
 import com.facebook.drawee.controller.ControllerListener;
 import com.facebook.drawee.view.SimpleDraweeView;
 import com.facebook.imagepipeline.common.RotationOptions;
+import com.facebook.imagepipeline.core.ImagePipeline;
+import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
+import com.facebook.imagepipeline.image.CloseableImage;
 import com.facebook.imagepipeline.image.ImageInfo;
 import com.facebook.imagepipeline.postprocessors.RoundAsCirclePostprocessor;
 import com.facebook.imagepipeline.request.ImageRequest;
 import com.facebook.imagepipeline.request.ImageRequestBuilder;
+import com.google.android.material.chip.ChipDrawable;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
+import com.nextcloud.talk.models.database.UserEntity;
+import com.nextcloud.talk.utils.text.Spans;
+import com.vanniktech.emoji.EmojiEditText;
 import com.vanniktech.emoji.EmojiTextView;
 
 import java.lang.reflect.Constructor;
@@ -209,6 +218,96 @@ public class DisplayUtils {
     }
 
 
+    public static Drawable getDrawableForMentionChipSpan(Context context, String id, String label,
+                                                         UserEntity conversationUser, String type,
+                                                         @XmlRes int chipResource,
+                                                         @Nullable EmojiEditText emojiEditText) {
+        ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource);
+        chip.setText(label);
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            Configuration config = context.getResources().getConfiguration();
+            chip.setLayoutDirection(config.getLayoutDirection());
+        }
+
+        int drawable;
+
+        boolean isCall = "call".equals(type) || "calls".equals(type);
+
+        if (!isCall) {
+            if (chipResource == R.xml.chip_accent_background) {
+                drawable = R.drawable.white_circle;
+            } else {
+                drawable = R.drawable.accent_circle;
+            }
+
+            chip.setChipIcon(context.getDrawable(drawable));
+        } else {
+            chip.setChipIcon(getRoundedDrawable(context.getDrawable(R.drawable.ic_people_group_white_24px)));
+        }
+
+        chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight());
+
+        if (!isCall) {
+            ImageRequest imageRequest =
+                    getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(conversationUser.getBaseUrl(), id, R.dimen.avatar_size_big));
+            ImagePipeline imagePipeline = Fresco.getImagePipeline();
+            DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline.fetchDecodedImage(imageRequest, context);
+
+            dataSource.subscribe(
+                    new BaseBitmapDataSubscriber() {
+                        @Override
+                        protected void onNewResultImpl(Bitmap bitmap) {
+                            if (bitmap != null) {
+                                chip.setChipIcon(getRoundedDrawable(new BitmapDrawable(bitmap)));
+
+                                // A hack to refresh the chip icon
+                                if (emojiEditText != null) {
+                                    emojiEditText.post(() -> emojiEditText.setTextKeepState(emojiEditText.getText(), TextView.BufferType.SPANNABLE));
+                                }
+                            }
+                        }
+
+                        @Override
+                        protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
+                        }
+                    },
+                    UiThreadImmediateExecutorService.getInstance());
+        }
+
+        return chip;
+    }
+
+
+    public static Spannable searchAndReplaceWithMentionSpan(Context context, Spannable text,
+                                                            String id, String label, String type,
+                                                            UserEntity conversationUser,
+                                                            @XmlRes int chipXmlRes) {
+
+        Spannable spannableString = new SpannableString(text);
+        String stringText = text.toString();
+
+        Matcher m = Pattern.compile("@" + label,
+                Pattern.CASE_INSENSITIVE | Pattern.LITERAL | Pattern.MULTILINE)
+                .matcher(spannableString);
+
+        int lastStartIndex = -1;
+        Spans.MentionChipSpan mentionChipSpan;
+        while (m.find()) {
+            int start = stringText.indexOf(m.group(), lastStartIndex);
+            int end = start + m.group().length();
+            lastStartIndex = end;
+            mentionChipSpan = new Spans.MentionChipSpan(DisplayUtils.getDrawableForMentionChipSpan(context,
+                    id, label, conversationUser, type, chipXmlRes, null),
+                    DynamicDrawableSpan.ALIGN_BASELINE, id,
+                    label);
+            spannableString.setSpan(mentionChipSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        }
+
+        return spannableString;
+
+    }
+
     public static Spannable searchAndColor(Spannable text, String searchText, @ColorInt int color) {
 
         Spannable spannableString = new SpannableString(text);

+ 7 - 0
app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java

@@ -105,6 +105,13 @@ public class UserUtils {
 
     }
 
+    public UserEntity getUserById(String id) {
+        Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.USER_ID.eq(id))
+                .limit(1).get();
+
+        return (UserEntity) findUserQueryResult.firstOrNull();
+    }
+
     public UserEntity getUserWithId(long id) {
         Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.ID.eq(id))
                 .limit(1).get();

+ 7 - 5
app/src/main/java/com/nextcloud/talk/utils/text/Spans.java

@@ -20,20 +20,22 @@
 
 package com.nextcloud.talk.utils.text;
 
-import android.text.style.StyleSpan;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
 import lombok.Data;
 
 public class Spans {
+
     @Data
-    public static class MentionSpan extends StyleSpan {
+    public static class MentionChipSpan extends TextAlignedImageSpan {
         String id;
         String label;
 
-        public MentionSpan(int style, String id, String label) {
-            super(style);
+        public MentionChipSpan(@NonNull Drawable drawable, int verticalAlignment, String id, String label) {
+            super(drawable, verticalAlignment);
             this.id = id;
             this.label = label;
         }
-
     }
+
 }

+ 71 - 0
app/src/main/java/com/nextcloud/talk/utils/text/TextAlignedImageSpan.java

@@ -0,0 +1,71 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Taken and adapter from
+ */
+
+package com.nextcloud.talk.utils.text;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.style.ImageSpan;
+import androidx.annotation.NonNull;
+
+public class TextAlignedImageSpan extends ImageSpan {
+    public TextAlignedImageSpan(@NonNull Drawable drawable, int verticalAlignment) {
+        super(drawable, verticalAlignment);
+    }
+
+    public int getSize(@NonNull Paint paint, CharSequence text, int start, int end,
+                       Paint.FontMetricsInt fontMetricsInt) {
+        Drawable drawable = getDrawable();
+        Rect drawableBounds = drawable.getBounds();
+
+        if (fontMetricsInt != null) {
+            Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
+            int fontHeight = fmPaint.bottom - fmPaint.top;
+            int drHeight = drawableBounds.bottom - drawableBounds.top;
+
+            int top = drHeight / 2 - fontHeight / 4;
+            int bottom = drHeight / 2 + fontHeight / 4;
+
+            fontMetricsInt.ascent = -bottom;
+            fontMetricsInt.top = -bottom;
+            fontMetricsInt.bottom = top;
+            fontMetricsInt.descent = top;
+        }
+
+        return drawableBounds.right;
+    }
+
+    @Override
+    public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
+                     float x, int top, int y, int bottom, @NonNull Paint paint) {
+        Drawable drawable = getDrawable();
+        canvas.save();
+        int transY;
+        transY = ((bottom - top) - drawable.getBounds().bottom) / 2 + top;
+        canvas.translate(x, transY);
+        drawable.draw(canvas);
+        canvas.restore();
+    }
+
+}

+ 26 - 0
app/src/main/res/drawable/accent_circle.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid
+        android:color="@color/colorAccent"/>
+</shape>

+ 26 - 0
app/src/main/res/drawable/white_circle.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid
+        android:color="@color/white"/>
+</shape>

+ 2 - 1
app/src/main/res/layout/item_custom_incoming_text_message.xml

@@ -52,6 +52,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:textColor="@color/colorPrimary"
+            android:layout_marginBottom="4dp"
             android:textSize="12sp" />
 
         <com.vanniktech.emoji.EmojiTextView
@@ -59,11 +60,11 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:textIsSelectable="true"
+            android:lineSpacingMultiplier="1.2"
             app:layout_alignSelf="flex_start"
             app:layout_flexGrow="1"
             app:layout_wrapBefore="true" />
 
-
         <TextView
             android:id="@id/messageTime"
             android:layout_width="wrap_content"

+ 1 - 0
app/src/main/res/layout/item_custom_outcoming_text_message.xml

@@ -44,6 +44,7 @@
             android:layout_height="wrap_content"
             android:layout_alignWithParentIfMissing="true"
             android:textColorHighlight="@color/nc_grey"
+            android:lineSpacingMultiplier="1.2"
             android:textIsSelectable="true" />
 
         <TextView

+ 1 - 0
app/src/main/res/layout/view_message_input.xml

@@ -40,6 +40,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_centerVertical="true"
+        android:lineSpacingMultiplier="1.2"
         android:imeOptions="actionDone"
         android:layout_toStartOf="@id/sendButtonSpace"
         android:inputType="textAutoCorrect|textMultiLine|textCapSentences"/>

+ 8 - 0
app/src/main/res/values/styles.xml

@@ -16,4 +16,12 @@
         <item name="android:textSize">12sp</item>
     </style>
 
+    <style name="ChipTextAppearance" parent="TextAppearance.MaterialComponents.Chip">
+        <item name="android:textColor">@android:color/white</item>
+    </style>
+
+    <style name="ChipAccentTextAppearance" parent="TextAppearance.MaterialComponents.Chip">
+        <item name="android:textColor">@color/colorAccent</item>
+    </style>
+
 </resources>

+ 26 - 0
app/src/main/res/xml/chip_accent_background.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<chip xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:textAppearance="@style/ChipTextAppearance"
+    app:chipBackgroundColor="@color/colorAccent"
+    app:closeIconEnabled="false"/>

+ 26 - 0
app/src/main/res/xml/chip_outgoing_own_mention.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<chip xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:textAppearance="@style/ChipAccentTextAppearance"
+    app:chipBackgroundColor="@color/white_four"
+    app:closeIconEnabled="false" />

+ 26 - 0
app/src/main/res/xml/chip_simple_background.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<chip xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:textAppearance="@style/ChipAccentTextAppearance"
+    app:chipBackgroundColor="@color/white"
+    app:closeIconEnabled="false" />