Browse Source

Merge pull request #1659 from nextcloud/feature/1644/avatar-hovercard

Avatar "hovercard"
Tim Krueger 3 năm trước cách đây
mục cha
commit
6b13d0b818
17 tập tin đã thay đổi với 663 bổ sung45 xóa
  1. 8 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
  2. 4 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java
  3. 8 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
  4. 8 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt
  5. 10 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
  6. 3 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java
  7. 4 0
      app/src/main/java/com/nextcloud/talk/api/NcApi.java
  8. 26 26
      app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/DavUtils.java
  9. 16 6
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  10. 8 2
      app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt
  11. 96 0
      app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCard.java
  12. 107 0
      app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardAction.java
  13. 69 0
      app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOCS.java
  14. 64 0
      app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOverall.java
  15. 217 0
      app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt
  16. 3 0
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  17. 12 0
      app/src/main/res/drawable/ic_talk.xml

+ 8 - 2
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt

@@ -4,6 +4,8 @@
  * @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>
@@ -48,6 +50,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.preferences.AppPreferences
@@ -56,8 +59,8 @@ import java.net.URLEncoder
 import javax.inject.Inject
 
 @AutoInjector(NextcloudTalkApplication::class)
-class IncomingLocationMessageViewHolder(incomingView: View) : MessageHolders
-.IncomingTextMessageViewHolder<ChatMessage>(incomingView) {
+class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : MessageHolders
+.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
     private val binding: ItemCustomIncomingLocationMessageBinding =
         ItemCustomIncomingLocationMessageBinding.bind(itemView)
 
@@ -102,6 +105,9 @@ class IncomingLocationMessageViewHolder(incomingView: View) : MessageHolders
         val author: String = message.actorDisplayName
         if (!TextUtils.isEmpty(author)) {
             binding.messageAuthor.text = author
+            binding.messageUserAvatar.setOnClickListener {
+                (payload as? ProfileBottomSheet)?.showFor(message.actorId, itemView.context)
+            }
         } else {
             binding.messageAuthor.setText(R.string.nc_nick_guest)
         }

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

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @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>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -31,8 +33,8 @@ import androidx.emoji.widget.EmojiTextView;
 public class IncomingPreviewMessageViewHolder extends MagicPreviewMessageViewHolder {
     private final ItemCustomIncomingPreviewMessageBinding binding;
 
-    public IncomingPreviewMessageViewHolder(View itemView) {
-        super(itemView);
+    public IncomingPreviewMessageViewHolder(View itemView, Object payload) {
+        super(itemView, payload);
         binding = ItemCustomIncomingPreviewMessageBinding.bind(itemView);
     }
 

+ 8 - 2
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt

@@ -4,6 +4,8 @@
  * @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>
@@ -46,6 +48,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.preferences.AppPreferences
@@ -54,8 +57,8 @@ import java.util.concurrent.ExecutionException
 import javax.inject.Inject
 
 @AutoInjector(NextcloudTalkApplication::class)
-class IncomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
-.IncomingTextMessageViewHolder<ChatMessage>(incomingView) {
+class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : MessageHolders
+.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
 
     private val binding: ItemCustomIncomingVoiceMessageBinding =
         ItemCustomIncomingVoiceMessageBinding.bind(itemView)
@@ -192,6 +195,9 @@ class IncomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
         val author: String = message.actorDisplayName
         if (!TextUtils.isEmpty(author)) {
             binding.messageAuthor.text = author
+            binding.messageUserAvatar.setOnClickListener {
+                (payload as? ProfileBottomSheet)?.showFor(message.actorId, itemView.context)
+            }
         } else {
             binding.messageAuthor.setText(R.string.nc_nick_guest)
         }

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

@@ -3,6 +3,8 @@
  *
  * @author Mario Danic
  * @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) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
@@ -44,6 +46,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
@@ -53,8 +56,8 @@ import com.stfalcon.chatkit.messages.MessageHolders
 import javax.inject.Inject
 
 @AutoInjector(NextcloudTalkApplication::class)
-class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders
-.IncomingTextMessageViewHolder<ChatMessage>(itemView) {
+class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : MessageHolders
+.IncomingTextMessageViewHolder<ChatMessage>(itemView, payload) {
 
     private val binding: ItemCustomIncomingTextMessageBinding = ItemCustomIncomingTextMessageBinding.bind(itemView)
 
@@ -72,6 +75,9 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders
         val author: String = message.actorDisplayName
         if (!TextUtils.isEmpty(author)) {
             binding.messageAuthor.text = author
+            binding.messageUserAvatar.setOnClickListener {
+                (payload as? ProfileBottomSheet)?.showFor(message.actorId, itemView.context)
+            }
         } else {
             binding.messageAuthor.setText(R.string.nc_nick_guest)
         }

+ 10 - 2
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java

@@ -4,6 +4,8 @@
  * @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>
@@ -54,6 +56,7 @@ import com.nextcloud.talk.jobs.DownloadFileToCacheWorker;
 import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.chat.ChatMessage;
+import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet;
 import com.nextcloud.talk.utils.AccountUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 import com.nextcloud.talk.utils.DrawableUtils;
@@ -110,8 +113,8 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
 
     View clickView;
 
-    public MagicPreviewMessageViewHolder(View itemView) {
-        super(itemView);
+    public MagicPreviewMessageViewHolder(View itemView, Object payload) {
+        super(itemView, payload);
         NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
     }
 
@@ -128,6 +131,11 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
                 }
             } else {
                 userAvatar.setVisibility(View.VISIBLE);
+                userAvatar.setOnClickListener(v -> {
+                    if (payload instanceof ProfileBottomSheet){
+                        ((ProfileBottomSheet) payload).showFor(message.actorId, v.getContext());
+                    }
+                });
 
                 if (ACTOR_TYPE_BOTS.equals(message.actorType) && ACTOR_ID_CHANGELOG.equals(message.actorId)) {
                     if (context != null) {

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

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @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>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -32,7 +34,7 @@ public class OutcomingPreviewMessageViewHolder extends MagicPreviewMessageViewHo
     private final ItemCustomOutcomingPreviewMessageBinding binding;
 
     public OutcomingPreviewMessageViewHolder(View itemView) {
-        super(itemView);
+        super(itemView, null);
         binding = ItemCustomOutcomingPreviewMessageBinding.bind(itemView);
     }
 

+ 4 - 0
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -31,6 +31,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall;
 import com.nextcloud.talk.models.json.conversations.RoomsOverall;
 import com.nextcloud.talk.models.json.generic.GenericOverall;
 import com.nextcloud.talk.models.json.generic.Status;
+import com.nextcloud.talk.models.json.hovercard.HoverCardOverall;
 import com.nextcloud.talk.models.json.mention.MentionOverall;
 import com.nextcloud.talk.models.json.notifications.NotificationOverall;
 import com.nextcloud.talk.models.json.participants.AddParticipantOverall;
@@ -425,4 +426,7 @@ public interface NcApi {
     @POST
     Observable<GenericOverall> notificationCalls(@Header("Authorization") String authorization, @Url String url,
                                                  @Field("level") Integer level);
+
+    @GET
+    Observable<HoverCardOverall> hoverCard(@Header("Authorization") String authorization, @Url String url);
 }

+ 26 - 26
app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/DavUtils.java

@@ -68,32 +68,32 @@ public class DavUtils {
     public static final String PROPERTY_QUOTA_AVAILABLE_BYTES = "quota-available-bytes";
 
     static Property.Name[] getAllPropSet() {
-        List<Property.Name> propSet = new ArrayList<>();
-
-        propSet.add(DisplayName.NAME);
-        propSet.add(GetContentType.NAME);
-        propSet.add(GetContentLength.NAME);
-        propSet.add(GetContentType.NAME);
-        propSet.add(GetContentLength.NAME);
-        propSet.add(GetLastModified.NAME);
-        propSet.add(CreationDate.NAME);
-        propSet.add(GetETag.NAME);
-        propSet.add(ResourceType.NAME);
-
-        propSet.add(NCPermission.NAME);
-        propSet.add(OCId.NAME);
-        propSet.add(OCSize.NAME);
-        propSet.add(OCFavorite.NAME);
-        propSet.add(new Property.Name(OC_NAMESPACE, EXTENDED_PROPERTY_OWNER_ID));
-        propSet.add(new Property.Name(OC_NAMESPACE, EXTENDED_PROPERTY_OWNER_DISPLAY_NAME));
-        propSet.add(new Property.Name(OC_NAMESPACE, EXTENDED_PROPERTY_UNREAD_COMMENTS));
-
-        propSet.add(NCEncrypted.NAME);
-        propSet.add(new Property.Name(NC_NAMESPACE, EXTENDED_PROPERTY_MOUNT_TYPE));
-        propSet.add(NCPreview.NAME);
-        propSet.add(new Property.Name(NC_NAMESPACE, EXTENDED_PROPERTY_NOTE));
-
-        return propSet.toArray(new Property.Name[0]);
+        List<Property.Name> props = new ArrayList<>();
+
+        props.add(DisplayName.NAME);
+        props.add(GetContentType.NAME);
+        props.add(GetContentLength.NAME);
+        props.add(GetContentType.NAME);
+        props.add(GetContentLength.NAME);
+        props.add(GetLastModified.NAME);
+        props.add(CreationDate.NAME);
+        props.add(GetETag.NAME);
+        props.add(ResourceType.NAME);
+
+        props.add(NCPermission.NAME);
+        props.add(OCId.NAME);
+        props.add(OCSize.NAME);
+        props.add(OCFavorite.NAME);
+        props.add(new Property.Name(OC_NAMESPACE, EXTENDED_PROPERTY_OWNER_ID));
+        props.add(new Property.Name(OC_NAMESPACE, EXTENDED_PROPERTY_OWNER_DISPLAY_NAME));
+        props.add(new Property.Name(OC_NAMESPACE, EXTENDED_PROPERTY_UNREAD_COMMENTS));
+
+        props.add(NCEncrypted.NAME);
+        props.add(new Property.Name(NC_NAMESPACE, EXTENDED_PROPERTY_MOUNT_TYPE));
+        props.add(NCPreview.NAME);
+        props.add(new Property.Name(NC_NAMESPACE, EXTENDED_PROPERTY_NOTE));
+
+        return props.toArray(new Property.Name[0]);
     }
 
     public static void registerCustomFactories() {

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

@@ -132,6 +132,7 @@ import com.nextcloud.talk.models.json.conversations.RoomsOverall
 import com.nextcloud.talk.models.json.generic.GenericOverall
 import com.nextcloud.talk.models.json.mention.Mention
 import com.nextcloud.talk.presenters.MentionAutocompletePresenter
+import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.ui.dialog.AttachmentDialog
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
@@ -427,9 +428,12 @@ class ChatController(args: Bundle) :
             adapterWasNull = true
 
             val messageHolders = MessageHolders()
+            val profileBottomSheet = ProfileBottomSheet(ncApi!!, conversationUser!!, router)
+
             messageHolders.setIncomingTextConfig(
                 MagicIncomingTextMessageViewHolder::class.java,
-                R.layout.item_custom_incoming_text_message
+                R.layout.item_custom_incoming_text_message,
+                profileBottomSheet
             )
             messageHolders.setOutcomingTextConfig(
                 MagicOutcomingTextMessageViewHolder::class.java,
@@ -438,7 +442,8 @@ class ChatController(args: Bundle) :
 
             messageHolders.setIncomingImageConfig(
                 IncomingPreviewMessageViewHolder::class.java,
-                R.layout.item_custom_incoming_preview_message
+                R.layout.item_custom_incoming_preview_message,
+                profileBottomSheet
             )
 
             messageHolders.setOutcomingImageConfig(
@@ -460,14 +465,17 @@ class ChatController(args: Bundle) :
                 MagicUnreadNoticeMessageViewHolder::class.java,
                 R.layout.item_date_header,
                 MagicUnreadNoticeMessageViewHolder::class.java,
-                R.layout.item_date_header, this
+                R.layout.item_date_header,
+                this
             )
 
             messageHolders.registerContentType(
                 CONTENT_TYPE_LOCATION,
                 IncomingLocationMessageViewHolder::class.java,
+                profileBottomSheet,
                 R.layout.item_custom_incoming_location_message,
                 OutcomingLocationMessageViewHolder::class.java,
+                null,
                 R.layout.item_custom_outcoming_location_message,
                 this
             )
@@ -475,8 +483,10 @@ class ChatController(args: Bundle) :
             messageHolders.registerContentType(
                 CONTENT_TYPE_VOICE_MESSAGE,
                 IncomingVoiceMessageViewHolder::class.java,
+                profileBottomSheet,
                 R.layout.item_custom_incoming_voice_message,
                 OutcomingVoiceMessageViewHolder::class.java,
+                null,
                 R.layout.item_custom_outcoming_voice_message,
                 this
             )
@@ -585,7 +595,7 @@ class ChatController(args: Bundle) :
                         if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < newMessagesCount) {
                             newMessagesCount = 0
 
-                            if (binding.popupBubbleView.isShown == true) {
+                            if (binding.popupBubbleView.isShown) {
                                 binding.popupBubbleView.hide()
                             }
                         }
@@ -640,7 +650,7 @@ class ChatController(args: Bundle) :
                     }
                 } catch (npe: NullPointerException) {
                     // view binding can be null
-                    // since this is called asynchrously and UI might have been destroyed in the meantime
+                    // since this is called asynchronously and UI might have been destroyed in the meantime
                     Log.i(TAG, "UI destroyed - view binding already gone")
                 }
             }
@@ -2178,7 +2188,7 @@ class ChatController(args: Bundle) :
                         bundle.putBoolean(BundleKeys.KEY_FORWARD_MSG_FLAG, true)
                         bundle.putString(BundleKeys.KEY_FORWARD_MSG_TEXT, message?.text)
                         bundle.putString(BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM, roomId)
-                        getRouter().pushController(
+                        router.pushController(
                             RouterTransaction.with(ConversationsListController(bundle))
                                 .pushChangeHandler(HorizontalChangeHandler())
                                 .popChangeHandler(HorizontalChangeHandler())

+ 8 - 2
app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt

@@ -1007,8 +1007,14 @@ class ConversationInfoController(args: Bundle) :
                 R.drawable.ic_lock_grey600_24px,
                 context!!.getString(R.string.nc_attendee_pin, participant.attendeePin)
             ),
-            BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context!!.getString(R.string.nc_promote)),
-            BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context!!.getString(R.string.nc_demote)),
+            BasicListItemWithImage(
+                R.drawable.ic_pencil_grey600_24dp,
+                context!!.getString(R.string.nc_promote)
+            ),
+            BasicListItemWithImage(
+                R.drawable.ic_pencil_grey600_24dp,
+                context!!.getString(R.string.nc_demote)
+            ),
             BasicListItemWithImage(
                 R.drawable.ic_delete_grey600_24dp,
                 context!!.getString(R.string.nc_remove_participant)

+ 96 - 0
app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCard.java

@@ -0,0 +1,96 @@
+/*
+ *
+ *   Nextcloud Talk application
+ *
+ *   @author Tim Krüger
+ *   Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.hovercard;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+
+import org.parceler.Parcel;
+
+import java.util.List;
+import java.util.Objects;
+
+@Parcel
+@JsonObject
+public class HoverCard {
+
+    @JsonField(name = "userId")
+    public String userId;
+
+    @JsonField(name = "displayName")
+    public String displayName;
+
+    @JsonField(name = "actions")
+    public List<HoverCardAction> actions;
+
+
+    public String getUserId() {
+        return this.userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
+    public List<HoverCardAction> getActions() {
+        return actions;
+    }
+
+    public void setActions(List<HoverCardAction> actions) {
+        this.actions = actions;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        HoverCard hoverCard = (HoverCard) o;
+        return Objects.equals(userId, hoverCard.userId) &&
+            Objects.equals(displayName, hoverCard.displayName) &&
+            Objects.equals(actions, hoverCard.actions);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(userId, displayName, actions);
+    }
+
+    @Override
+    public String toString() {
+        return "HoverCard{" +
+            "userId='" + userId + '\'' +
+            ", displayName='" + displayName + '\'' +
+            ", actions=" + actions +
+            '}';
+    }
+}

+ 107 - 0
app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardAction.java

@@ -0,0 +1,107 @@
+/*
+ *
+ *   Nextcloud Talk application
+ *
+ *   @author Tim Krüger
+ *   Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.hovercard;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+
+import org.parceler.Parcel;
+
+import java.util.Objects;
+
+@Parcel
+@JsonObject
+public class HoverCardAction {
+
+    @JsonField(name = "title")
+    public String title;
+
+    @JsonField(name = "icon")
+    public String icon;
+
+    @JsonField(name = "hyperlink")
+    public String hyperlink;
+
+    @JsonField(name = "appId")
+    public String appId;
+
+    public String getTitle() {
+        return this.title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getIcon() {
+        return icon;
+    }
+
+    public void setIcon(String icon) {
+        this.icon = icon;
+    }
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public String getHyperlink() {
+        return hyperlink;
+    }
+
+    public void setHyperlink(String hyperlink) {
+        this.hyperlink = hyperlink;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        HoverCardAction that = (HoverCardAction) o;
+        return Objects.equals(title, that.title) &&
+            Objects.equals(icon, that.icon) &&
+            Objects.equals(hyperlink, that.hyperlink) &&
+            Objects.equals(appId, that.appId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(title, icon, hyperlink, appId);
+    }
+
+    @Override
+    public String toString() {
+        return "HoverCardAction{" +
+            "title='" + title + '\'' +
+            ", icon='" + icon + '\'' +
+            ", hyper='" + hyperlink + '\'' +
+            ", appId='" + appId + '\'' +
+            '}';
+    }
+}

+ 69 - 0
app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOCS.java

@@ -0,0 +1,69 @@
+/*
+ *
+ *   Nextcloud Talk application
+ *
+ *   @author Tim Krüger
+ *   Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.hovercard;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+import com.nextcloud.talk.models.json.generic.GenericOCS;
+
+import java.util.Objects;
+
+@JsonObject
+public class HoverCardOCS extends GenericOCS {
+    @JsonField(name = "data")
+    public HoverCard data;
+
+    public HoverCard getData() {
+        return this.data;
+    }
+
+    public void setData(HoverCard data) {
+        this.data = data;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+        HoverCardOCS that = (HoverCardOCS) o;
+        return Objects.equals(data, that.data);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), data);
+    }
+
+    @Override
+    public String toString() {
+        return "HoverCardOCS{" +
+            "data=" + data +
+            '}';
+    }
+
+}

+ 64 - 0
app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOverall.java

@@ -0,0 +1,64 @@
+/*
+ *
+ *   Nextcloud Talk application
+ *
+ *   @author Tim Krüger
+ *   Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.hovercard;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+
+import java.util.Objects;
+
+@JsonObject
+public class HoverCardOverall {
+    @JsonField(name = "ocs")
+    public HoverCardOCS ocs;
+
+    public HoverCardOCS getOcs() {
+        return this.ocs;
+    }
+
+    public void setOcs(HoverCardOCS ocs) {
+        this.ocs = ocs;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        HoverCardOverall that = (HoverCardOverall) o;
+        return Objects.equals(ocs, that.ocs);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(ocs);
+    }
+
+    @Override
+    public String toString() {
+        return "HoverCardOverall{" +
+            "ocs=" + ocs +
+            '}';
+    }
+}

+ 217 - 0
app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt

@@ -0,0 +1,217 @@
+/*
+ *
+ *   Nextcloud Talk application
+ *
+ *   @author Tim Krüger
+ *   Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.ui.bottom.sheet
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import com.afollestad.materialdialogs.LayoutMode
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.bottomsheets.BottomSheet
+import com.bluelinelabs.conductor.Router
+import com.nextcloud.talk.R
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.controllers.bottomsheet.items.BasicListItemWithImage
+import com.nextcloud.talk.controllers.bottomsheet.items.listItemsWithImage
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.models.json.conversations.RoomOverall
+import com.nextcloud.talk.models.json.hovercard.HoverCardAction
+import com.nextcloud.talk.models.json.hovercard.HoverCardOverall
+import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet.AllowedAppIds.EMAIL
+import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet.AllowedAppIds.PROFILE
+import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet.AllowedAppIds.SPREED
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.ConductorRemapping
+import com.nextcloud.talk.utils.bundle.BundleKeys
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import org.parceler.Parcels
+
+private const val TAG = "ProfileBottomSheet"
+
+class ProfileBottomSheet(val ncApi: NcApi, val userEntity: UserEntity, val router: Router) {
+
+    private val allowedAppIds = listOf(SPREED.stringValue, PROFILE.stringValue, EMAIL.stringValue)
+
+    fun showFor(user: String, context: Context) {
+
+        ncApi.hoverCard(
+            ApiUtils.getCredentials(userEntity.username, userEntity.token),
+            ApiUtils.getUrlForHoverCard(userEntity.baseUrl, user)
+        ).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : io.reactivex.Observer<HoverCardOverall> {
+                override fun onSubscribe(d: Disposable) {
+                }
+
+                override fun onNext(hoverCardOverall: HoverCardOverall) {
+                    bottomSheet(hoverCardOverall.ocs.data.actions, hoverCardOverall.ocs.data.displayName, user, context)
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "Failed to get hover card for user $user", e)
+                }
+
+                override fun onComplete() {
+                }
+            })
+    }
+
+    private fun bottomSheet(actions: List<HoverCardAction>, displayName: String, userId: String, context: Context) {
+
+        val filteredActions = actions.filter { allowedAppIds.contains(it.appId) }
+        val items = filteredActions.map { configureActionListItem(it) }
+
+        MaterialDialog(context, BottomSheet(LayoutMode.WRAP_CONTENT)).show {
+            cornerRadius(res = R.dimen.corner_radius)
+
+            title(text = displayName)
+            listItemsWithImage(items = items) { _, index, _ ->
+
+                val action = filteredActions[index]
+
+                when (AllowedAppIds.createFor(action)) {
+                    PROFILE -> openProfile(action.hyperlink, context)
+                    EMAIL -> composeEmail(action.title, context)
+                    SPREED -> talkTo(userId)
+                }
+            }
+        }
+    }
+
+    private fun configureActionListItem(action: HoverCardAction): BasicListItemWithImage {
+
+        val drawable = when (AllowedAppIds.createFor(action)) {
+            PROFILE -> R.drawable.ic_user
+            EMAIL -> R.drawable.ic_email
+            SPREED -> R.drawable.ic_talk
+        }
+
+        return BasicListItemWithImage(
+            drawable,
+            action.title
+        )
+    }
+
+    private fun talkTo(userId: String) {
+
+        val apiVersion =
+            ApiUtils.getConversationApiVersion(userEntity, intArrayOf(ApiUtils.APIv4, 1))
+        val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
+            apiVersion,
+            userEntity.baseUrl,
+            "1",
+            null,
+            userId,
+            null
+        )
+        val credentials = ApiUtils.getCredentials(userEntity.username, userEntity.token)
+        ncApi!!.createRoom(
+            credentials,
+            retrofitBucket.getUrl(), retrofitBucket.getQueryMap()
+        )
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<RoomOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(roomOverall: RoomOverall) {
+                    val bundle = Bundle()
+                    bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, userEntity)
+                    bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken())
+                    bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId())
+
+                    // FIXME once APIv2+ is used only, the createRoom already returns all the data
+                    ncApi!!.getRoom(
+                        credentials,
+                        ApiUtils.getUrlForRoom(
+                            apiVersion, userEntity.baseUrl,
+                            roomOverall.getOcs().getData().getToken()
+                        )
+                    )
+                        .subscribeOn(Schedulers.io())
+                        .observeOn(AndroidSchedulers.mainThread())
+                        .subscribe(object : Observer<RoomOverall> {
+                            override fun onSubscribe(d: Disposable) {
+                                // unused atm
+                            }
+
+                            override fun onNext(roomOverall: RoomOverall) {
+                                bundle.putParcelable(
+                                    BundleKeys.KEY_ACTIVE_CONVERSATION,
+                                    Parcels.wrap(roomOverall.getOcs().getData())
+                                )
+                                ConductorRemapping.remapChatController(
+                                    router, userEntity.id,
+                                    roomOverall.getOcs().getData().getToken(), bundle, true
+                                )
+                            }
+
+                            override fun onError(e: Throwable) {
+                                Log.e(TAG, e.message, e)
+                            }
+
+                            override fun onComplete() {
+                                // unused atm
+                            }
+                        })
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, e.message, e)
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun composeEmail(address: String, context: Context) {
+        val addresses = arrayListOf(address)
+        val intent = Intent(Intent.ACTION_SENDTO).apply {
+            data = Uri.parse("mailto:") // only email apps should handle this
+            putExtra(Intent.EXTRA_EMAIL, addresses)
+        }
+        context.startActivity(intent)
+    }
+
+    private fun openProfile(hyperlink: String, context: Context) {
+        val webpage: Uri = Uri.parse(hyperlink)
+        val intent = Intent(Intent.ACTION_VIEW, webpage)
+        context.startActivity(intent)
+    }
+
+    enum class AllowedAppIds(val stringValue: String) {
+        SPREED("spreed"),
+        PROFILE("profile"),
+        EMAIL("email");
+
+        companion object {
+            fun createFor(action: HoverCardAction): AllowedAppIds = valueOf(action.appId.uppercase())
+        }
+    }
+}

+ 3 - 0
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -400,4 +400,7 @@ public class ApiUtils {
     public static String getUrlToSendLocation(int version, String baseUrl, String roomToken) {
         return getUrlForChat(version, baseUrl, roomToken) + "/share";
     }
+
+    public static String getUrlForHoverCard(String baseUrl, String userId) { return baseUrl + ocsApiVersion +
+        "/hovercard/v1/" + userId; }
 }

+ 12 - 0
app/src/main/res/drawable/ic_talk.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector android:autoMirrored="true"
+    android:height="24dp"
+    android:viewportHeight="16"
+    android:viewportWidth="16"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="#757575"
+        android:pathData="m7.9992,0.999a6.9993,6.9994 0,0 0,-6.9992 6.9996,6.9993 6.9994,0 0,0 6.9992,6.9994 6.9993,6.9994 0,0 0,3.6308 -1.024c0.8602,0.3418 2.7871,1.356 3.2457,0.9179 0.4792,-0.4577 -0.5626,-2.6116 -0.8124,-3.412a6.9993,6.9994 0,0 0,0.935 -3.4814,6.9993 6.9994,0 0,0 -6.9991,-6.9993zM8,3.6601a4.34,4.3401 0,0 1,4.34 4.3401,4.34 4.3401,0 0,1 -4.34,4.3398 4.34,4.3401 0,0 1,-4.34 -4.3398,4.34 4.3401,0 0,1 4.34,-4.3401z"
+        android:strokeWidth=".14" />
+</vector>