Explorar o código

Profile page

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
tobiasKaminsky %!s(int64=3) %!d(string=hai) anos
pai
achega
6d07584a54

BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testProfileBottomSheet.png


+ 45 - 0
src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java

@@ -31,6 +31,8 @@ import android.view.ViewGroup;
 import android.widget.TextView;
 
 import com.google.gson.Gson;
+import com.nextcloud.android.lib.resources.profile.Action;
+import com.nextcloud.android.lib.resources.profile.HoverCard;
 import com.nextcloud.client.account.RegisteredUser;
 import com.nextcloud.client.account.Server;
 import com.nextcloud.client.device.DeviceInfo;
@@ -54,6 +56,7 @@ import com.owncloud.android.lib.resources.users.StatusType;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.ui.fragment.OCFileListBottomSheetActions;
 import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog;
+import com.owncloud.android.ui.fragment.ProfileBottomSheetDialog;
 import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.ScreenshotTest;
 
@@ -64,6 +67,7 @@ import org.junit.Test;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 
 import androidx.fragment.app.DialogFragment;
@@ -378,6 +382,47 @@ public class DialogFragmentIT extends AbstractIT {
         screenshot(sut.getWindow().getDecorView());
     }
 
+    @Test
+    @ScreenshotTest
+    public void testProfileBottomSheet() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        // Fixed values for HoverCard
+        List<Action> actions = new ArrayList<>();
+        actions.add(new Action("profile",
+                               "View profile",
+                               "https://dev.nextcloud.com/core/img/actions/profile.svg",
+                               "https://dev.nextcloud.com/index.php/u/christine"));
+        actions.add(new Action("core",
+                               "christine.scott@nextcloud.com",
+                               "https://dev.nextcloud.com/core/img/actions/mail.svg",
+                               "mailto:christine.scott@nextcloud.com"));
+
+        actions.add(new Action("spreed",
+                               "Talk to Christine",
+                               "https://dev.nextcloud.com/apps/spreed/img/app-dark.svg",
+                               "https://dev.nextcloud.com/apps/spreed/?callUser=christine"
+        ));
+
+        HoverCard hoverCard = new HoverCard("christine", "Christine Scott", actions);
+
+        // show dialog
+        Intent intent = new Intent(targetContext, FileDisplayActivity.class);
+        FileDisplayActivity fda = activityRule.launchActivity(intent);
+
+        ProfileBottomSheetDialog sut = new ProfileBottomSheetDialog(fda,
+                                                                    user,
+                                                                    hoverCard);
+
+        fda.runOnUiThread(sut::show);
+
+        waitForIdleSync();
+
+        screenshot(sut.getWindow().getDecorView());
+    }
+
     private FileDisplayActivity showDialog(DialogFragment dialog) {
         Intent intent = new Intent(targetContext, FileDisplayActivity.class);
         FileDisplayActivity sut = activityRule.launchActivity(intent);

+ 4 - 1
src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java

@@ -869,6 +869,7 @@ public final class ThumbnailsCacheManager {
         private final float mAvatarRadius;
         private User user;
         private String mUserId;
+        private String displayName;
         private String mServerName;
         private Context mContext;
 
@@ -879,6 +880,7 @@ public final class ThumbnailsCacheManager {
                                     Resources resources,
                                     float avatarRadius,
                                     String userId,
+                                    String displayName,
                                     String serverName,
                                     Context context) {
             mAvatarGenerationListener = new WeakReference<>(avatarGenerationListener);
@@ -887,6 +889,7 @@ public final class ThumbnailsCacheManager {
             mResources = resources;
             mAvatarRadius = avatarRadius;
             mUserId = userId;
+            this.displayName = displayName;
             mServerName = serverName;
             mContext = context;
         }
@@ -1023,7 +1026,7 @@ public final class ThumbnailsCacheManager {
 
             if (avatar == null) {
                 try {
-                    return TextDrawable.createAvatar(user, mAvatarRadius);
+                    return TextDrawable.createAvatarByUserId(displayName, mAvatarRadius);
                 } catch (Exception e1) {
                     return ResourcesCompat.getDrawable(mResources, R.drawable.ic_user, null);
                 }

+ 5 - 1
src/main/java/com/owncloud/android/ui/adapter/ShareViewHolder.java

@@ -48,7 +48,9 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
         super(itemView);
     }
 
-    public ShareViewHolder(FileDetailsShareShareItemBinding binding, User user, Context context) {
+    public ShareViewHolder(FileDetailsShareShareItemBinding binding,
+                           User user,
+                           Context context) {
         this(binding.getRoot());
         this.binding = binding;
         this.user = user;
@@ -91,6 +93,8 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
                                        context.getResources(),
                                        binding.icon,
                                        context);
+
+                binding.icon.setOnClickListener(v -> listener.showProfileBottomSheet(user, share.getShareWith()));
             default:
                 setImage(binding.icon, name, R.drawable.ic_user);
                 break;

+ 12 - 12
src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.java

@@ -25,7 +25,6 @@
 
 package com.owncloud.android.ui.adapter;
 
-import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
@@ -39,6 +38,7 @@ import com.owncloud.android.databinding.FileDetailsSharePublicLinkAddNewItemBind
 import com.owncloud.android.databinding.FileDetailsShareShareItemBinding;
 import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.lib.resources.shares.ShareType;
+import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.utils.DisplayUtils;
 
 import java.util.ArrayList;
@@ -55,24 +55,24 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     implements DisplayUtils.AvatarGenerationListener {
 
     private ShareeListAdapterListener listener;
-    private Context context;
+    private FileActivity fileActivity;
     private List<OCShare> shares;
     private float avatarRadiusDimension;
     private String userId;
     private User user;
 
-    public ShareeListAdapter(Context context,
+    public ShareeListAdapter(FileActivity fileActivity,
                              List<OCShare> shares,
                              ShareeListAdapterListener listener,
                              String userId,
                              User user) {
-        this.context = context;
+        this.fileActivity = fileActivity;
         this.shares = shares;
         this.listener = listener;
         this.userId = userId;
         this.user = user;
 
-        avatarRadiusDimension = context.getResources().getDimension(R.dimen.user_icon_radius);
+        avatarRadiusDimension = fileActivity.getResources().getDimension(R.dimen.user_icon_radius);
 
         sortShares();
     }
@@ -89,26 +89,26 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             case PUBLIC_LINK:
             case EMAIL:
                 return new LinkShareViewHolder(
-                    FileDetailsShareLinkShareItemBinding.inflate(LayoutInflater.from(context),
+                    FileDetailsShareLinkShareItemBinding.inflate(LayoutInflater.from(fileActivity),
                                                                  parent,
                                                                  false),
-                    context);
+                    fileActivity);
             case NEW_PUBLIC_LINK:
                 return new NewLinkShareViewHolder(
-                    FileDetailsSharePublicLinkAddNewItemBinding.inflate(LayoutInflater.from(context),
+                    FileDetailsSharePublicLinkAddNewItemBinding.inflate(LayoutInflater.from(fileActivity),
                                                                         parent,
                                                                         false)
                 );
             case INTERNAL:
                 return new InternalShareViewHolder(
-                    FileDetailsShareInternalShareLinkBinding.inflate(LayoutInflater.from(context), parent, false),
-                    context);
+                    FileDetailsShareInternalShareLinkBinding.inflate(LayoutInflater.from(fileActivity), parent, false),
+                    fileActivity);
             default:
-                return new ShareViewHolder(FileDetailsShareShareItemBinding.inflate(LayoutInflater.from(context),
+                return new ShareViewHolder(FileDetailsShareShareItemBinding.inflate(LayoutInflater.from(fileActivity),
                                                                                     parent,
                                                                                     false),
                                            user,
-                                           context);
+                                           fileActivity);
         }
     }
 

+ 3 - 0
src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapterListener.java

@@ -24,6 +24,7 @@ package com.owncloud.android.ui.adapter;
 
 import android.widget.ImageView;
 
+import com.nextcloud.client.account.User;
 import com.owncloud.android.lib.resources.shares.OCShare;
 
 public interface ShareeListAdapterListener {
@@ -38,4 +39,6 @@ public interface ShareeListAdapterListener {
     void createPublicShareLink();
 
     void requestPasswordForShare(OCShare share, boolean askForPassword);
+
+    void showProfileBottomSheet(User user, String shareWith);
 }

+ 85 - 0
src/main/java/com/owncloud/android/ui/asynctasks/RetrieveHoverCardAsyncTask.java

@@ -0,0 +1,85 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * 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/>.
+ */
+
+package com.owncloud.android.ui.asynctasks;
+
+import android.os.AsyncTask;
+
+import com.nextcloud.android.lib.resources.profile.GetHoverCardRemoteOperation;
+import com.nextcloud.android.lib.resources.profile.HoverCard;
+import com.nextcloud.client.account.User;
+import com.nextcloud.client.network.ClientFactory;
+import com.nextcloud.common.NextcloudClient;
+import com.owncloud.android.R;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.ui.fragment.ProfileBottomSheetDialog;
+import com.owncloud.android.utils.DisplayUtils;
+
+import java.lang.ref.WeakReference;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.Lifecycle;
+
+public class RetrieveHoverCardAsyncTask extends AsyncTask<Void, Void, HoverCard> {
+    private final User user;
+    private final String userId;
+    private final WeakReference<FragmentActivity> activityWeakReference;
+    private final ClientFactory clientFactory;
+
+    public RetrieveHoverCardAsyncTask(User user,
+                                      String userId,
+                                      FragmentActivity activity,
+                                      ClientFactory clientFactory) {
+        this.user = user;
+        this.userId = userId;
+        this.activityWeakReference = new WeakReference<>(activity);
+        this.clientFactory = clientFactory;
+    }
+
+    @Override
+    protected HoverCard doInBackground(Void... voids) {
+        try {
+            NextcloudClient client = clientFactory.createNextcloudClient(user);
+            RemoteOperationResult<HoverCard> result = new GetHoverCardRemoteOperation(userId).execute(client);
+
+            if (result.isSuccess()) {
+                return result.getResultData();
+            } else {
+                return null;
+            }
+        } catch (ClientFactory.CreationException | NullPointerException e) {
+            return null;
+        }
+    }
+
+    @Override
+    protected void onPostExecute(HoverCard hoverCard) {
+        FragmentActivity activity = this.activityWeakReference.get();
+
+        if (activity != null && activity.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
+            if (hoverCard.getActions().size() > 0) {
+                new ProfileBottomSheetDialog(activity, user, hoverCard).show();
+            } else {
+                DisplayUtils.showSnackMessage(activity, R.string.no_actions);
+            }
+        }
+    }
+}

+ 12 - 0
src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java

@@ -41,6 +41,7 @@ import android.widget.ImageView;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.network.ClientFactory;
 import com.owncloud.android.R;
 import com.owncloud.android.databinding.FileDetailsSharingFragmentBinding;
 import com.owncloud.android.datamodel.FileDataStorageManager;
@@ -50,12 +51,14 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.lib.resources.shares.SharePermissionsBuilder;
 import com.owncloud.android.lib.resources.shares.ShareType;
+import com.owncloud.android.lib.resources.status.NextcloudVersion;
 import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
 import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.ui.adapter.ShareeListAdapter;
 import com.owncloud.android.ui.adapter.ShareeListAdapterListener;
+import com.owncloud.android.ui.asynctasks.RetrieveHoverCardAsyncTask;
 import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment;
 import com.owncloud.android.ui.dialog.NoteDialogFragment;
 import com.owncloud.android.ui.dialog.RenamePublicShareDialogFragment;
@@ -110,6 +113,8 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
 
     @Inject UserAccountManager accountManager;
 
+    @Inject ClientFactory clientFactory;
+
     public static FileDetailSharingFragment newInstance(OCFile file, User user) {
         FileDetailSharingFragment fragment = new FileDetailSharingFragment();
         Bundle args = new Bundle();
@@ -607,6 +612,13 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
         dialog.show(getChildFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT);
     }
 
+    @Override
+    public void showProfileBottomSheet(User user, String shareWith) {
+        if (user.getServer().getVersion().isNewerOrEqual(NextcloudVersion.Companion.getNextcloud_23())) {
+            new RetrieveHoverCardAsyncTask(user, shareWith, fileActivity, clientFactory).execute();
+        }
+    }
+
     /**
      * Get known server capabilities from DB
      */

+ 176 - 0
src/main/java/com/owncloud/android/ui/fragment/ProfileBottomSheetDialog.kt

@@ -0,0 +1,176 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ *
+ * 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.owncloud.android.ui.fragment
+
+import android.content.ActivityNotFoundException
+import android.content.DialogInterface
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.core.content.res.ResourcesCompat
+import androidx.fragment.app.FragmentActivity
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.nextcloud.android.lib.resources.profile.Action
+import com.nextcloud.android.lib.resources.profile.HoverCard
+import com.nextcloud.client.account.User
+import com.owncloud.android.R
+import com.owncloud.android.databinding.ProfileBottomSheetActionBinding
+import com.owncloud.android.databinding.ProfileBottomSheetFragmentBinding
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.theme.ThemeColorUtils
+import com.owncloud.android.utils.theme.ThemeDrawableUtils
+
+/**
+ * Show actions of an user
+ */
+class ProfileBottomSheetDialog(
+    private val fileActivity: FragmentActivity,
+    private val user: User,
+    private val hoverCard: HoverCard
+) : BottomSheetDialog(fileActivity), DisplayUtils.AvatarGenerationListener {
+    private var _binding: ProfileBottomSheetFragmentBinding? = null
+
+    // This property is only valid between onCreateView and onDestroyView.
+    private val binding get() = _binding!!
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        _binding = ProfileBottomSheetFragmentBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+        if (window != null) {
+            window!!.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+        }
+        val primaryColor = ThemeColorUtils.primaryColor(context, true)
+
+        binding.icon.tag = hoverCard.userId
+        DisplayUtils.setAvatar(
+            user,
+            hoverCard.userId,
+            hoverCard.displayName,
+            this,
+            context.resources.getDimension(R.dimen.list_item_avatar_icon_radius),
+            context.resources,
+            binding.icon,
+            context
+        )
+
+        binding.displayName.text = hoverCard.displayName
+
+        for (action in hoverCard.actions) {
+            val actionBinding = ProfileBottomSheetActionBinding.inflate(
+                layoutInflater
+            )
+            val creatorView: View = actionBinding.root
+
+            if (action.appId == "email") {
+                action.hyperlink = action.title
+                action.title = context.resources.getString(R.string.write_email)
+            }
+
+            actionBinding.name.text = action.title
+
+            val icon = when (action.appId) {
+                "profile" -> R.drawable.ic_user
+                "email" -> R.drawable.ic_email
+                "spreed" -> R.drawable.ic_talk
+                else -> R.drawable.ic_edit
+            }
+            actionBinding.icon.setImageDrawable(
+                ResourcesCompat.getDrawable(
+                    context.resources,
+                    icon,
+                    null
+                )
+            )
+            ThemeDrawableUtils.tintDrawable(actionBinding.icon.drawable, primaryColor)
+
+            creatorView.setOnClickListener { v: View? ->
+                send(hoverCard.userId, action)
+                dismiss()
+            }
+            binding.creators.addView(creatorView)
+        }
+
+        setOnShowListener { d: DialogInterface? ->
+            BottomSheetBehavior.from(binding.root.parent as View)
+                .setPeekHeight(binding.root.measuredHeight)
+        }
+    }
+
+    private fun send(userId: String, action: Action) {
+        when (action.appId) {
+            "profile" -> openWebsite(action.hyperlink)
+            "core" -> sendEmail(action.hyperlink)
+            "spreed" -> openTalk(userId, action.hyperlink)
+        }
+    }
+
+    private fun openWebsite(url: String) {
+        DisplayUtils.startLinkIntent(fileActivity, url)
+    }
+
+    private fun sendEmail(email: String) {
+        val intent = Intent(Intent.ACTION_SENDTO).apply {
+            data = Uri.parse("mailto:")
+            putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
+        }
+
+        DisplayUtils.startIntentIfAppAvailable(intent, fileActivity, R.string.no_email_app_available)
+    }
+
+    private fun openTalk(userId: String, hyperlink: String) {
+        try {
+            val sharingIntent = Intent(Intent.ACTION_VIEW)
+            sharingIntent.setClassName(
+                "com.nextcloud.talk2",
+                "com.nextcloud.talk.activities.MainActivity"
+            )
+            sharingIntent.putExtra("server", user.server.uri)
+            sharingIntent.putExtra("userId", userId)
+            fileActivity.startActivity(sharingIntent)
+        } catch (e: ActivityNotFoundException) {
+            openWebsite(hyperlink)
+        }
+    }
+
+    override fun onStop() {
+        super.onStop()
+        _binding = null
+    }
+
+    override fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any?) {
+        if (callContext is ImageView) {
+            callContext.setImageDrawable(avatarDrawable)
+        }
+    }
+
+    override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean {
+        if (callContext is ImageView) {
+            // needs to be changed once federated users have avatars
+            return callContext.tag.toString() == tag!!.split("@").toTypedArray()[0]
+        }
+        return false
+    }
+}

+ 10 - 1
src/main/java/com/owncloud/android/utils/DisplayUtils.java

@@ -527,6 +527,7 @@ public final class DisplayUtils {
                                                             resources,
                                                             avatarRadius,
                                                             userId,
+                                                            displayName,
                                                             serverName,
                                                             context);
 
@@ -755,10 +756,18 @@ public final class DisplayUtils {
     }
 
     static public void startLinkIntent(Activity activity, @StringRes int link) {
-        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(link)));
+        startLinkIntent(activity, activity.getString(link));
+    }
+
+    static public void startLinkIntent(Activity activity, Uri url) {
+        Intent intent = new Intent(Intent.ACTION_VIEW, url);
         DisplayUtils.startIntentIfAppAvailable(intent, activity, R.string.no_browser_available);
     }
 
+    static public void startLinkIntent(Activity activity, String url) {
+        startLinkIntent(activity, Uri.parse(url));
+    }
+
     static public void startIntentIfAppAvailable(Intent intent, Activity activity, @StringRes int error) {
         if (intent.resolveActivity(activity.getPackageManager()) != null) {
             activity.startActivity(intent);

+ 10 - 9
src/main/res/drawable/ic_talk.xml

@@ -2,7 +2,7 @@
 <!--
   Nextcloud Android client application
 
-  Copyright (C) 2020 Nextcloud GmbH
+  Copyright (C) 2021 Nextcloud GmbH
 
   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
@@ -17,13 +17,14 @@
   You should have received a copy of the GNU Affero General Public
   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="128"
-    android:viewportHeight="128">
+<vector android:autoMirrored="true"
+    android:height="16dp"
+    android:viewportHeight="16"
+    android:viewportWidth="16"
+    android:width="16dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
     <path
-        android:fillColor="#757575"
-        android:pathData="M63.992,0.689C29.031,0.689 0.691,29.031 0.692,63.992c0,34.96 28.34,63.301 63.3,63.302 6.982,-0.014 13.881,-1.183 20.426,-3.43 4.317,-1.482 8.48,-3.433 12.411,-5.831 3.383,1.344 8.59,3.838 13.736,5.902 6.688,2.683 13.274,4.639 15.618,2.399 2.317,-2.212 0.703,-8.809 -1.647,-15.575 -2.046,-5.892 -4.649,-11.913 -5.701,-15.282 2.544,-4.415 4.535,-9.101 5.945,-13.954 1.648,-5.674 2.5,-11.574 2.512,-17.532C127.291,29.032 98.952,0.692 63.992,0.691ZM63.999,24.756l0.001,0c21.677,0 39.25,17.573 39.25,39.251 -0.001,21.677 -17.574,39.249 -39.251,39.249 -21.676,0 -39.249,-17.572 -39.25,-39.249 0,-21.678 17.573,-39.251 39.25,-39.251z"
-        android:strokeWidth="4.78543139" />
+        android:fillColor="#FF000000"
+        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>

+ 2 - 3
src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml

@@ -21,8 +21,7 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:orientation="vertical"
-    android:background="@color/bg_default">
+    android:orientation="vertical">
 
     <TextView
         android:id="@+id/add_to_cloud"
@@ -31,7 +30,7 @@
         android:padding="@dimen/standard_padding"
         android:text="@string/add_to_cloud"
         android:textSize="@dimen/bottom_sheet_text_size"
-        android:textColor="@color/text_color"/>
+        android:textColor="@color/text_color" />
 
     <LinearLayout
         android:id="@+id/menu_upload_files"

+ 51 - 0
src/main/res/layout/profile_bottom_sheet_action.xml

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2019 Tobias Kaminsky
+  ~ Copyright (C) 2019 Nextcloud GmbH
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU Affero 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 Affero General Public License for more details.
+  ~
+  ~ 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:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:paddingStart="24dp"
+    android:paddingTop="@dimen/standard_half_padding"
+    android:paddingEnd="@dimen/standard_padding"
+    android:paddingBottom="@dimen/standard_half_padding"
+    tools:ignore="UseCompoundDrawables">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:contentDescription="@null"
+        tools:src="@drawable/ic_email" />
+
+    <TextView
+        android:id="@+id/name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:paddingStart="24dp"
+        android:paddingEnd="0dp"
+        tools:text="Compose email"
+        android:textColor="@color/text_color"
+        android:textSize="@dimen/bottom_sheet_text_size" />
+</LinearLayout>

+ 73 - 0
src/main/res/layout/profile_bottom_sheet_fragment.xml

@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Nextcloud Android client application
+
+  Copyright (C) 2021 Tobias Kaminsky
+
+  This program is free software; you can redistribute it and/or
+  modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  License as published by the Free Software Foundation; either
+  version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+
+  You should have received a copy of the GNU Affero General Public
+  License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal"
+        tools:ignore="UseCompoundDrawables">
+
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="@dimen/user_icon_size"
+            android:layout_height="@dimen/user_icon_size"
+            android:layout_gravity="center_vertical"
+            android:layout_marginBottom="@dimen/standard_half_margin"
+            android:layout_marginEnd="@dimen/standard_margin"
+            android:layout_marginLeft="@dimen/standard_margin"
+            android:layout_marginRight="@dimen/standard_margin"
+            android:layout_marginStart="@dimen/standard_margin"
+            android:layout_marginTop="@dimen/standard_half_margin"
+            android:contentDescription="@string/user_icon"
+            android:src="@drawable/ic_user" />
+
+        <TextView
+            android:id="@+id/displayName"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:paddingTop="@dimen/standard_padding"
+            android:paddingBottom="@dimen/standard_padding"
+            tools:text="Christine Scott"
+            android:textColor="@color/text_color"
+            android:textSize="@dimen/bottom_sheet_text_size" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/action_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:visibility="visible"
+        tools:visibility="visible">
+
+        <LinearLayout
+            android:id="@+id/creators"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"></LinearLayout>
+
+    </LinearLayout>
+
+</LinearLayout>

+ 3 - 0
src/main/res/values/strings.xml

@@ -781,6 +781,7 @@
     <string name="hint_note">Note</string>
     <string name="no_browser_available">No app available to handle links</string>
     <string name="no_pdf_app_available">No App available to handle PDF</string>
+    <string name="no_email_app_available">No App available to handle mail address</string>
     <string name="share_via_link_hide_download">Hide download</string>
     <string name="unread_comments">Unread comments exist</string>
     <string name="richdocuments_failed_to_load_document">Failed to load document!</string>
@@ -962,4 +963,6 @@
     <string name="strict_mode">Strict mode: no HTTP connection allowed!</string>
     <string name="fullscreen">Fullscreen</string>
     <string name="more">More</string>
+    <string name="write_email">Send email</string>
+    <string name="no_actions">No actions for this user</string>
 </resources>

+ 12 - 1
src/test/java/com/owncloud/android/ui/adapter/ShareeListAdapterTest.kt

@@ -26,6 +26,7 @@ import android.content.res.Resources
 import com.nextcloud.client.account.AnonymousUser
 import com.owncloud.android.lib.resources.shares.OCShare
 import com.owncloud.android.lib.resources.shares.ShareType
+import com.owncloud.android.ui.activity.FileActivity
 import org.junit.Assert
 import org.junit.Test
 import org.mockito.Mock
@@ -37,11 +38,15 @@ class ShareeListAdapterTest {
     @Mock
     private val context: Context? = null
 
+    @Mock
+    private val fileActivity: FileActivity? = null
+
     @Test
     fun testSorting() {
         MockitoAnnotations.openMocks(this)
         val resources = Mockito.mock(Resources::class.java)
         Mockito.`when`(context!!.resources).thenReturn(resources)
+        Mockito.`when`(fileActivity!!.resources).thenReturn(resources)
         val expectedSortOrder: MutableList<OCShare?> = ArrayList()
         expectedSortOrder.add(
             OCShare("/1").apply {
@@ -83,7 +88,13 @@ class ShareeListAdapterTest {
         val randomOrder: MutableList<OCShare?> = ArrayList(expectedSortOrder)
         randomOrder.shuffle()
         val user = AnonymousUser("nextcloud")
-        val sut = ShareeListAdapter(context, randomOrder, null, null, user)
+        val sut = ShareeListAdapter(
+            fileActivity,
+            randomOrder,
+            null,
+            user.accountName,
+            user
+        )
         sut.sortShares()
 
         // compare