Prechádzať zdrojové kódy

Merge pull request #1668 from nextcloud/feature/898/userStatus

add user status
Marcel Hibbe 3 rokov pred
rodič
commit
2ccbbc5969
60 zmenil súbory, kde vykonal 2980 pridanie a 539 odobranie
  1. 1 1
      app/build.gradle
  2. 2 1
      app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt
  3. 89 45
      app/src/main/java/com/nextcloud/talk/activities/CallActivity.java
  4. 1 1
      app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java
  5. 28 0
      app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusClickListener.kt
  6. 49 0
      app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusListAdapter.kt
  7. 58 0
      app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusViewHolder.kt
  8. 28 32
      app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java
  9. 47 26
      app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java
  10. 88 24
      app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java
  11. 122 50
      app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.java
  12. 44 2
      app/src/main/java/com/nextcloud/talk/api/NcApi.java
  13. 2 2
      app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
  14. 42 52
      app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.java
  15. 1 0
      app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java
  16. 16 10
      app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt
  17. 42 9
      app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java
  18. 17 1
      app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java
  19. 4 2
      app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt
  20. 39 0
      app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt
  21. 0 110
      app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.java
  22. 52 0
      app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.kt
  23. 9 0
      app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.java
  24. 18 0
      app/src/main/java/com/nextcloud/talk/models/json/status/ClearAt.kt
  25. 52 0
      app/src/main/java/com/nextcloud/talk/models/json/status/Status.kt
  26. 69 0
      app/src/main/java/com/nextcloud/talk/models/json/status/StatusOCS.java
  27. 64 0
      app/src/main/java/com/nextcloud/talk/models/json/status/StatusOverall.java
  28. 9 0
      app/src/main/java/com/nextcloud/talk/models/json/status/StatusType.kt
  29. 23 0
      app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatus.kt
  30. 37 0
      app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOCS.kt
  31. 37 0
      app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOverall.kt
  32. 71 0
      app/src/main/java/com/nextcloud/talk/models/json/statuses/StatusesOCS.java
  33. 64 0
      app/src/main/java/com/nextcloud/talk/models/json/statuses/StatusesOverall.java
  34. 24 4
      app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java
  35. 133 0
      app/src/main/java/com/nextcloud/talk/ui/StatusDrawable.java
  36. 114 29
      app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java
  37. 479 0
      app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt
  38. 28 0
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  39. 67 0
      app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
  40. 34 0
      app/src/main/res/drawable/ic_edit.xml
  41. 32 0
      app/src/main/res/drawable/ic_user_status_away.xml
  42. 38 0
      app/src/main/res/drawable/ic_user_status_dnd.xml
  43. 34 0
      app/src/main/res/drawable/ic_user_status_invisible.xml
  44. 32 0
      app/src/main/res/drawable/online_status.xml
  45. 26 0
      app/src/main/res/drawable/online_status_with_border.xml
  46. 1 0
      app/src/main/res/layout/current_account_item.xml
  47. 43 5
      app/src/main/res/layout/dialog_choose_account.xml
  48. 458 0
      app/src/main/res/layout/dialog_set_status.xml
  49. 62 0
      app/src/main/res/layout/predefined_status.xml
  50. 2 2
      app/src/main/res/layout/rv_item_contact.xml
  51. 87 49
      app/src/main/res/layout/rv_item_conversation_info_participant.xml
  52. 8 2
      app/src/main/res/layout/rv_item_conversation_with_last_message.xml
  53. 0 77
      app/src/main/res/layout/rv_item_mention.xml
  54. 6 0
      app/src/main/res/values-night/colors.xml
  55. 8 0
      app/src/main/res/values/colors.xml
  56. 6 0
      app/src/main/res/values/dimens.xml
  57. 23 1
      app/src/main/res/values/strings.xml
  58. 8 0
      app/src/main/res/values/styles.xml
  59. 1 1
      scripts/analysis/findbugs-results.txt
  60. 1 1
      scripts/analysis/lint-results.txt

+ 1 - 1
app/build.gradle

@@ -194,7 +194,7 @@ dependencies {
     implementation 'androidx.appcompat:appcompat:1.3.1'
     implementation 'com.google.android.material:material:1.4.0'
     implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
-    implementation 'com.github.vanniktech:Emoji:0.6.0'  // 0.7.0 has display issue - don't update to 0.7.0
+    implementation "com.vanniktech:emoji-google:0.8.0"
     implementation group: 'androidx.emoji', name: 'emoji-bundled', version: '1.1.0'
     implementation 'org.michaelevans.colorart:library:0.0.3'
     implementation "androidx.work:work-runtime:${workVersion}"

+ 2 - 1
app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt

@@ -272,7 +272,8 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
                 apiVersion,
                 signatureVerification.userEntity.baseUrl,
                 decryptedPushMessage.id
-            )
+            ),
+            null
         )
             .repeatWhen { completed ->
                 completed.zipWith(Observable.range(1, 12), { _, i -> i })

+ 89 - 45
app/src/main/java/com/nextcloud/talk/activities/CallActivity.java

@@ -160,6 +160,9 @@ import pub.devrel.easypermissions.AfterPermissionGranted;
 @AutoInjector(NextcloudTalkApplication.class)
 public class CallActivity extends CallBaseActivity {
 
+    public static final String VIDEO_STREAM_TYPE_SCREEN = "screen";
+    public static final String VIDEO_STREAM_TYPE_VIDEO = "video";
+
     @Inject
     NcApi ncApi;
     @Inject
@@ -396,7 +399,8 @@ public class CallActivity extends CallBaseActivity {
         PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
         DefaultVideoEncoderFactory defaultVideoEncoderFactory = new DefaultVideoEncoderFactory(
             rootEglBase.getEglBaseContext(), true, true);
-        DefaultVideoDecoderFactory defaultVideoDecoderFactory = new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext());
+        DefaultVideoDecoderFactory defaultVideoDecoderFactory = new DefaultVideoDecoderFactory(
+            rootEglBase.getEglBaseContext());
 
         peerConnectionFactory = PeerConnectionFactory.builder()
             .setOptions(options)
@@ -436,7 +440,8 @@ public class CallActivity extends CallBaseActivity {
             offerToReceiveVideoString = "false";
         }
 
-        sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", offerToReceiveVideoString));
+        sdpConstraints.mandatory.add(
+            new MediaConstraints.KeyValuePair("OfferToReceiveVideo", offerToReceiveVideoString));
 
         sdpConstraintsForMCU.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
         sdpConstraintsForMCU.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"));
@@ -600,7 +605,8 @@ public class CallActivity extends CallBaseActivity {
         Log.d(TAG, "initGridAdapter");
         int columns;
         int participantsInGrid = participantDisplayItems.size();
-        if (getResources() != null && getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+        if (getResources() != null
+            && getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
             if (participantsInGrid > 2) {
                 columns = 2;
             } else {
@@ -618,7 +624,9 @@ public class CallActivity extends CallBaseActivity {
 
         binding.gridview.setNumColumns(columns);
 
-        binding.conversationRelativeLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+        binding.conversationRelativeLayout
+            .getViewTreeObserver()
+            .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
             @Override
             public void onGlobalLayout() {
                 binding.conversationRelativeLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
@@ -627,7 +635,10 @@ public class CallActivity extends CallBaseActivity {
             }
         });
 
-        binding.callInfosLinearLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+        binding
+            .callInfosLinearLayout
+            .getViewTreeObserver()
+            .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
             @Override
             public void onGlobalLayout() {
                 binding.callInfosLinearLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
@@ -776,7 +787,8 @@ public class CallActivity extends CallBaseActivity {
 
         //Create a VideoSource instance
         if (videoCapturer != null) {
-            SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase.getEglBaseContext());
+            SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread",
+                                                                                    rootEglBase.getEglBaseContext());
             videoSource = peerConnectionFactory.createVideoSource(false);
             videoCapturer.initialize(surfaceTextureHelper, getApplicationContext(), videoSource.getCapturerObserver());
         }
@@ -1141,14 +1153,19 @@ public class CallActivity extends CallBaseActivity {
 
                 @Override
                 public void onNext(@io.reactivex.annotations.NonNull SignalingSettingsOverall signalingSettingsOverall) {
-                    if (signalingSettingsOverall.getOcs() != null && signalingSettingsOverall.getOcs().getSettings() != null) {
+                    if (signalingSettingsOverall.getOcs() != null
+                        && signalingSettingsOverall.getOcs().getSettings() != null) {
                         externalSignalingServer = new ExternalSignalingServer();
 
-                        if (!TextUtils.isEmpty(signalingSettingsOverall.getOcs().getSettings().getExternalSignalingServer()) &&
-                            !TextUtils.isEmpty(signalingSettingsOverall.getOcs().getSettings().getExternalSignalingTicket())) {
+                        if (!TextUtils.isEmpty(
+                            signalingSettingsOverall.getOcs().getSettings().getExternalSignalingServer()) &&
+                            !TextUtils.isEmpty(
+                                signalingSettingsOverall.getOcs().getSettings().getExternalSignalingTicket())) {
                             externalSignalingServer = new ExternalSignalingServer();
-                            externalSignalingServer.setExternalSignalingServer(signalingSettingsOverall.getOcs().getSettings().getExternalSignalingServer());
-                            externalSignalingServer.setExternalSignalingTicket(signalingSettingsOverall.getOcs().getSettings().getExternalSignalingTicket());
+                            externalSignalingServer.setExternalSignalingServer(
+                                signalingSettingsOverall.getOcs().getSettings().getExternalSignalingServer());
+                            externalSignalingServer.setExternalSignalingTicket(
+                                signalingSettingsOverall.getOcs().getSettings().getExternalSignalingTicket());
                             hasExternalSignalingServer = true;
                         } else {
                             hasExternalSignalingServer = false;
@@ -1157,8 +1174,17 @@ public class CallActivity extends CallBaseActivity {
 
                         if (!conversationUser.getUserId().equals("?")) {
                             try {
-                                userUtils.createOrUpdateUser(null, null, null, null, null, null, null,
-                                                             conversationUser.getId(), null, null, LoganSquare.serialize(externalSignalingServer))
+                                userUtils.createOrUpdateUser(null,
+                                                             null,
+                                                             null,
+                                                             null,
+                                                             null,
+                                                             null,
+                                                             null,
+                                                             conversationUser.getId(),
+                                                             null,
+                                                             null,
+                                                             LoganSquare.serialize(externalSignalingServer))
                                     .subscribeOn(Schedulers.io())
                                     .subscribe();
                             } catch (IOException exception) {
@@ -1547,14 +1573,16 @@ public class CallActivity extends CallBaseActivity {
                             sessionDescriptionStringWithPreferredCodec);
 
                         if (peerConnectionWrapper.getPeerConnection() != null) {
-                            peerConnectionWrapper.getPeerConnection().setRemoteDescription(peerConnectionWrapper
-                                                                                                    .getMagicSdpObserver(), sessionDescriptionWithPreferredCodec);
+                            peerConnectionWrapper.getPeerConnection().setRemoteDescription(
+                                peerConnectionWrapper.getMagicSdpObserver(),
+                                sessionDescriptionWithPreferredCodec);
                         }
                         break;
                     case "candidate":
                         NCIceCandidate ncIceCandidate = ncSignalingMessage.getPayload().getIceCandidate();
                         IceCandidate iceCandidate = new IceCandidate(ncIceCandidate.getSdpMid(),
-                                                                     ncIceCandidate.getSdpMLineIndex(), ncIceCandidate.getCandidate());
+                                                                     ncIceCandidate.getSdpMLineIndex(),
+                                                                     ncIceCandidate.getCandidate());
                         peerConnectionWrapper.addCandidate(iceCandidate);
                         break;
                     case "endOfCandidates":
@@ -1651,7 +1679,8 @@ public class CallActivity extends CallBaseActivity {
                 public void onNext(@io.reactivex.annotations.NonNull GenericOverall genericOverall) {
                     if (shutDownView) {
                         finish();
-                    } else if (currentCallStatus == CallStatus.RECONNECTING || currentCallStatus == CallStatus.PUBLISHER_FAILED) {
+                    } else if (currentCallStatus == CallStatus.RECONNECTING
+                        || currentCallStatus == CallStatus.PUBLISHER_FAILED) {
                         initiateCall();
                     }
                 }
@@ -1694,7 +1723,10 @@ public class CallActivity extends CallBaseActivity {
             long inCallFlag = (long) participant.get("inCall");
             if (!participant.get("sessionId").equals(currentSessionId)) {
                 boolean isNewSession;
-                Log.d(TAG, "   inCallFlag of participant " + participant.get("sessionId").toString().substring(0, 4) + " : " + inCallFlag);
+                Log.d(TAG, "   inCallFlag of participant "
+                    + participant.get("sessionId").toString().substring(0, 4)
+                    + " : "
+                    + inCallFlag);
                 isNewSession = inCallFlag != 0;
 
                 if (isNewSession) {
@@ -1733,12 +1765,12 @@ public class CallActivity extends CallBaseActivity {
 
         if (hasMCU) {
             // Ensure that own publishing peer is set up.
-            getPeerConnectionWrapperForSessionIdAndType(webSocketClient.getSessionId(), "video", true);
+            getPeerConnectionWrapperForSessionIdAndType(webSocketClient.getSessionId(), VIDEO_STREAM_TYPE_VIDEO, true);
         }
 
         for (String sessionId : newSessions) {
             Log.d(TAG, "   newSession joined: " + sessionId);
-            getPeerConnectionWrapperForSessionIdAndType(sessionId, "video", false);
+            getPeerConnectionWrapperForSessionIdAndType(sessionId, VIDEO_STREAM_TYPE_VIDEO, false);
         }
 
         if (newSessions.size() > 0 && !currentCallStatus.equals(CallStatus.IN_CONVERSATION)) {
@@ -1755,7 +1787,7 @@ public class CallActivity extends CallBaseActivity {
         Log.d(TAG, "getPeersForCall");
         int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
 
-        ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken))
+        ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken), null)
             .subscribeOn(Schedulers.io())
             .subscribe(new Observer<ParticipantsOverall>() {
                 @Override
@@ -1790,7 +1822,8 @@ public class CallActivity extends CallBaseActivity {
 
     private PeerConnectionWrapper getPeerConnectionWrapperForSessionId(String sessionId, String type) {
         for (int i = 0; i < peerConnectionWrapperList.size(); i++) {
-            if (peerConnectionWrapperList.get(i).getSessionId().equals(sessionId) && peerConnectionWrapperList.get(i).getVideoStreamType().equals(type)) {
+            if (peerConnectionWrapperList.get(i).getSessionId().equals(sessionId)
+                && peerConnectionWrapperList.get(i).getVideoStreamType().equals(type)) {
                 return peerConnectionWrapperList.get(i);
             }
         }
@@ -1798,7 +1831,9 @@ public class CallActivity extends CallBaseActivity {
         return null;
     }
 
-    private PeerConnectionWrapper getPeerConnectionWrapperForSessionIdAndType(String sessionId, String type, boolean publisher) {
+    private PeerConnectionWrapper getPeerConnectionWrapperForSessionIdAndType(String sessionId,
+                                                                              String type,
+                                                                              boolean publisher) {
         PeerConnectionWrapper peerConnectionWrapper;
         if ((peerConnectionWrapper = getPeerConnectionWrapperForSessionId(sessionId, type)) != null) {
             return peerConnectionWrapper;
@@ -1876,7 +1911,7 @@ public class CallActivity extends CallBaseActivity {
             for (int i = 0; i < peerConnectionWrappers.size(); i++) {
                 peerConnectionWrapper = peerConnectionWrappers.get(i);
                 if (peerConnectionWrapper.getSessionId().equals(sessionId)) {
-                    if (peerConnectionWrapper.getVideoStreamType().equals("screen") || !justScreen) {
+                    if (VIDEO_STREAM_TYPE_SCREEN.equals(peerConnectionWrapper.getVideoStreamType()) || !justScreen) {
                         runOnUiThread(() -> removeMediaStream(sessionId));
                         deletePeerConnection(peerConnectionWrapper);
                     }
@@ -1904,7 +1939,8 @@ public class CallActivity extends CallBaseActivity {
     private void updateSelfVideoViewPosition() {
         Log.d(TAG, "updateSelfVideoViewPosition");
         if (!isInPipMode) {
-            FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) binding.selfVideoRenderer.getLayoutParams();
+            FrameLayout.LayoutParams layoutParams =
+                (FrameLayout.LayoutParams) binding.selfVideoRenderer.getLayoutParams();
 
             DisplayMetrics displayMetrics = getApplicationContext().getResources().getDisplayMetrics();
             int screenWidthPx = displayMetrics.widthPixels;
@@ -1941,42 +1977,46 @@ public class CallActivity extends CallBaseActivity {
     public void onMessageEvent(PeerConnectionEvent peerConnectionEvent) {
         String sessionId = peerConnectionEvent.getSessionId();
 
-        if (peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent.PeerConnectionEventType
-                                                                        .PEER_CLOSED)) {
-            endPeerConnection(sessionId, peerConnectionEvent.getVideoStreamType().equals("screen"));
-        } else if (peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent
-                                                                               .PeerConnectionEventType.SENSOR_FAR) ||
-            peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent
-                                                                        .PeerConnectionEventType.SENSOR_NEAR)) {
+        if (peerConnectionEvent.getPeerConnectionEventType() ==
+            PeerConnectionEvent.PeerConnectionEventType.PEER_CLOSED) {
+            endPeerConnection(sessionId, VIDEO_STREAM_TYPE_SCREEN.equals(peerConnectionEvent.getVideoStreamType()));
+        } else if (peerConnectionEvent.getPeerConnectionEventType() ==
+            PeerConnectionEvent.PeerConnectionEventType.SENSOR_FAR ||
+            peerConnectionEvent.getPeerConnectionEventType() ==
+                PeerConnectionEvent.PeerConnectionEventType.SENSOR_NEAR) {
 
             if (!isVoiceOnlyCall) {
-                boolean enableVideo = peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent
-                                                                                                  .PeerConnectionEventType.SENSOR_FAR) && videoOn;
+                boolean enableVideo = peerConnectionEvent.getPeerConnectionEventType() ==
+                    PeerConnectionEvent.PeerConnectionEventType.SENSOR_FAR && videoOn;
                 if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) &&
                     (currentCallStatus.equals(CallStatus.CONNECTING) || isConnectionEstablished()) && videoOn
                     && enableVideo != localVideoTrack.enabled()) {
                     toggleMedia(enableVideo, true);
                 }
             }
-        } else if (peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent.PeerConnectionEventType.NICK_CHANGE)) {
+        } else if (peerConnectionEvent.getPeerConnectionEventType() ==
+            PeerConnectionEvent.PeerConnectionEventType.NICK_CHANGE) {
             if (participantDisplayItems.get(sessionId) != null) {
                 participantDisplayItems.get(sessionId).setNick(peerConnectionEvent.getNick());
             }
             participantsAdapter.notifyDataSetChanged();
 
-        } else if (peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent.PeerConnectionEventType.VIDEO_CHANGE) && !isVoiceOnlyCall) {
+        } else if (peerConnectionEvent.getPeerConnectionEventType() ==
+            PeerConnectionEvent.PeerConnectionEventType.VIDEO_CHANGE && !isVoiceOnlyCall) {
             if (participantDisplayItems.get(sessionId) != null) {
                 participantDisplayItems.get(sessionId).setStreamEnabled(peerConnectionEvent.getChangeValue());
             }
             participantsAdapter.notifyDataSetChanged();
 
-        } else if (peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent.PeerConnectionEventType.AUDIO_CHANGE)) {
+        } else if (peerConnectionEvent.getPeerConnectionEventType() ==
+            PeerConnectionEvent.PeerConnectionEventType.AUDIO_CHANGE) {
             if (participantDisplayItems.get(sessionId) != null) {
                 participantDisplayItems.get(sessionId).setAudioEnabled(peerConnectionEvent.getChangeValue());
             }
             participantsAdapter.notifyDataSetChanged();
 
-        } else if (peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent.PeerConnectionEventType.PUBLISHER_FAILED)) {
+        } else if (peerConnectionEvent.getPeerConnectionEventType() ==
+            PeerConnectionEvent.PeerConnectionEventType.PUBLISHER_FAILED) {
             currentCallStatus = CallStatus.PUBLISHER_FAILED;
             webSocketClient.clearResumeId();
             hangup(false);
@@ -2074,7 +2114,8 @@ public class CallActivity extends CallBaseActivity {
             StringBuilder stringBuilder = new StringBuilder();
             stringBuilder.append("{")
                 .append("\"fn\":\"")
-                .append(StringEscapeUtils.escapeJson(LoganSquare.serialize(ncMessageWrapper.getSignalingMessage()))).append("\"")
+                .append(StringEscapeUtils.escapeJson(LoganSquare.serialize(ncMessageWrapper.getSignalingMessage())))
+                .append("\"")
                 .append(",")
                 .append("\"sessionId\":")
                 .append("\"").append(StringEscapeUtils.escapeJson(callSession)).append("\"")
@@ -2127,7 +2168,10 @@ public class CallActivity extends CallBaseActivity {
                                                          this);
     }
 
-    private void setupVideoStreamForLayout(@Nullable MediaStream mediaStream, String session, boolean videoStreamEnabled, String videoStreamType) {
+    private void setupVideoStreamForLayout(@Nullable MediaStream mediaStream,
+                                           String session,
+                                           boolean videoStreamEnabled,
+                                           String videoStreamType) {
         String nick;
         if (hasExternalSignalingServer) {
             nick = webSocketClient.getDisplayNameForSession(session);
@@ -2416,13 +2460,12 @@ public class CallActivity extends CallBaseActivity {
 
     @Subscribe(threadMode = ThreadMode.BACKGROUND)
     public void onMessageEvent(NetworkEvent networkEvent) {
-        if (networkEvent.getNetworkConnectionEvent()
-            .equals(NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED)) {
+        if (networkEvent.getNetworkConnectionEvent() == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED) {
             if (handler != null) {
                 handler.removeCallbacksAndMessages(null);
             }
-        } else if (networkEvent.getNetworkConnectionEvent()
-            .equals(NetworkEvent.NetworkConnectionEvent.NETWORK_DISCONNECTED)) {
+        } else if (networkEvent.getNetworkConnectionEvent() ==
+            NetworkEvent.NetworkConnectionEvent.NETWORK_DISCONNECTED) {
             if (handler != null) {
                 handler.removeCallbacksAndMessages(null);
             }
@@ -2516,7 +2559,8 @@ public class CallActivity extends CallBaseActivity {
         if (isVoiceOnlyCall) {
             binding.callControls.setVisibility(View.VISIBLE);
         } else {
-            binding.callControls.setVisibility(View.INVISIBLE); // animateCallControls needs this to be invisible for a check.
+            // animateCallControls needs this to be invisible for a check.
+            binding.callControls.setVisibility(View.INVISIBLE);
         }
         initViews();
 

+ 1 - 1
app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java

@@ -215,7 +215,7 @@ public class CallNotificationActivity extends CallBaseActivity {
         int apiVersion = ApiUtils.getCallApiVersion(userBeingCalled, new int[]{ApiUtils.APIv4, 1});
 
         ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(apiVersion, userBeingCalled.getBaseUrl(),
-                                                                  currentConversation.getToken()))
+                                                                  currentConversation.getToken()), null)
             .subscribeOn(Schedulers.io())
             .repeatWhen(completed -> completed.zipWith(Observable.range(1, 12), (n, i) -> i)
                 .flatMap(retryCount -> Observable.timer(5, TimeUnit.SECONDS))

+ 28 - 0
app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusClickListener.kt

@@ -0,0 +1,28 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 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.nextcloud.talk.adapters
+
+import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
+
+interface PredefinedStatusClickListener {
+    fun onClick(predefinedStatus: PredefinedStatus)
+}

+ 49 - 0
app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusListAdapter.kt

@@ -0,0 +1,49 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 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.nextcloud.talk.adapters
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.talk.databinding.PredefinedStatusBinding
+import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
+
+class PredefinedStatusListAdapter(
+    private val clickListener: PredefinedStatusClickListener,
+    val context: Context
+) : RecyclerView.Adapter<PredefinedStatusViewHolder>() {
+    internal var list: List<PredefinedStatus> = emptyList()
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PredefinedStatusViewHolder {
+        val itemBinding = PredefinedStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+        return PredefinedStatusViewHolder(itemBinding)
+    }
+
+    override fun onBindViewHolder(holder: PredefinedStatusViewHolder, position: Int) {
+        holder.bind(list[position], clickListener, context)
+    }
+
+    override fun getItemCount(): Int {
+        return list.size
+    }
+}

+ 58 - 0
app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusViewHolder.kt

@@ -0,0 +1,58 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 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.nextcloud.talk.adapters
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.talk.R
+import com.nextcloud.talk.databinding.PredefinedStatusBinding
+import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
+import com.nextcloud.talk.utils.DisplayUtils
+
+private const val ONE_SECOND_IN_MILLIS = 1000
+
+class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) : RecyclerView.ViewHolder(binding.root) {
+
+    fun bind(status: PredefinedStatus, clickListener: PredefinedStatusClickListener, context: Context) {
+        binding.root.setOnClickListener { clickListener.onClick(status) }
+        binding.icon.text = status.icon
+        binding.name.text = status.message
+
+        if (status.clearAt == null) {
+            binding.clearAt.text = context.getString(R.string.dontClear)
+        } else {
+            val clearAt = status.clearAt!!
+            if (clearAt.type.equals("period")) {
+                binding.clearAt.text = DisplayUtils.getRelativeTimestamp(
+                    context,
+                    System.currentTimeMillis() + clearAt.time.toInt() * ONE_SECOND_IN_MILLIS,
+                    true
+                )
+            } else {
+                // end-of
+                if (clearAt.time.equals("day")) {
+                    binding.clearAt.text = context.getString(R.string.today)
+                }
+            }
+        }
+    }
+}

+ 28 - 32
app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
  *
  * This program is free software: you can redistribute it and/or modify
@@ -24,17 +26,12 @@ import android.accounts.Account;
 import android.net.Uri;
 import android.text.TextUtils;
 import android.view.View;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
 
 import com.facebook.drawee.backends.pipeline.Fresco;
 import com.facebook.drawee.interfaces.DraweeController;
-import com.facebook.drawee.view.SimpleDraweeView;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
+import com.nextcloud.talk.databinding.AccountItemBinding;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.participants.Participant;
 import com.nextcloud.talk.utils.ApiUtils;
@@ -44,9 +41,6 @@ import java.util.List;
 import java.util.regex.Pattern;
 
 import androidx.annotation.Nullable;
-import androidx.emoji.widget.EmojiTextView;
-import butterknife.BindView;
-import butterknife.ButterKnife;
 import eu.davidea.flexibleadapter.FlexibleAdapter;
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
 import eu.davidea.flexibleadapter.items.IFilterable;
@@ -56,10 +50,10 @@ import eu.davidea.viewholders.FlexibleViewHolder;
 public class AdvancedUserItem extends AbstractFlexibleItem<AdvancedUserItem.UserItemViewHolder> implements
         IFilterable<String> {
 
-    private Participant participant;
-    private UserEntity userEntity;
+    private final Participant participant;
+    private final UserEntity userEntity;
     @Nullable
-    private Account account;
+    private final Account account;
 
     public AdvancedUserItem(Participant participant, UserEntity userEntity, @Nullable Account account) {
         this.participant = participant;
@@ -110,68 +104,70 @@ public class AdvancedUserItem extends AbstractFlexibleItem<AdvancedUserItem.User
 
     @Override
     public void bindViewHolder(FlexibleAdapter adapter, UserItemViewHolder holder, int position, List payloads) {
-        holder.avatarImageView.setController(null);
+        holder.binding.userIcon.setController(null);
 
         if (adapter.hasFilter()) {
             FlexibleUtils.highlightText(
-                    holder.contactDisplayName,
+                    holder.binding.userName,
                     participant.getDisplayName(),
                     String.valueOf(adapter.getFilter(String.class)),
                     NextcloudTalkApplication.Companion.getSharedApplication()
                             .getResources()
                             .getColor(R.color.colorPrimary));
         } else {
-            holder.contactDisplayName.setText(participant.getDisplayName());
+            holder.binding.userName.setText(participant.getDisplayName());
         }
 
         if (userEntity != null && !TextUtils.isEmpty(userEntity.getBaseUrl())) {
             String host = Uri.parse(userEntity.getBaseUrl()).getHost();
             if (!TextUtils.isEmpty(host)) {
-                holder.serverUrl.setText(Uri.parse(userEntity.getBaseUrl()).getHost());
+                holder.binding.account.setText(Uri.parse(userEntity.getBaseUrl()).getHost());
             } else {
-                holder.serverUrl.setText(userEntity.getBaseUrl());
+                holder.binding.account.setText(userEntity.getBaseUrl());
             }
         }
 
-        holder.avatarImageView.getHierarchy().setPlaceholderImage(R.drawable.account_circle_48dp);
-        holder.avatarImageView.getHierarchy().setFailureImage(R.drawable.account_circle_48dp);
+        holder.binding.userIcon.getHierarchy().setPlaceholderImage(R.drawable.account_circle_48dp);
+        holder.binding.userIcon.getHierarchy().setFailureImage(R.drawable.account_circle_48dp);
 
         if (userEntity != null && userEntity.getBaseUrl() != null &&
                 userEntity.getBaseUrl().startsWith("http://") ||
                 userEntity.getBaseUrl().startsWith("https://")) {
 
             DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                .setOldController(holder.avatarImageView.getController())
+                .setOldController(holder.binding.userIcon.getController())
                 .setAutoPlayAnimations(true)
-                .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userEntity.getBaseUrl(),
-                                                                                                     participant.getActorId(), R.dimen.small_item_height), null))
+                .setImageRequest(
+                    DisplayUtils.getImageRequestForUrl(
+                        ApiUtils.getUrlForAvatarWithName(
+                            userEntity.getBaseUrl(),
+                            participant.getActorId(),
+                            R.dimen.small_item_height),
+                        null))
                 .build();
-            holder.avatarImageView.setController(draweeController);
+            holder.binding.userIcon.setController(draweeController);
         }
     }
 
     @Override
     public boolean filter(String constraint) {
         return participant.getDisplayName() != null &&
-                Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(participant.getDisplayName().trim()).find();
+                Pattern
+                    .compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
+                    .matcher(participant.getDisplayName().trim())
+                    .find();
     }
 
-
     static class UserItemViewHolder extends FlexibleViewHolder {
 
-        @BindView(R.id.user_name)
-        public EmojiTextView contactDisplayName;
-        @BindView(R.id.account)
-        public TextView serverUrl;
-        @BindView(R.id.user_icon)
-        public SimpleDraweeView avatarImageView;
+        public AccountItemBinding binding;
 
         /**
          * Default constructor.
          */
         UserItemViewHolder(View view, FlexibleAdapter adapter) {
             super(view, adapter);
-            ButterKnife.bind(this, view);
+            binding = AccountItemBinding.bind(view);
         }
     }
 }

+ 47 - 26
app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java

@@ -3,6 +3,8 @@
  *
  * @author Mario Danic
  * @author Andy Scherzinger
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
  * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
@@ -46,6 +48,9 @@ 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.models.json.conversations.Conversation;
+import com.nextcloud.talk.models.json.status.Status;
+import com.nextcloud.talk.models.json.status.StatusType;
+import com.nextcloud.talk.ui.StatusDrawable;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 
@@ -66,26 +71,30 @@ import eu.davidea.flexibleadapter.utils.FlexibleUtils;
 import eu.davidea.viewholders.FlexibleViewHolder;
 
 public class ConversationItem extends AbstractFlexibleItem<ConversationItem.ConversationItemViewHolder> implements ISectionable<ConversationItem.ConversationItemViewHolder, GenericTextHeaderItem>,
-        IFilterable<String> {
+    IFilterable<String> {
 
+    private static final float STATUS_SIZE_IN_DP = 9f;
 
     private Conversation conversation;
     private UserEntity userEntity;
     private Context context;
     private GenericTextHeaderItem header;
+    private Status status;
 
-    public ConversationItem(Conversation conversation, UserEntity userEntity, Context activityContext) {
+    public ConversationItem(Conversation conversation, UserEntity userEntity, Context activityContext, Status status) {
         this.conversation = conversation;
         this.userEntity = userEntity;
         this.context = activityContext;
+        this.status = status;
     }
 
     public ConversationItem(Conversation conversation, UserEntity userEntity,
-                            Context activityContext, GenericTextHeaderItem genericTextHeaderItem) {
+                            Context activityContext, GenericTextHeaderItem genericTextHeaderItem, Status status) {
         this.conversation = conversation;
         this.userEntity = userEntity;
         this.context = activityContext;
         this.header = genericTextHeaderItem;
+        this.status = status;
     }
 
     @Override
@@ -120,7 +129,7 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
     @Override
     public void bindViewHolder(FlexibleAdapter<IFlexible> adapter, ConversationItemViewHolder holder, int position, List<Object> payloads) {
         Context appContext =
-                NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext();
+            NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext();
         holder.dialogAvatar.setController(null);
 
         holder.dialogName.setTextColor(ResourcesCompat.getColor(context.getResources(),
@@ -129,8 +138,8 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
 
         if (adapter.hasFilter()) {
             FlexibleUtils.highlightText(holder.dialogName, conversation.getDisplayName(),
-                    String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.Companion.getSharedApplication()
-                            .getResources().getColor(R.color.colorPrimary));
+                                        String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.Companion.getSharedApplication()
+                                            .getResources().getColor(R.color.colorPrimary));
         } else {
             holder.dialogName.setText(conversation.getDisplayName());
         }
@@ -147,19 +156,19 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
 
             ColorStateList lightBubbleFillColor = ColorStateList.valueOf(
                 ContextCompat.getColor(context,
-                R.color.conversation_unread_bubble));
+                                       R.color.conversation_unread_bubble));
             int lightBubbleTextColor = ContextCompat.getColor(
                 context,
                 R.color.conversation_unread_bubble_text);
             ColorStateList lightBubbleStrokeColor = ColorStateList.valueOf(
                 ContextCompat.getColor(context,
-                R.color.colorPrimary));
+                                       R.color.colorPrimary));
 
             if (conversation.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
                 holder.dialogUnreadBubble.setChipBackgroundColorResource(R.color.colorPrimary);
                 holder.dialogUnreadBubble.setTextColor(Color.WHITE);
             } else if (conversation.isUnreadMention()) {
-                if (CapabilitiesUtil.hasSpreedFeatureCapability(userEntity, "direct-mention-flag")){
+                if (CapabilitiesUtil.hasSpreedFeatureCapability(userEntity, "direct-mention-flag")) {
                     if (conversation.getUnreadMentionDirect()) {
                         holder.dialogUnreadBubble.setChipBackgroundColorResource(R.color.colorPrimary);
                         holder.dialogUnreadBubble.setTextColor(Color.WHITE);
@@ -192,28 +201,38 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
             holder.pinnedConversationImageView.setVisibility(View.GONE);
         }
 
+        if (Conversation.ConversationType.ROOM_SYSTEM != conversation.getType()) {
+            float size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, appContext);
+            holder.userStatusImage.setImageDrawable(new StatusDrawable(
+                status != null ? status.getStatus() : "",
+                status != null ? status.getIcon() : "",
+                size,
+                context.getResources().getColor(R.color.bg_default),
+                appContext));
+        }
+
         if (conversation.getLastMessage() != null) {
             holder.dialogDate.setVisibility(View.VISIBLE);
             holder.dialogDate.setText(DateUtils.getRelativeTimeSpanString(conversation.getLastActivity() * 1000L,
-                    System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
+                                                                          System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
 
-            if (!TextUtils.isEmpty(conversation.getLastMessage().getSystemMessage()) || Conversation.ConversationType.ROOM_SYSTEM.equals(conversation.getType())) {
+            if (!TextUtils.isEmpty(conversation.getLastMessage().getSystemMessage()) || Conversation.ConversationType.ROOM_SYSTEM == conversation.getType()) {
                 holder.dialogLastMessage.setText(conversation.getLastMessage().getText());
             } else {
                 String authorDisplayName = "";
                 conversation.getLastMessage().setActiveUser(userEntity);
                 String text;
-                if (conversation.getLastMessage().getMessageType().equals(ChatMessage.MessageType.REGULAR_TEXT_MESSAGE)) {
+                if (conversation.getLastMessage().getMessageType() == ChatMessage.MessageType.REGULAR_TEXT_MESSAGE) {
                     if (conversation.getLastMessage().getActorId().equals(userEntity.getUserId())) {
                         text = String.format(appContext.getString(R.string.nc_formatted_message_you),
-                                conversation.getLastMessage().getLastMessageDisplayText());
+                                             conversation.getLastMessage().getLastMessageDisplayText());
                     } else {
                         authorDisplayName = !TextUtils.isEmpty(conversation.getLastMessage().getActorDisplayName()) ?
-                                conversation.getLastMessage().getActorDisplayName() :
-                                "guests".equals(conversation.getLastMessage().getActorType()) ? appContext.getString(R.string.nc_guest) : "";
+                            conversation.getLastMessage().getActorDisplayName() :
+                            "guests".equals(conversation.getLastMessage().getActorType()) ? appContext.getString(R.string.nc_guest) : "";
                         text = String.format(appContext.getString(R.string.nc_formatted_message),
-                                authorDisplayName,
-                                conversation.getLastMessage().getLastMessageDisplayText());
+                                             authorDisplayName,
+                                             conversation.getLastMessage().getLastMessageDisplayText());
                     }
                 } else {
                     text = conversation.getLastMessage().getLastMessageDisplayText();
@@ -266,22 +285,22 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
                 case ROOM_TYPE_ONE_TO_ONE_CALL:
                     if (!TextUtils.isEmpty(conversation.getName())) {
                         DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                                .setOldController(holder.dialogAvatar.getController())
-                                .setAutoPlayAnimations(true)
-                                .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userEntity.getBaseUrl(), conversation.getName(), R.dimen.avatar_size), userEntity))
-                                .build();
+                            .setOldController(holder.dialogAvatar.getController())
+                            .setAutoPlayAnimations(true)
+                            .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userEntity.getBaseUrl(), conversation.getName(), R.dimen.avatar_size), userEntity))
+                            .build();
                         holder.dialogAvatar.setController(draweeController);
                     } else {
                         holder.dialogAvatar.setVisibility(View.GONE);
                     }
                     break;
                 case ROOM_GROUP_CALL:
-                        holder.dialogAvatar.setImageDrawable(ContextCompat.getDrawable(context,
-                                                                                       R.drawable.ic_circular_group));
+                    holder.dialogAvatar.setImageDrawable(ContextCompat.getDrawable(context,
+                                                                                   R.drawable.ic_circular_group));
                     break;
                 case ROOM_PUBLIC_CALL:
-                        holder.dialogAvatar.setImageDrawable(ContextCompat.getDrawable(context,
-                                                                                       R.drawable.ic_circular_link));
+                    holder.dialogAvatar.setImageDrawable(ContextCompat.getDrawable(context,
+                                                                                   R.drawable.ic_circular_link));
                     break;
                 default:
                     holder.dialogAvatar.setVisibility(View.GONE);
@@ -292,7 +311,7 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
     @Override
     public boolean filter(String constraint) {
         return conversation.getDisplayName() != null &&
-                Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(conversation.getDisplayName().trim()).find();
+            Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(conversation.getDisplayName().trim()).find();
     }
 
     @Override
@@ -318,6 +337,8 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
         Chip dialogUnreadBubble;
         @BindView(R.id.favoriteConversationImageView)
         ImageView pinnedConversationImageView;
+        @BindView(R.id.user_status_image)
+        ImageView userStatusImage;
 
         ConversationItemViewHolder(View view, FlexibleAdapter adapter) {
             super(view, adapter);

+ 88 - 24
app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java

@@ -1,7 +1,9 @@
 /*
  * Nextcloud Talk application
  *
+ * @author Marcel Hibbe
  * @author Mario Danic
+ * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -29,12 +31,17 @@ import com.facebook.drawee.interfaces.DraweeController;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.models.database.UserEntity;
+import com.nextcloud.talk.models.json.mention.Mention;
+import com.nextcloud.talk.models.json.status.StatusType;
+import com.nextcloud.talk.ui.StatusDrawable;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 
 import java.util.List;
+import java.util.Objects;
 import java.util.regex.Pattern;
 
+import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.core.content.res.ResourcesCompat;
 import eu.davidea.flexibleadapter.FlexibleAdapter;
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
@@ -45,23 +52,30 @@ import eu.davidea.flexibleadapter.utils.FlexibleUtils;
 public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
         implements IFilterable<String> {
 
+    private static final float STATUS_SIZE_IN_DP = 9f;
+    private static final String NO_ICON = "";
     public static final String SOURCE_CALLS = "calls";
     public static final String SOURCE_GUESTS = "guests";
-    private String objectId;
-    private String displayName;
+
     private String source;
-    private UserEntity currentUser;
-    private Context context;
+    private final String objectId;
+    private final String displayName;
+    private final String status;
+    private final String statusIcon;
+    private final String statusMessage;
+    private final UserEntity currentUser;
+    private final Context context;
 
     public MentionAutocompleteItem(
-            String objectId,
-            String displayName,
-            String source,
+            Mention mention,
             UserEntity currentUser,
             Context activityContext) {
-        this.objectId = objectId;
-        this.displayName = displayName;
-        this.source = source;
+        this.objectId = mention.getId();
+        this.displayName = mention.getLabel();
+        this.source = mention.getSource();
+        this.status = mention.getStatus();
+        this.statusIcon = mention.getStatusIcon();
+        this.statusMessage = mention.getStatusMessage();
         this.currentUser = currentUser;
         this.context = activityContext;
     }
@@ -94,7 +108,7 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
 
     @Override
     public int getLayoutRes() {
-        return R.layout.rv_item_mention;
+        return R.layout.rv_item_conversation_info_participant;
     }
 
     @Override
@@ -118,14 +132,14 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
             FlexibleUtils.highlightText(holder.contactDisplayName,
                                         displayName,
                                         String.valueOf(adapter.getFilter(String.class)),
-                                        NextcloudTalkApplication.Companion.getSharedApplication()
-                                                .getResources().getColor(R.color.colorPrimary));
+                                        Objects.requireNonNull(NextcloudTalkApplication.Companion.getSharedApplication())
+                                            .getResources().getColor(R.color.colorPrimary));
             if (holder.contactMentionId != null) {
                 FlexibleUtils.highlightText(holder.contactMentionId,
                                             "@" + objectId,
                                             String.valueOf(adapter.getFilter(String.class)),
                                             NextcloudTalkApplication.Companion.getSharedApplication()
-                                                    .getResources().getColor(R.color.colorPrimary));
+                                                .getResources().getColor(R.color.colorPrimary));
             }
         } else {
             holder.contactDisplayName.setText(displayName);
@@ -135,7 +149,9 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
         }
 
         if (SOURCE_CALLS.equals(source)) {
-            holder.simpleDraweeView.setImageResource(R.drawable.ic_circular_group);
+            if (holder.participantAvatar != null){
+                holder.participantAvatar.setImageResource(R.drawable.ic_circular_group);
+            }
         } else {
             String avatarId = objectId;
             String avatarUrl = ApiUtils.getUrlForAvatarWithName(currentUser.getBaseUrl(),
@@ -144,21 +160,69 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
             if (SOURCE_GUESTS.equals(source)) {
                 avatarId = displayName;
                 avatarUrl = ApiUtils.getUrlForAvatarWithNameForGuests(
-                        currentUser.getBaseUrl(),
-                        avatarId,
-                        R.dimen.avatar_size_big);
+                    currentUser.getBaseUrl(),
+                    avatarId,
+                    R.dimen.avatar_size_big);
+            }
+
+            if(holder.participantAvatar != null){
+                holder.participantAvatar.setController(null);
             }
 
-            holder.simpleDraweeView.setController(null);
             DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                    .setOldController(holder.simpleDraweeView.getController())
-                    .setAutoPlayAnimations(true)
-                    .setImageRequest(DisplayUtils.getImageRequestForUrl(avatarUrl, null))
-                    .build();
-            holder.simpleDraweeView.setController(draweeController);
+                .setOldController(holder.participantAvatar.getController())
+                .setAutoPlayAnimations(true)
+                .setImageRequest(DisplayUtils.getImageRequestForUrl(avatarUrl, null))
+                .build();
+            holder.participantAvatar.setController(draweeController);
+        }
+
+        drawStatus(holder);
+    }
+
+    private void drawStatus(UserItem.UserItemViewHolder holder) {
+        if (holder.statusMessage != null && holder.participantEmoji != null && holder.userStatusImage != null) {
+            float size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, context);
+            holder.userStatusImage.setImageDrawable(new StatusDrawable(
+                status,
+                NO_ICON,
+                size,
+                context.getResources().getColor(R.color.bg_default),
+                context));
+
+            if (statusMessage != null) {
+                holder.statusMessage.setText(statusMessage);
+                alignUsernameVertical(holder, 0);
+            } else {
+                holder.statusMessage.setText("");
+                alignUsernameVertical(holder, 10);
+            }
+
+            if (statusIcon != null && !statusIcon.isEmpty()) {
+                holder.participantEmoji.setText(statusIcon);
+            } else {
+                holder.participantEmoji.setVisibility(View.GONE);
+            }
+
+            if (status != null && status.equals(StatusType.DND.getString())) {
+                if (statusMessage == null || statusMessage.isEmpty()) {
+                    holder.statusMessage.setText(R.string.dnd);
+                }
+            } else if (status != null && status.equals(StatusType.AWAY.getString())) {
+                if (statusMessage == null || statusMessage.isEmpty()) {
+                    holder.statusMessage.setText(R.string.away);
+                }
+            }
         }
     }
 
+    private void alignUsernameVertical(UserItem.UserItemViewHolder holder, float densityPixelsFromTop) {
+        ConstraintLayout.LayoutParams layoutParams =
+            (ConstraintLayout.LayoutParams) holder.contactDisplayName.getLayoutParams();
+        layoutParams.topMargin = (int) DisplayUtils.convertDpToPixel(densityPixelsFromTop, context);
+        holder.contactDisplayName.setLayoutParams(layoutParams);
+    }
+
     @Override
     public boolean filter(String constraint) {
         return objectId != null &&

+ 122 - 50
app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.java

@@ -2,7 +2,9 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Marcel Hibbe
  * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
+ * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,15 +22,13 @@
 
 package com.nextcloud.talk.adapters.items;
 
+import android.annotation.SuppressLint;
+import android.content.Context;
 import android.content.res.Resources;
 import android.text.TextUtils;
 import android.view.View;
 import android.widget.ImageView;
 
-import androidx.annotation.Nullable;
-import androidx.core.content.res.ResourcesCompat;
-import androidx.emoji.widget.EmojiTextView;
-
 import com.facebook.drawee.backends.pipeline.Fresco;
 import com.facebook.drawee.interfaces.DraweeController;
 import com.facebook.drawee.view.SimpleDraweeView;
@@ -38,12 +38,18 @@ import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.converters.EnumParticipantTypeConverter;
 import com.nextcloud.talk.models.json.participants.Participant;
 import com.nextcloud.talk.models.json.participants.Participant.InCallFlags;
+import com.nextcloud.talk.models.json.status.StatusType;
+import com.nextcloud.talk.ui.StatusDrawable;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 
 import java.util.List;
 import java.util.regex.Pattern;
 
+import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.emoji.widget.EmojiTextView;
 import butterknife.BindView;
 import butterknife.ButterKnife;
 import eu.davidea.flexibleadapter.FlexibleAdapter;
@@ -54,14 +60,22 @@ import eu.davidea.flexibleadapter.utils.FlexibleUtils;
 import eu.davidea.viewholders.FlexibleViewHolder;
 
 public class UserItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder> implements
-        ISectionable<UserItem.UserItemViewHolder, GenericTextHeaderItem>, IFilterable<String> {
+    ISectionable<UserItem.UserItemViewHolder, GenericTextHeaderItem>, IFilterable<String> {
 
+    private static final float STATUS_SIZE_IN_DP = 9f;
+    private static final String NO_ICON = "";
+
+    private Context context;
     private Participant participant;
     private UserEntity userEntity;
     private GenericTextHeaderItem header;
     public boolean isOnline = true;
 
-    public UserItem(Participant participant, UserEntity userEntity, GenericTextHeaderItem genericTextHeaderItem) {
+    public UserItem(Context activityContext,
+                    Participant participant,
+                    UserEntity userEntity,
+                    GenericTextHeaderItem genericTextHeaderItem) {
+        this.context = activityContext;
         this.participant = participant;
         this.userEntity = userEntity;
         this.header = genericTextHeaderItem;
@@ -72,7 +86,7 @@ public class UserItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
         if (o instanceof UserItem) {
             UserItem inItem = (UserItem) o;
             return participant.getActorType() == inItem.getModel().getActorType() &&
-                    participant.getActorId().equals(inItem.getModel().getActorId());
+                participant.getActorId().equals(inItem.getModel().getActorId());
         }
         return false;
     }
@@ -109,10 +123,13 @@ public class UserItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
         return new UserItemViewHolder(view, adapter);
     }
 
+    @SuppressLint("SetTextI18n")
     @Override
     public void bindViewHolder(FlexibleAdapter adapter, UserItemViewHolder holder, int position, List payloads) {
 
-        holder.simpleDraweeView.setController(null);
+        if (holder.participantAvatar != null) {
+            holder.participantAvatar.setController(null);
+        }
 
         if (holder.checkedImageView != null) {
             if (participant.isSelected()) {
@@ -122,69 +139,71 @@ public class UserItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
             }
         }
 
+        drawStatus(holder);
+
         if (!isOnline) {
             holder.contactDisplayName.setTextColor(ResourcesCompat.getColor(
-                    holder.contactDisplayName.getContext().getResources(),
-                    R.color.medium_emphasis_text,
-                    null)
-            );
-            holder.simpleDraweeView.setAlpha(0.38f);
+                holder.contactDisplayName.getContext().getResources(),
+                R.color.medium_emphasis_text,
+                null)
+                                                  );
+            holder.participantAvatar.setAlpha(0.38f);
         } else {
             holder.contactDisplayName.setTextColor(ResourcesCompat.getColor(
-                    holder.contactDisplayName.getContext().getResources(),
-                    R.color.high_emphasis_text,
-                    null)
-            );
-            holder.simpleDraweeView.setAlpha(1.0f);
+                holder.contactDisplayName.getContext().getResources(),
+                R.color.high_emphasis_text,
+                null)
+                                                  );
+            holder.participantAvatar.setAlpha(1.0f);
         }
 
         if (adapter.hasFilter()) {
             FlexibleUtils.highlightText(holder.contactDisplayName, participant.getDisplayName(),
-                    String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.Companion.getSharedApplication()
-                            .getResources().getColor(R.color.colorPrimary));
+                                        String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.Companion.getSharedApplication()
+                                            .getResources().getColor(R.color.colorPrimary));
         }
 
         holder.contactDisplayName.setText(participant.getDisplayName());
 
         if (TextUtils.isEmpty(participant.getDisplayName()) &&
-                (participant.getType().equals(Participant.ParticipantType.GUEST) || participant.getType().equals(Participant.ParticipantType.USER_FOLLOWING_LINK))) {
+            (participant.getType().equals(Participant.ParticipantType.GUEST) || participant.getType().equals(Participant.ParticipantType.USER_FOLLOWING_LINK))) {
             holder.contactDisplayName.setText(NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest));
         }
 
         if (participant.getActorType() == Participant.ActorType.GROUPS ||
-                "groups".equals(participant.getSource()) ||
-                participant.getActorType() == Participant.ActorType.CIRCLES ||
-                "circles".equals(participant.getSource())) {
-            holder.simpleDraweeView.setImageResource(R.drawable.ic_circular_group);
+            "groups".equals(participant.getSource()) ||
+            participant.getActorType() == Participant.ActorType.CIRCLES ||
+            "circles".equals(participant.getSource())) {
+            holder.participantAvatar.setImageResource(R.drawable.ic_circular_group);
         } else if (participant.getActorType() == Participant.ActorType.EMAILS) {
-            holder.simpleDraweeView.setImageResource(R.drawable.ic_circular_mail);
+            holder.participantAvatar.setImageResource(R.drawable.ic_circular_mail);
         } else if (participant.getActorType() == Participant.ActorType.GUESTS ||
-                Participant.ParticipantType.GUEST.equals(participant.getType()) ||
-                Participant.ParticipantType.GUEST_MODERATOR.equals(participant.getType())) {
+            Participant.ParticipantType.GUEST.equals(participant.getType()) ||
+            Participant.ParticipantType.GUEST_MODERATOR.equals(participant.getType())) {
 
             String displayName = NextcloudTalkApplication.Companion.getSharedApplication()
-                    .getResources().getString(R.string.nc_guest);
+                .getResources().getString(R.string.nc_guest);
 
             if (!TextUtils.isEmpty(participant.getDisplayName())) {
                 displayName = participant.getDisplayName();
             }
 
             DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                    .setOldController(holder.simpleDraweeView.getController())
-                    .setAutoPlayAnimations(true)
-                    .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithNameForGuests(userEntity.getBaseUrl(),
-                                                                                                                  displayName, R.dimen.avatar_size), null))
-                    .build();
-            holder.simpleDraweeView.setController(draweeController);
+                .setOldController(holder.participantAvatar.getController())
+                .setAutoPlayAnimations(true)
+                .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithNameForGuests(userEntity.getBaseUrl(),
+                                                                                                              displayName, R.dimen.avatar_size), null))
+                .build();
+            holder.participantAvatar.setController(draweeController);
 
         } else if (participant.getActorType() == Participant.ActorType.USERS || participant.getSource().equals("users")) {
             DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                    .setOldController(holder.simpleDraweeView.getController())
-                    .setAutoPlayAnimations(true)
-                    .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userEntity.getBaseUrl(),
-                                                                                                         participant.getActorId(), R.dimen.avatar_size), null))
-                    .build();
-            holder.simpleDraweeView.setController(draweeController);
+                .setOldController(holder.participantAvatar.getController())
+                .setAutoPlayAnimations(true)
+                .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userEntity.getBaseUrl(),
+                                                                                                     participant.getActorId(), R.dimen.avatar_size), null))
+                .build();
+            holder.participantAvatar.setController(draweeController);
         }
 
         Resources resources = NextcloudTalkApplication.Companion.getSharedApplication().getResources();
@@ -195,17 +214,17 @@ public class UserItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
                 holder.videoCallIconView.setImageResource(R.drawable.ic_call_grey_600_24dp);
                 holder.videoCallIconView.setVisibility(View.VISIBLE);
                 holder.videoCallIconView.setContentDescription(
-                        resources.getString(R.string.nc_call_state_with_phone, participant.displayName));
+                    resources.getString(R.string.nc_call_state_with_phone, participant.displayName));
             } else if ((inCallFlag & InCallFlags.WITH_VIDEO) > 0) {
                 holder.videoCallIconView.setImageResource(R.drawable.ic_videocam_grey_600_24dp);
                 holder.videoCallIconView.setVisibility(View.VISIBLE);
                 holder.videoCallIconView.setContentDescription(
-                        resources.getString(R.string.nc_call_state_with_video, participant.displayName));
+                    resources.getString(R.string.nc_call_state_with_video, participant.displayName));
             } else if (inCallFlag > InCallFlags.DISCONNECTED) {
                 holder.videoCallIconView.setImageResource(R.drawable.ic_mic_grey_600_24dp);
                 holder.videoCallIconView.setVisibility(View.VISIBLE);
                 holder.videoCallIconView.setContentDescription(
-                        resources.getString(R.string.nc_call_state_in_call, participant.displayName));
+                    resources.getString(R.string.nc_call_state_in_call, participant.displayName));
             } else {
                 holder.videoCallIconView.setVisibility(View.GONE);
             }
@@ -243,18 +262,61 @@ public class UserItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
                         break;
                 }
 
-                if (!holder.contactMentionId.getText().equals(userType)) {
-                    holder.contactMentionId.setText(userType);
+                if (!userType.equals(NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_user))) {
+                    holder.contactMentionId.setText("(" + userType + ")");
+                }
+            }
+        }
+    }
+
+    private void drawStatus(UserItemViewHolder holder) {
+        if (holder.statusMessage != null && holder.participantEmoji != null && holder.userStatusImage != null) {
+            float size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, context);
+            holder.userStatusImage.setImageDrawable(new StatusDrawable(
+                participant.status,
+                NO_ICON,
+                size,
+                context.getResources().getColor(R.color.bg_default),
+                context));
+
+            if (participant.statusMessage != null) {
+                holder.statusMessage.setText(participant.statusMessage);
+                alignUsernameVertical(holder, 0);
+            } else {
+                holder.statusMessage.setText("");
+                alignUsernameVertical(holder, 10);
+            }
+
+            if (participant.statusIcon != null && !participant.statusIcon.isEmpty()) {
+                holder.participantEmoji.setText(participant.statusIcon);
+            } else {
+                holder.participantEmoji.setVisibility(View.GONE);
+            }
+
+            if (participant.status != null && participant.status.equals(StatusType.DND.getString())) {
+                if (participant.statusMessage == null || participant.statusMessage.isEmpty()) {
+                    holder.statusMessage.setText(R.string.dnd);
+                }
+            } else if (participant.status != null && participant.status.equals(StatusType.AWAY.getString())) {
+                if (participant.statusMessage == null || participant.statusMessage.isEmpty()) {
+                    holder.statusMessage.setText(R.string.away);
                 }
             }
         }
     }
 
+    private void alignUsernameVertical(UserItem.UserItemViewHolder holder, float densityPixelsFromTop) {
+        ConstraintLayout.LayoutParams layoutParams =
+            (ConstraintLayout.LayoutParams) holder.contactDisplayName.getLayoutParams();
+        layoutParams.topMargin = (int) DisplayUtils.convertDpToPixel(densityPixelsFromTop, context);
+        holder.contactDisplayName.setLayoutParams(layoutParams);
+    }
+
     @Override
     public boolean filter(String constraint) {
         return participant.getDisplayName() != null &&
-                (Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(participant.getDisplayName().trim()).find() ||
-                        Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(participant.getActorId().trim()).find());
+            (Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(participant.getDisplayName().trim()).find() ||
+                Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(participant.getActorId().trim()).find());
     }
 
     @Override
@@ -271,8 +333,9 @@ public class UserItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
 
         @BindView(R.id.name_text)
         public EmojiTextView contactDisplayName;
-        @BindView(R.id.simple_drawee_view)
-        public SimpleDraweeView simpleDraweeView;
+        @Nullable
+        @BindView(R.id.avatar_drawee_view)
+        public SimpleDraweeView participantAvatar;
         @Nullable
         @BindView(R.id.secondary_text)
         public EmojiTextView contactMentionId;
@@ -282,6 +345,15 @@ public class UserItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
         @Nullable
         @BindView(R.id.checkedImageView)
         ImageView checkedImageView;
+        @Nullable
+        @BindView(R.id.participant_status_emoji)
+        com.vanniktech.emoji.EmojiEditText participantEmoji;
+        @Nullable
+        @BindView(R.id.user_status_image)
+        ImageView userStatusImage;
+        @Nullable
+        @BindView(R.id.conversation_info_status_message)
+        EmojiTextView statusMessage;
 
         /**
          * Default constructor.

+ 44 - 2
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -40,6 +40,8 @@ import com.nextcloud.talk.models.json.push.PushRegistrationOverall;
 import com.nextcloud.talk.models.json.search.ContactsByNumberOverall;
 import com.nextcloud.talk.models.json.signaling.SignalingOverall;
 import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
+import com.nextcloud.talk.models.json.status.StatusOverall;
+import com.nextcloud.talk.models.json.statuses.StatusesOverall;
 import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall;
 import com.nextcloud.talk.models.json.userprofile.UserProfileOverall;
 
@@ -185,7 +187,8 @@ public interface NcApi {
         Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken
     */
     @GET
-    Observable<ParticipantsOverall> getPeersForCall(@Header("Authorization") String authorization, @Url String url);
+    Observable<ParticipantsOverall> getPeersForCall(@Header("Authorization") String authorization, @Url String url,
+                                                   @QueryMap Map<String, Boolean> fields);
 
     @FormUrlEncoded
     @POST
@@ -333,7 +336,8 @@ public interface NcApi {
     @GET
     Observable<MentionOverall> getMentionAutocompleteSuggestions(@Header("Authorization") String authorization,
                                                                  @Url String url, @Query("search") String query,
-                                                                 @Nullable @Query("limit") Integer limit);
+                                                                 @Nullable @Query("limit") Integer limit,
+                                                                 @QueryMap Map<String, String> fields);
 
     // Url is: /api/{apiVersion}/room/{token}/pin
     @POST
@@ -443,4 +447,42 @@ public interface NcApi {
     @GET
     Observable<RoomsOverall> getOpenConversations(@Header("Authorization") String authorization, @Url String url);
 
+
+    /*
+     * OCS Status API
+     */
+    @GET
+    Observable<StatusOverall> status(@Header("Authorization") String authorization, @Url String url);
+
+    @GET
+    Observable<ResponseBody> getPredefinedStatuses(@Header("Authorization") String authorization, @Url String url);
+
+    @DELETE
+    Observable<GenericOverall> statusDeleteMessage(@Header("Authorization") String authorization, @Url String url);
+
+
+    @FormUrlEncoded
+    @PUT
+    Observable<GenericOverall> setPredefinedStatusMessage(@Header("Authorization") String authorization,
+                                      @Url String url,
+                                      @Field("messageId") String selectedPredefinedMessageId,
+                                      @Field("clearAt") Long clearAt);
+
+    @FormUrlEncoded
+    @PUT
+    Observable<GenericOverall> setCustomStatusMessage(@Header("Authorization") String authorization,
+                                  @Url String url,
+                                  @Field("statusIcon") String statusIcon,
+                                  @Field("message") String message,
+                                  @Field("clearAt") Long clearAt);
+
+    @FormUrlEncoded
+    @PUT
+    Observable<GenericOverall> setStatusType(@Header("Authorization") String authorization,
+                                                      @Url String url,
+                                                      @Field("statusType") String statusType);
+
+    @GET
+    Observable<StatusesOverall> getUserStatuses(@Header("Authorization") String authorization, @Url String url);
+
 }

+ 2 - 2
app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt

@@ -66,7 +66,7 @@ import com.nextcloud.talk.utils.database.user.UserModule
 import com.nextcloud.talk.utils.preferences.AppPreferences
 import com.nextcloud.talk.webrtc.MagicWebRTCUtils
 import com.vanniktech.emoji.EmojiManager
-import com.vanniktech.emoji.googlecompat.GoogleCompatEmojiProvider
+import com.vanniktech.emoji.google.GoogleEmojiProvider
 import de.cotech.hw.SecurityKeyManager
 import de.cotech.hw.SecurityKeyManagerConfig
 import okhttp3.OkHttpClient
@@ -188,7 +188,7 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
         config.setReplaceAll(true)
         val emojiCompat = EmojiCompat.init(config)
 
-        EmojiManager.install(GoogleCompatEmojiProvider(emojiCompat))
+        EmojiManager.install(GoogleEmojiProvider())
 
         NotificationUtils.registerNotificationChannels(applicationContext, appPreferences)
     }

+ 42 - 52
app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.java

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -24,20 +26,14 @@ import android.content.Context;
 import android.text.format.Formatter;
 import android.view.View;
 import android.widget.CheckBox;
-import android.widget.ImageView;
-import android.widget.TextView;
 import android.widget.Toast;
 
-import androidx.appcompat.content.res.AppCompatResources;
-import autodagger.AutoInjector;
-import butterknife.BindView;
-import butterknife.ButterKnife;
 import com.facebook.drawee.backends.pipeline.Fresco;
 import com.facebook.drawee.interfaces.DraweeController;
-import com.facebook.drawee.view.SimpleDraweeView;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.components.filebrowser.models.BrowserFile;
+import com.nextcloud.talk.databinding.RvItemBrowserFileBinding;
 import com.nextcloud.talk.interfaces.SelectionInterface;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.utils.ApiUtils;
@@ -49,9 +45,8 @@ import java.util.List;
 
 import javax.inject.Inject;
 
+import androidx.appcompat.content.res.AppCompatResources;
 import autodagger.AutoInjector;
-import butterknife.BindView;
-import butterknife.ButterKnife;
 import eu.davidea.flexibleadapter.FlexibleAdapter;
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
 import eu.davidea.flexibleadapter.items.IFilterable;
@@ -59,12 +54,12 @@ import eu.davidea.flexibleadapter.items.IFlexible;
 import eu.davidea.viewholders.FlexibleViewHolder;
 
 @AutoInjector(NextcloudTalkApplication.class)
-public class BrowserFileItem extends AbstractFlexibleItem<BrowserFileItem.ViewHolder> implements IFilterable<String> {
+public class BrowserFileItem extends AbstractFlexibleItem<BrowserFileItem.BrowserFileItemViewHolder> implements IFilterable<String> {
     @Inject
     Context context;
-    private BrowserFile browserFile;
-    private UserEntity activeUser;
-    private SelectionInterface selectionInterface;
+    private final BrowserFile browserFile;
+    private final UserEntity activeUser;
+    private final SelectionInterface selectionInterface;
     private boolean selected;
 
     public BrowserFileItem(BrowserFile browserFile, UserEntity activeUser, SelectionInterface selectionInterface) {
@@ -94,9 +89,8 @@ public class BrowserFileItem extends AbstractFlexibleItem<BrowserFileItem.ViewHo
     }
 
     @Override
-    public ViewHolder createViewHolder(View view, FlexibleAdapter<IFlexible> adapter) {
-        return new ViewHolder(view, adapter);
-
+    public BrowserFileItemViewHolder createViewHolder(View view, FlexibleAdapter<IFlexible> adapter) {
+        return new BrowserFileItemViewHolder(view, adapter);
     }
 
     private boolean isSelected() {
@@ -108,8 +102,11 @@ public class BrowserFileItem extends AbstractFlexibleItem<BrowserFileItem.ViewHo
     }
 
     @Override
-    public void bindViewHolder(FlexibleAdapter<IFlexible> adapter, ViewHolder holder, int position, List<Object> payloads) {
-        holder.fileIconImageView.setController(null);
+    public void bindViewHolder(FlexibleAdapter<IFlexible> adapter,
+                               BrowserFileItemViewHolder holder,
+                               int position,
+                               List<Object> payloads) {
+        holder.binding.fileIcon.setController(null);
         if (!browserFile.isAllowedToReShare() || browserFile.isEncrypted()) {
             holder.itemView.setEnabled(false);
             holder.itemView.setAlpha(0.38f);
@@ -119,31 +116,32 @@ public class BrowserFileItem extends AbstractFlexibleItem<BrowserFileItem.ViewHo
         }
 
         if (browserFile.isEncrypted()) {
-            holder.fileEncryptedImageView.setVisibility(View.VISIBLE);
+            holder.binding.fileEncryptedImageView.setVisibility(View.VISIBLE);
 
         } else {
-            holder.fileEncryptedImageView.setVisibility(View.GONE);
+            holder.binding.fileEncryptedImageView.setVisibility(View.GONE);
         }
 
         if (browserFile.isFavorite()) {
-            holder.fileFavoriteImageView.setVisibility(View.VISIBLE);
+            holder.binding.fileFavoriteImageView.setVisibility(View.VISIBLE);
         } else {
-            holder.fileFavoriteImageView.setVisibility(View.GONE);
+            holder.binding.fileFavoriteImageView.setVisibility(View.GONE);
         }
 
         if (selectionInterface.shouldOnlySelectOneImageFile()) {
             if (browserFile.isFile && browserFile.mimeType.startsWith("image/")) {
-                holder.selectFileCheckbox.setVisibility(View.VISIBLE);
+                holder.binding.selectFileCheckbox.setVisibility(View.VISIBLE);
             } else {
-                holder.selectFileCheckbox.setVisibility(View.GONE);
+                holder.binding.selectFileCheckbox.setVisibility(View.GONE);
             }
         } else {
-            holder.selectFileCheckbox.setVisibility(View.VISIBLE);
+            holder.binding.selectFileCheckbox.setVisibility(View.VISIBLE);
         }
 
         if (context != null) {
             holder
-                .fileIconImageView
+                .binding
+                .fileIcon
                 .getHierarchy()
                 .setPlaceholderImage(
                     AppCompatResources.getDrawable(
@@ -160,25 +158,28 @@ public class BrowserFileItem extends AbstractFlexibleItem<BrowserFileItem.ViewHo
                         .setAutoPlayAnimations(true)
                         .setImageRequest(DisplayUtils.getImageRequestForUrl(path, null))
                         .build();
-                holder.fileIconImageView.setController(draweeController);
+                holder.binding.fileIcon.setController(draweeController);
             }
         }
 
-        holder.filenameTextView.setText(browserFile.getDisplayName());
-        holder.fileModifiedTextView.setText(String.format(context.getString(R.string.nc_last_modified),
+        holder.binding.filenameTextView.setText(browserFile.getDisplayName());
+        holder.binding.fileModifiedInfo.setText(String.format(context.getString(R.string.nc_last_modified),
                 Formatter.formatShortFileSize(context, browserFile.getSize()),
                 DateUtils.INSTANCE.getLocalDateTimeStringFromTimestamp(browserFile.getModifiedTimestamp())));
         setSelected(selectionInterface.isPathSelected(browserFile.getPath()));
-        holder.selectFileCheckbox.setChecked(isSelected());
+        holder.binding.selectFileCheckbox.setChecked(isSelected());
 
         if (!browserFile.isEncrypted()) {
-            holder.selectFileCheckbox.setOnClickListener(new View.OnClickListener() {
+            holder.binding.selectFileCheckbox.setOnClickListener(new View.OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     if (!browserFile.isAllowedToReShare()) {
                         ((CheckBox) v).setChecked(false);
-                        Toast.makeText(context, context.getResources().getString(R.string.nc_file_browser_reshare_forbidden),
-                                Toast.LENGTH_LONG).show();
+                        Toast.makeText(
+                            context,
+                            context.getResources().getString(R.string.nc_file_browser_reshare_forbidden),
+                            Toast.LENGTH_LONG)
+                            .show();
                     } else if (((CheckBox) v).isChecked() != isSelected()) {
                         setSelected(((CheckBox) v).isChecked());
                         selectionInterface.toggleBrowserItemSelection(browserFile.getPath());
@@ -187,8 +188,8 @@ public class BrowserFileItem extends AbstractFlexibleItem<BrowserFileItem.ViewHo
             });
         }
 
-        holder.filenameTextView.setSelected(true);
-        holder.fileModifiedTextView.setSelected(true);
+        holder.binding.filenameTextView.setSelected(true);
+        holder.binding.fileModifiedInfo.setSelected(true);
     }
 
     @Override
@@ -196,24 +197,13 @@ public class BrowserFileItem extends AbstractFlexibleItem<BrowserFileItem.ViewHo
         return false;
     }
 
-    static class ViewHolder extends FlexibleViewHolder {
-
-        @BindView(R.id.file_icon)
-        public SimpleDraweeView fileIconImageView;
-        @BindView(R.id.file_modified_info)
-        public TextView fileModifiedTextView;
-        @BindView(R.id.filename_text_view)
-        public TextView filenameTextView;
-        @BindView(R.id.select_file_checkbox)
-        public CheckBox selectFileCheckbox;
-        @BindView(R.id.fileEncryptedImageView)
-        public ImageView fileEncryptedImageView;
-        @BindView(R.id.fileFavoriteImageView)
-        public ImageView fileFavoriteImageView;
-
-        ViewHolder(View view, FlexibleAdapter adapter) {
+    static class BrowserFileItemViewHolder extends FlexibleViewHolder {
+
+        RvItemBrowserFileBinding binding;
+
+        BrowserFileItemViewHolder(View view, FlexibleAdapter adapter) {
             super(view, adapter);
-            ButterKnife.bind(this, view);
+            binding = RvItemBrowserFileBinding.bind(view);
         }
     }
 }

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

@@ -551,6 +551,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ
                                     }
 
                                     UserItem newContactItem = new UserItem(
+                                        getApplicationContext(),
                                         participant,
                                         currentUser,
                                         userHeaderItems.get(headerTitle)

+ 16 - 10
app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt

@@ -4,6 +4,8 @@
  * @author Mario Danic
  * @author Andy Scherzinger
  * @author Tim Krüger
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
  * 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>
@@ -88,6 +90,7 @@ import org.greenrobot.eventbus.ThreadMode
 import java.util.Calendar
 import java.util.Collections
 import java.util.Comparator
+import java.util.HashMap
 import java.util.Locale
 import javax.inject.Inject
 
@@ -120,7 +123,7 @@ class ConversationInfoController(args: Bundle) :
     private var conversation: Conversation? = null
 
     private var adapter: FlexibleAdapter<UserItem>? = null
-    private var recyclerViewItems: MutableList<UserItem> = ArrayList()
+    private var userItems: MutableList<UserItem> = ArrayList()
 
     private var saveStateHandler: LovelySaveStateHandler? = null
 
@@ -362,7 +365,7 @@ class ConversationInfoController(args: Bundle) :
     private fun setupAdapter() {
         if (activity != null) {
             if (adapter == null) {
-                adapter = FlexibleAdapter(recyclerViewItems, activity, true)
+                adapter = FlexibleAdapter(userItems, activity, true)
             }
 
             val layoutManager = SmoothScrollLinearLayoutManager(activity)
@@ -378,12 +381,12 @@ class ConversationInfoController(args: Bundle) :
         var userItem: UserItem
         var participant: Participant
 
-        recyclerViewItems = ArrayList()
+        userItems = ArrayList()
         var ownUserItem: UserItem? = null
 
         for (i in participants.indices) {
             participant = participants[i]
-            userItem = UserItem(participant, conversationUser, null)
+            userItem = UserItem(router.activity, participant, conversationUser, null)
             if (participant.sessionId != null) {
                 userItem.isOnline = !participant.sessionId.equals("0")
             } else {
@@ -395,20 +398,20 @@ class ConversationInfoController(args: Bundle) :
                 ownUserItem.model.sessionId = "-1"
                 ownUserItem.isOnline = true
             } else {
-                recyclerViewItems.add(userItem)
+                userItems.add(userItem)
             }
         }
 
-        Collections.sort(recyclerViewItems, UserItemComparator())
+        Collections.sort(userItems, UserItemComparator())
 
         if (ownUserItem != null) {
-            recyclerViewItems.add(0, ownUserItem)
+            userItems.add(0, ownUserItem)
         }
 
         setupAdapter()
 
         binding.participantsListCategory.visibility = View.VISIBLE
-        adapter!!.updateDataSet(recyclerViewItems)
+        adapter!!.updateDataSet(userItems)
     }
 
     override val title: String
@@ -426,9 +429,12 @@ class ConversationInfoController(args: Bundle) :
             apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
         }
 
+        val fieldMap = HashMap<String, Boolean>()
+        fieldMap["includeStatus"] = true
+
         ncApi?.getPeersForCall(
             credentials,
-            ApiUtils.getUrlForParticipants(apiVersion, conversationUser!!.baseUrl, conversationToken)
+            ApiUtils.getUrlForParticipants(apiVersion, conversationUser!!.baseUrl, conversationToken), fieldMap
         )
             ?.subscribeOn(Schedulers.io())
             ?.observeOn(AndroidSchedulers.mainThread())
@@ -462,7 +468,7 @@ class ConversationInfoController(args: Bundle) :
         val bundle = Bundle()
         val existingParticipantsId = arrayListOf<String>()
 
-        for (userItem in recyclerViewItems) {
+        for (userItem in userItems) {
             if (userItem.model.getActorType() == USERS) {
                 existingParticipantsId.add(userItem.model.getActorId())
             }

+ 42 - 9
app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java

@@ -64,7 +64,6 @@ import com.facebook.imagepipeline.image.CloseableImage;
 import com.facebook.imagepipeline.request.ImageRequest;
 import com.google.android.material.button.MaterialButton;
 import com.google.android.material.floatingactionbutton.FloatingActionButton;
-
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.activities.MainActivity;
 import com.nextcloud.talk.adapters.items.ConversationItem;
@@ -72,8 +71,6 @@ import com.nextcloud.talk.adapters.items.GenericTextHeaderItem;
 import com.nextcloud.talk.api.NcApi;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.controllers.base.BaseController;
-import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum;
-import com.nextcloud.talk.controllers.bottomsheet.EntryMenuController;
 import com.nextcloud.talk.events.ConversationsListFetchDataEvent;
 import com.nextcloud.talk.events.EventStatus;
 import com.nextcloud.talk.interfaces.ConversationMenuInterface;
@@ -84,7 +81,8 @@ import com.nextcloud.talk.jobs.UploadAndShareFilesWorker;
 import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.conversations.Conversation;
-import com.nextcloud.talk.models.json.participants.Participant;
+import com.nextcloud.talk.models.json.status.Status;
+import com.nextcloud.talk.models.json.statuses.StatusesOverall;
 import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment;
 import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog;
 import com.nextcloud.talk.utils.ApiUtils;
@@ -132,6 +130,7 @@ import butterknife.BindView;
 import eu.davidea.flexibleadapter.FlexibleAdapter;
 import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager;
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
+import io.reactivex.Observer;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.Disposable;
 import io.reactivex.schedulers.Schedulers;
@@ -191,8 +190,6 @@ public class ConversationsListController extends BaseController implements Searc
     private SearchView searchView;
     private String searchQuery;
 
-    private View view;
-
     private String credentials;
 
     private boolean adapterWasNull = true;
@@ -220,6 +217,8 @@ public class ConversationsListController extends BaseController implements Searc
 
     private ConversationsListBottomDialog conversationsListBottomDialog;
 
+    private HashMap<String, Status> userStatuses = new HashMap<>();
+
     public ConversationsListController(Bundle bundle) {
         super();
         setHasOptionsMenu(true);
@@ -473,6 +472,37 @@ public class ConversationsListController extends BaseController implements Searc
 
     @SuppressLint("LongLogTag")
     public void fetchData() {
+        fetchUserStatuses();
+    }
+
+    private void fetchUserStatuses() {
+        ncApi.getUserStatuses(credentials, ApiUtils.getUrlForUserStatuses(currentUser.getBaseUrl()))
+            .subscribe(new Observer<StatusesOverall>() {
+                @Override
+                public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
+                }
+
+                @Override
+                public void onNext(@NonNull StatusesOverall statusesOverall) {
+                    for (Status status : statusesOverall.getOcs().getData()) {
+                        userStatuses.put(status.getUserId(), status);
+                    }
+                    fetchRooms();
+                }
+
+                @Override
+                public void onError(@io.reactivex.annotations.NonNull Throwable e) {
+                    Log.e(TAG, "failed to fetch user statuses", e);
+                }
+
+                @Override
+                public void onComplete() {
+                }
+            });
+
+    }
+
+    private void fetchRooms() {
         dispose(null);
 
         isRefreshing = true;
@@ -531,14 +561,16 @@ public class ConversationsListController extends BaseController implements Searc
                             ConversationItem conversationItem = new ConversationItem(
                                 conversation,
                                 currentUser,
-                                getActivity());
+                                getActivity(),
+                                userStatuses.get(conversation.name));
                             conversationItems.add(conversationItem);
 
                             ConversationItem conversationItemWithHeader = new ConversationItem(
                                 conversation,
                                 currentUser,
                                 getActivity(),
-                                callHeaderItems.get(headerTitle));
+                                callHeaderItems.get(headerTitle),
+                                userStatuses.get(conversation.name));
                             conversationItemsWithHeader.add(conversationItemWithHeader);
                         }
                     }
@@ -610,7 +642,8 @@ public class ConversationsListController extends BaseController implements Searc
                             conversation,
                             currentUser,
                             getActivity(),
-                            callHeaderItems.get(headerTitle));
+                            callHeaderItems.get(headerTitle),
+                            userStatuses.get(conversation.name));
 
                         openConversationItems.add(conversationItem);
                     }

+ 17 - 1
app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java

@@ -56,7 +56,7 @@ public abstract class CapabilitiesUtil {
                 Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
                 if (capabilities.getExternalCapability() != null &&
                         capabilities.getExternalCapability().containsKey("v1")) {
-                    return capabilities.getExternalCapability().get("v1").contains("capabilityName");
+                    return capabilities.getExternalCapability().get("v1").contains(capabilityName);
                 }
             } catch (IOException e) {
                 Log.e(TAG, "Failed to get capabilities for the user");
@@ -175,6 +175,22 @@ public abstract class CapabilitiesUtil {
         return false;
     }
 
+    public static boolean isUserStatusAvailable(@Nullable UserEntity user) {
+        if (user != null && user.getCapabilities() != null) {
+            try {
+                Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                if (capabilities.getUserStatusCapability() != null &&
+                    capabilities.getUserStatusCapability().getEnabled() &&
+                    capabilities.getUserStatusCapability().getSupportsEmoji()) {
+                    return true;
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to get capabilities for the user");
+            }
+        }
+        return false;
+    }
+
     public static String getAttachmentFolder(@Nullable UserEntity user) {
         if (user != null && user.getCapabilities() != null) {
             try {

+ 4 - 2
app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt

@@ -38,8 +38,10 @@ data class Capabilities(
     @JsonField(name = ["external"])
     var externalCapability: HashMap<String, List<String>>?,
     @JsonField(name = ["provisioning_api"])
-    var provisioningCapability: ProvisioningCapability?
+    var provisioningCapability: ProvisioningCapability?,
+    @JsonField(name = ["user_status"])
+    var userStatusCapability: UserStatusCapability?
 ) : Parcelable {
     // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
-    constructor() : this(null, null, null, null, null)
+    constructor() : this(null, null, null, null, null, null)
 }

+ 39 - 0
app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt

@@ -0,0 +1,39 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.capabilities
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class UserStatusCapability(
+    @JsonField(name = ["enabled"])
+    var enabled: Boolean,
+    @JsonField(name = ["supports_emoji"])
+    var supportsEmoji: Boolean
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(false, false)
+}

+ 0 - 110
app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.java

@@ -1,110 +0,0 @@
-/*
- * 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/>.
- */
-package com.nextcloud.talk.models.json.mention;
-
-import com.bluelinelabs.logansquare.annotation.JsonField;
-import com.bluelinelabs.logansquare.annotation.JsonObject;
-
-import org.parceler.Parcel;
-
-@Parcel
-@JsonObject
-public class Mention {
-    @JsonField(name = "id")
-    String id;
-
-    @JsonField(name = "label")
-    String label;
-
-    // type of user (guests or users or calls)
-    @JsonField(name = "source")
-    String source;
-
-    public String getId() {
-        return this.id;
-    }
-
-    public String getLabel() {
-        return this.label;
-    }
-
-    public String getSource() {
-        return this.source;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public void setLabel(String label) {
-        this.label = label;
-    }
-
-    public void setSource(String source) {
-        this.source = source;
-    }
-
-    public boolean equals(final Object o) {
-        if (o == this) {
-            return true;
-        }
-        if (!(o instanceof Mention)) {
-            return false;
-        }
-        final Mention other = (Mention) o;
-        if (!other.canEqual((Object) this)) {
-            return false;
-        }
-        final Object this$id = this.getId();
-        final Object other$id = other.getId();
-        if (this$id == null ? other$id != null : !this$id.equals(other$id)) {
-            return false;
-        }
-        final Object this$label = this.getLabel();
-        final Object other$label = other.getLabel();
-        if (this$label == null ? other$label != null : !this$label.equals(other$label)) {
-            return false;
-        }
-        final Object this$source = this.getSource();
-        final Object other$source = other.getSource();
-
-        return this$source == null ? other$source == null : this$source.equals(other$source);
-    }
-
-    protected boolean canEqual(final Object other) {
-        return other instanceof Mention;
-    }
-
-    public int hashCode() {
-        final int PRIME = 59;
-        int result = 1;
-        final Object $id = this.getId();
-        result = result * PRIME + ($id == null ? 43 : $id.hashCode());
-        final Object $label = this.getLabel();
-        result = result * PRIME + ($label == null ? 43 : $label.hashCode());
-        final Object $source = this.getSource();
-        result = result * PRIME + ($source == null ? 43 : $source.hashCode());
-        return result;
-    }
-
-    public String toString() {
-        return "Mention(id=" + this.getId() + ", label=" + this.getLabel() + ", source=" + this.getSource() + ")";
-    }
-}

+ 52 - 0
app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.kt

@@ -0,0 +1,52 @@
+/*
+ * 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/>.
+ */
+package com.nextcloud.talk.models.json.mention
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class Mention(
+    @JsonField(name = ["id"])
+    var id: String,
+
+    @JsonField(name = ["label"])
+    var label: String,
+
+    // type of user (guests or users or calls)
+    @JsonField(name = ["source"])
+    var source: String,
+
+    @JsonField(name = ["status"])
+    var status: String?,
+
+    @JsonField(name = ["statusIcon"])
+    var statusIcon: String?,
+
+    @JsonField(name = ["statusMessage"])
+    var statusMessage: String?
+
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this("", "", "", "", "", "")
+}

+ 9 - 0
app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.java

@@ -78,6 +78,15 @@ public class Participant {
     @JsonField(name = "inCall")
     public Object inCall;
 
+    @JsonField(name = "status")
+    public String status;
+
+    @JsonField(name = "statusIcon")
+    public String statusIcon;
+
+    @JsonField(name = "statusMessage")
+    public String statusMessage;
+
     public String source;
 
     public boolean selected;

+ 18 - 0
app/src/main/java/com/nextcloud/talk/models/json/status/ClearAt.kt

@@ -0,0 +1,18 @@
+package com.nextcloud.talk.models.json.status
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class ClearAt(
+    @JsonField(name = ["type"])
+    var type: String,
+    @JsonField(name = ["time"])
+    var time: String
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this("type", "time")
+}

+ 52 - 0
app/src/main/java/com/nextcloud/talk/models/json/status/Status.kt

@@ -0,0 +1,52 @@
+/*
+ *
+ *   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.status
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class Status(
+    @JsonField(name = ["userId"])
+    var userId: String?,
+    @JsonField(name = ["message"])
+    var message: String?,
+    /* TODO: Change to enum */
+    @JsonField(name = ["messageId"])
+    var messageId: String?,
+    @JsonField(name = ["messageIsPredefined"])
+    var messageIsPredefined: Boolean,
+    @JsonField(name = ["icon"])
+    var icon: String?,
+    @JsonField(name = ["clearAt"])
+    var clearAt: Long = 0,
+    /* TODO: Change to enum */
+    @JsonField(name = ["status"])
+    var status: String = "offline",
+    @JsonField(name = ["statusIsUserDefined"])
+    var statusIsUserDefined: Boolean
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, null, null, false, null, 0, "offline", false)
+}

+ 69 - 0
app/src/main/java/com/nextcloud/talk/models/json/status/StatusOCS.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.status;
+
+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 StatusOCS extends GenericOCS {
+    @JsonField(name = "data")
+    public Status data;
+
+    public Status getData() {
+        return this.data;
+    }
+
+    public void setData(Status 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;
+        }
+        StatusOCS that = (StatusOCS) o;
+        return Objects.equals(data, that.data);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), data);
+    }
+
+    @Override
+    public String toString() {
+        return "StatusOCS{" +
+            "data=" + data +
+            '}';
+    }
+
+}

+ 64 - 0
app/src/main/java/com/nextcloud/talk/models/json/status/StatusOverall.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.status;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+
+import java.util.Objects;
+
+@JsonObject
+public class StatusOverall {
+    @JsonField(name = "ocs")
+    public StatusOCS ocs;
+
+    public StatusOCS getOcs() {
+        return this.ocs;
+    }
+
+    public void setOcs(StatusOCS ocs) {
+        this.ocs = ocs;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        StatusOverall that = (StatusOverall) o;
+        return Objects.equals(ocs, that.ocs);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(ocs);
+    }
+
+    @Override
+    public String toString() {
+        return "StatusOverall{" +
+            "ocs=" + ocs +
+            '}';
+    }
+}

+ 9 - 0
app/src/main/java/com/nextcloud/talk/models/json/status/StatusType.kt

@@ -0,0 +1,9 @@
+package com.nextcloud.talk.models.json.status
+
+enum class StatusType(val string: String) {
+    ONLINE("online"),
+    OFFLINE("offline"),
+    DND("dnd"),
+    AWAY("away"),
+    INVISIBLE("invisible");
+}

+ 23 - 0
app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatus.kt

@@ -0,0 +1,23 @@
+package com.nextcloud.talk.models.json.status.predefined
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import com.nextcloud.talk.models.json.status.ClearAt
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class PredefinedStatus(
+    @JsonField(name = ["id"])
+    var id: String,
+    @JsonField(name = ["icon"])
+    var icon: String,
+    @JsonField(name = ["message"])
+    var message: String,
+    @JsonField(name = ["clearAt"])
+    var clearAt: ClearAt?
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this("id", "icon", "message", null)
+}

+ 37 - 0
app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOCS.kt

@@ -0,0 +1,37 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.models.json.status.predefined
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import com.nextcloud.talk.models.json.generic.GenericOCS
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class PredefinedStatusOCS(
+    @JsonField(name = ["data"])
+    var data: List<PredefinedStatus>?
+) : GenericOCS(), Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null)
+}

+ 37 - 0
app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOverall.kt

@@ -0,0 +1,37 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.status.predefined
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class PredefinedStatusOverall(
+    @JsonField(name = ["ocs"])
+    var ocs: PredefinedStatusOCS? = null
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null)
+}

+ 71 - 0
app/src/main/java/com/nextcloud/talk/models/json/statuses/StatusesOCS.java

@@ -0,0 +1,71 @@
+/*
+ *
+ *   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.statuses;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+import com.nextcloud.talk.models.json.generic.GenericOCS;
+import com.nextcloud.talk.models.json.status.Status;
+
+import java.util.List;
+import java.util.Objects;
+
+@JsonObject
+public class StatusesOCS extends GenericOCS {
+    @JsonField(name = "data")
+    public List<Status> data;
+
+    public List<Status> getData() {
+        return this.data;
+    }
+
+    public void setData(List<Status> 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;
+        }
+        StatusesOCS that = (StatusesOCS) o;
+        return Objects.equals(data, that.data);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), data);
+    }
+
+    @Override
+    public String toString() {
+        return "StatusesOCS{" +
+            "data=" + data +
+            '}';
+    }
+
+}

+ 64 - 0
app/src/main/java/com/nextcloud/talk/models/json/statuses/StatusesOverall.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.statuses;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+
+import java.util.Objects;
+
+@JsonObject
+public class StatusesOverall {
+    @JsonField(name = "ocs")
+    public StatusesOCS ocs;
+
+    public StatusesOCS getOcs() {
+        return this.ocs;
+    }
+
+    public void setOcs(StatusesOCS ocs) {
+        this.ocs = ocs;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        StatusesOverall that = (StatusesOverall) o;
+        return Objects.equals(ocs, that.ocs);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(ocs);
+    }
+
+    @Override
+    public String toString() {
+        return "StatusesOverall{" +
+            "ocs=" + ocs +
+            '}';
+    }
+}

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

@@ -3,6 +3,8 @@
  *
  * @author Mario Danic
  * @author Andy Scherzinger
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
  * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
@@ -22,8 +24,11 @@
 
 package com.nextcloud.talk.presenters;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
+import android.util.Log;
 import android.view.View;
+import android.view.ViewGroup;
 
 import com.nextcloud.talk.adapters.items.MentionAutocompleteItem;
 import com.nextcloud.talk.api.NcApi;
@@ -38,7 +43,9 @@ import com.otaliastudios.autocomplete.RecyclerViewPresenter;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import javax.inject.Inject;
 
@@ -54,6 +61,7 @@ import io.reactivex.schedulers.Schedulers;
 
 @AutoInjector(NextcloudTalkApplication.class)
 public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention> implements FlexibleAdapter.OnItemClickListener {
+    private static final String TAG = "MentionAutocompletePresenter";
     @Inject
     NcApi ncApi;
     @Inject
@@ -88,6 +96,14 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
         return adapter;
     }
 
+    @Override
+    protected PopupDimensions getPopupDimensions() {
+        PopupDimensions popupDimensions = new PopupDimensions();
+        popupDimensions.width = ViewGroup.LayoutParams.MATCH_PARENT;
+        popupDimensions.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+        return popupDimensions;
+    }
+
     @Override
     protected void onQuery(@Nullable CharSequence query) {
 
@@ -101,10 +117,14 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
         int apiVersion = ApiUtils.getChatApiVersion(currentUser, new int[] {1});
 
         adapter.setFilter(queryString);
+
+        Map<String, String> queryMap = new HashMap<>();
+        queryMap.put("includeStatus", "true");
+
         ncApi.getMentionAutocompleteSuggestions(
                 ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
                 ApiUtils.getUrlForMentionSuggestions(apiVersion, currentUser.getBaseUrl(), roomToken),
-                queryString, 5)
+                queryString, 5, queryMap)
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
                 .retry(3)
@@ -125,9 +145,7 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
                             for (Mention mention : mentionsList) {
                                 internalAbstractFlexibleItemList.add(
                                         new MentionAutocompleteItem(
-                                                mention.getId(),
-                                                mention.getLabel(),
-                                                mention.getSource(),
+                                                mention,
                                                 currentUser,
                                                 context));
                             }
@@ -140,9 +158,11 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
                         }
                     }
 
+                    @SuppressLint("LongLogTag")
                     @Override
                     public void onError(@NotNull Throwable e) {
                         adapter.clear();
+                        Log.e(TAG, "failed to get MentionAutocompleteSuggestions", e);
                     }
 
                     @Override

+ 133 - 0
app/src/main/java/com/nextcloud/talk/ui/StatusDrawable.java

@@ -0,0 +1,133 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * @author Marcel Hibbe
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
+ * Copyright (C) 2020 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.nextcloud.talk.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+
+import com.nextcloud.talk.R;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.core.content.res.ResourcesCompat;
+
+/**
+ * A Drawable object that draws a status
+ */
+public class StatusDrawable extends Drawable {
+    private String text;
+    private @DrawableRes int icon = -1;
+    private Paint textPaint;
+    private int backgroundColor;
+    private final float radius;
+    private Context context;
+
+    public StatusDrawable(String status, String statusIcon, float statusSize, int backgroundColor, Context context) {
+        radius = statusSize;
+        this.backgroundColor = backgroundColor;
+
+
+        if ("dnd".equals(status)) {
+            icon = R.drawable.ic_user_status_dnd;
+            this.context = context;
+        } else if (TextUtils.isEmpty(statusIcon) && status != null) {
+            switch (status) {
+                case "online":
+                    icon = R.drawable.online_status;
+                    this.context = context;
+                    break;
+
+                case "away":
+                    icon = R.drawable.ic_user_status_away;
+                    this.context = context;
+                    break;
+
+                default:
+                    // do not show
+                    break;
+            }
+        } else {
+            text = statusIcon;
+
+            textPaint = new Paint();
+            textPaint.setTextSize(statusSize);
+            textPaint.setAntiAlias(true);
+            textPaint.setTextAlign(Paint.Align.CENTER);
+        }
+    }
+
+    /**
+     * Draw in its bounds (set via setBounds) respecting optional effects such as alpha (set via setAlpha) and color
+     * filter (set via setColorFilter) a circular background with a user's first character.
+     *
+     * @param canvas The canvas to draw into
+     */
+    @Override
+    public void draw(@NonNull Canvas canvas) {
+        if (text != null) {
+            textPaint.setTextSize(1.6f * radius);
+            canvas.drawText(text, radius, radius - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint);
+        }
+
+        if (icon != -1) {
+
+            Paint backgroundPaint = new Paint();
+            backgroundPaint.setStyle(Paint.Style.FILL);
+            backgroundPaint.setAntiAlias(true);
+            backgroundPaint.setColor(backgroundColor);
+
+            canvas.drawCircle(radius, radius, radius, backgroundPaint);
+
+            Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), icon, null);
+
+            if (drawable != null) {
+                drawable.setBounds(0,
+                                   0,
+                                   (int) (2 * radius),
+                                   (int) (2 * radius));
+                drawable.draw(canvas);
+            }
+        }
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        textPaint.setAlpha(alpha);
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter cf) {
+        textPaint.setColorFilter(cf);
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+}

+ 114 - 29
app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java

@@ -3,8 +3,10 @@
  *
  * @author Andy Scherzinger
  * @author Mario Danic
+ * @author Marcel Hibbe
  * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
+ * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -40,11 +42,16 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.activities.MainActivity;
 import com.nextcloud.talk.adapters.items.AdvancedUserItem;
+import com.nextcloud.talk.api.NcApi;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.databinding.DialogChooseAccountBinding;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.User;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.participants.Participant;
+import com.nextcloud.talk.models.json.status.Status;
+import com.nextcloud.talk.models.json.status.StatusOverall;
+import com.nextcloud.talk.ui.StatusDrawable;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 import com.nextcloud.talk.utils.database.user.UserUtils;
@@ -62,24 +69,33 @@ import autodagger.AutoInjector;
 import eu.davidea.flexibleadapter.FlexibleAdapter;
 import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager;
 import io.reactivex.Observer;
+import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
 
 @AutoInjector(NextcloudTalkApplication.class)
 public class ChooseAccountDialogFragment extends DialogFragment {
     private static final String TAG = ChooseAccountDialogFragment.class.getSimpleName();
 
+    private static final float STATUS_SIZE_IN_DP = 9f;
+
     @Inject
     UserUtils userUtils;
 
     @Inject
     CookieManager cookieManager;
 
+    @Inject
+    NcApi ncApi;
+
     private DialogChooseAccountBinding binding;
     private View dialogView;
 
     private FlexibleAdapter<AdvancedUserItem> adapter;
     private final List<AdvancedUserItem> userItems = new ArrayList<>();
 
+    private Status status;
+
     @SuppressLint("InflateParams")
     @NonNull
     @Override
@@ -106,24 +122,26 @@ public class ChooseAccountDialogFragment extends DialogFragment {
             binding.currentAccount.account.setText((Uri.parse(user.getBaseUrl()).getHost()));
 
             if (user.getBaseUrl() != null &&
-                    (user.getBaseUrl().startsWith("http://") || user.getBaseUrl().startsWith("https://"))) {
+                (user.getBaseUrl().startsWith("http://") || user.getBaseUrl().startsWith("https://"))) {
                 binding.currentAccount.userIcon.setVisibility(View.VISIBLE);
 
                 DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                        .setOldController(binding.currentAccount.userIcon.getController())
-                        .setAutoPlayAnimations(true)
-                        .setImageRequest(DisplayUtils.getImageRequestForUrl(
-                                ApiUtils.getUrlForAvatarWithName(
-                                        user.getBaseUrl(),
-                                        user.getUserId(),
-                                        R.dimen.small_item_height),
-                                null))
-                        .build();
+                    .setOldController(binding.currentAccount.userIcon.getController())
+                    .setAutoPlayAnimations(true)
+                    .setImageRequest(DisplayUtils.getImageRequestForUrl(
+                        ApiUtils.getUrlForAvatarWithName(
+                            user.getBaseUrl(),
+                            user.getUserId(),
+                            R.dimen.small_item_height),
+                        null))
+                    .build();
                 binding.currentAccount.userIcon.setController(draweeController);
 
             } else {
                 binding.currentAccount.userIcon.setVisibility(View.INVISIBLE);
             }
+
+            loadCurrentStatus(user);
         }
 
         // Creating listeners for quick-actions
@@ -140,6 +158,17 @@ public class ChooseAccountDialogFragment extends DialogFragment {
             });
         }
 
+        binding.setStatus.setOnClickListener(v -> {
+            dismiss();
+
+            if (status != null) {
+                SetStatusDialogFragment setStatusDialog = SetStatusDialogFragment.newInstance(user, status);
+                setStatusDialog.show(getActivity().getSupportFragmentManager(), "fragment_set_status");
+            } else {
+                Log.w(TAG, "status was null");
+            }
+        });
+
         if (adapter == null) {
             adapter = new FlexibleAdapter<>(userItems, getActivity(), false);
 
@@ -171,6 +200,41 @@ public class ChooseAccountDialogFragment extends DialogFragment {
         prepareViews();
     }
 
+    private void loadCurrentStatus(User user) {
+        String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken());
+
+        if (CapabilitiesUtil.isUserStatusAvailable(userUtils.getCurrentUser())) {
+            binding.statusView.setVisibility(View.VISIBLE);
+
+            ncApi.status(credentials, ApiUtils.getUrlForStatus(user.getBaseUrl())).
+                subscribeOn(Schedulers.io()).
+                observeOn(AndroidSchedulers.mainThread()).
+                subscribe(new Observer<StatusOverall>() {
+
+                    @Override
+                    public void onSubscribe(@NonNull Disposable d) {
+                    }
+
+                    @Override
+                    public void onNext(@NonNull StatusOverall statusOverall) {
+                        status = statusOverall.ocs.data;
+
+                        binding.setStatus.setEnabled(true);
+                        drawStatus();
+                    }
+
+                    @Override
+                    public void onError(@NonNull Throwable e) {
+                        Log.e(TAG, "Can't receive user status from server. ", e);
+                    }
+
+                    @Override
+                    public void onComplete() {
+                    }
+                });
+        }
+    }
+
     private void prepareViews() {
         if (getActivity() != null) {
             LinearLayoutManager layoutManager = new SmoothScrollLinearLayoutManager(getActivity());
@@ -196,21 +260,21 @@ public class ChooseAccountDialogFragment extends DialogFragment {
     }
 
     private final FlexibleAdapter.OnItemClickListener onSwitchItemClickListener =
-            new FlexibleAdapter.OnItemClickListener() {
-        @Override
-        public boolean onItemClick(View view, int position) {
-            if (userItems.size() > position) {
-                UserEntity userEntity = (userItems.get(position)).getEntity();
-                userUtils.createOrUpdateUser(null,
-                                             null,
-                                             null,
-                                             null,
-                                             null,
-                                             Boolean.TRUE,
-                                             null, userEntity.getId(),
-                                             null,
-                                             null,
-                                             null)
+        new FlexibleAdapter.OnItemClickListener() {
+            @Override
+            public boolean onItemClick(View view, int position) {
+                if (userItems.size() > position) {
+                    UserEntity userEntity = (userItems.get(position)).getEntity();
+                    userUtils.createOrUpdateUser(null,
+                                                 null,
+                                                 null,
+                                                 null,
+                                                 null,
+                                                 Boolean.TRUE,
+                                                 null, userEntity.getId(),
+                                                 null,
+                                                 null,
+                                                 null)
                         .subscribe(new Observer<UserEntity>() {
                             @Override
                             public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
@@ -223,7 +287,7 @@ public class ChooseAccountDialogFragment extends DialogFragment {
                                 userUtils.disableAllUsersWithoutId(userEntity.getId());
                                 if (getActivity() != null) {
                                     getActivity().runOnUiThread(
-                                            () -> ((MainActivity) getActivity()).resetConversationsList());
+                                        () -> ((MainActivity) getActivity()).resetConversationsList());
                                 }
                                 dismiss();
                             }
@@ -238,9 +302,30 @@ public class ChooseAccountDialogFragment extends DialogFragment {
                                 // DONE
                             }
                         });
-            }
+                }
 
-            return true;
+                return true;
+            }
+        };
+
+    private void drawStatus() {
+        float size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, getContext());
+        binding.currentAccount.ticker.setBackground(null);
+        binding.currentAccount.ticker.setImageDrawable(new StatusDrawable(
+            status.getStatus(),
+            status.getIcon(),
+            size,
+            getContext().getResources().getColor(R.color.dialog_background),
+            getContext()));
+        binding.currentAccount.ticker.setVisibility(View.VISIBLE);
+
+
+        if (status.getMessage() != null && !status.getMessage().isEmpty()) {
+            binding.currentAccount.status.setText(status.getMessage());
+            binding.currentAccount.status.setVisibility(View.VISIBLE);
+        } else {
+            binding.currentAccount.status.setText("");
+            binding.currentAccount.status.setVisibility(View.GONE);
         }
-    };
+    }
 }

+ 479 - 0
app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt

@@ -0,0 +1,479 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Tobias Kaminsky
+ * @author Marcel Hibbe
+ * Copyright (C) 2020 Nextcloud GmbH
+ * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU 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/>.
+ */
+
+package com.nextcloud.talk.ui.dialog
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import android.widget.AdapterView
+import android.widget.AdapterView.OnItemSelectedListener
+import android.widget.ArrayAdapter
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import autodagger.AutoInjector
+import com.bluelinelabs.logansquare.LoganSquare
+import com.nextcloud.talk.R
+import com.nextcloud.talk.adapters.PredefinedStatusClickListener
+import com.nextcloud.talk.adapters.PredefinedStatusListAdapter
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.databinding.DialogSetStatusBinding
+import com.nextcloud.talk.models.database.User
+import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.nextcloud.talk.models.json.status.ClearAt
+import com.nextcloud.talk.models.json.status.Status
+import com.nextcloud.talk.models.json.status.StatusType
+import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
+import com.nextcloud.talk.models.json.status.predefined.PredefinedStatusOverall
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+import com.vanniktech.emoji.EmojiPopup
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import okhttp3.ResponseBody
+import java.util.Calendar
+import java.util.Locale
+import javax.inject.Inject
+
+private const val ARG_CURRENT_USER_PARAM = "currentUser"
+private const val ARG_CURRENT_STATUS_PARAM = "currentStatus"
+
+private const val POS_DONT_CLEAR = 0
+private const val POS_HALF_AN_HOUR = 1
+private const val POS_AN_HOUR = 2
+private const val POS_FOUR_HOURS = 3
+private const val POS_TODAY = 4
+private const val POS_END_OF_WEEK = 5
+
+private const val ONE_SECOND_IN_MILLIS = 1000
+private const val ONE_MINUTE_IN_SECONDS = 60
+private const val THIRTY_MINUTES = 30
+private const val FOUR_HOURS = 4
+private const val LAST_HOUR_OF_DAY = 23
+private const val LAST_MINUTE_OF_HOUR = 59
+private const val LAST_SECOND_OF_MINUTE = 59
+
+@AutoInjector(NextcloudTalkApplication::class)
+class SetStatusDialogFragment :
+    DialogFragment(), PredefinedStatusClickListener {
+
+    private val logTag = SetStatusDialogFragment::class.java.simpleName
+
+    private lateinit var binding: DialogSetStatusBinding
+
+    private var currentUser: User? = null
+    private var currentStatus: Status? = null
+
+    val predefinedStatusesList = ArrayList<PredefinedStatus>()
+
+    private lateinit var adapter: PredefinedStatusListAdapter
+    private var clearAt: Long? = null
+    private lateinit var popup: EmojiPopup
+
+    @Inject
+    lateinit var ncApi: NcApi
+
+    lateinit var credentials: String
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+
+        arguments?.let {
+            currentUser = it.getParcelable(ARG_CURRENT_USER_PARAM)
+            currentStatus = it.getParcelable(ARG_CURRENT_STATUS_PARAM)
+
+            credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)
+            ncApi.getPredefinedStatuses(credentials, ApiUtils.getUrlForPredefinedStatuses(currentUser?.baseUrl))
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(object : Observer<ResponseBody> {
+
+                    override fun onSubscribe(d: Disposable) {
+                    }
+
+                    override fun onNext(responseBody: ResponseBody) {
+                        val predefinedStatusOverall: PredefinedStatusOverall = LoganSquare.parse(
+                            responseBody
+                                .string(),
+                            PredefinedStatusOverall::class.java
+                        )
+                        predefinedStatusOverall.ocs?.data?.let { it1 -> predefinedStatusesList.addAll(it1) }
+
+                        adapter.notifyDataSetChanged()
+                    }
+
+                    override fun onError(e: Throwable) {
+                    }
+
+                    override fun onComplete() {}
+                })
+        }
+    }
+
+    @SuppressLint("InflateParams")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        binding = DialogSetStatusBinding.inflate(LayoutInflater.from(context))
+
+        return AlertDialog.Builder(requireContext())
+            .setView(binding.root)
+            .create()
+    }
+
+    @SuppressLint("DefaultLocale")
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        currentStatus?.let {
+            binding.emoji.setText(it.icon)
+            binding.customStatusInput.text?.clear()
+            binding.customStatusInput.setText(it.message)
+            visualizeStatus(it.status)
+
+            if (it.clearAt > 0) {
+                binding.clearStatusAfterSpinner.visibility = View.GONE
+                binding.remainingClearTime.apply {
+                    binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message)
+                    visibility = View.VISIBLE
+                    text = DisplayUtils.getRelativeTimestamp(context, it.clearAt * ONE_SECOND_IN_MILLIS, true)
+                        .toString()
+                        .decapitalize(Locale.getDefault())
+                    setOnClickListener {
+                        visibility = View.GONE
+                        binding.clearStatusAfterSpinner.visibility = View.VISIBLE
+                        binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_after)
+                    }
+                }
+            }
+        }
+
+        adapter = PredefinedStatusListAdapter(this, requireContext())
+        adapter.list = predefinedStatusesList
+
+        binding.predefinedStatusList.adapter = adapter
+        binding.predefinedStatusList.layoutManager = LinearLayoutManager(context)
+
+        binding.onlineStatus.setOnClickListener { setStatus(StatusType.ONLINE) }
+        binding.dndStatus.setOnClickListener { setStatus(StatusType.DND) }
+        binding.awayStatus.setOnClickListener { setStatus(StatusType.AWAY) }
+        binding.invisibleStatus.setOnClickListener { setStatus(StatusType.INVISIBLE) }
+
+        binding.clearStatus.setOnClickListener { clearStatus() }
+        binding.setStatus.setOnClickListener { setStatusMessage() }
+        binding.emoji.setOnClickListener { openEmojiPopup() }
+
+        popup = EmojiPopup.Builder
+            .fromRootView(view)
+            .setOnEmojiClickListener { _, _ ->
+                popup.dismiss()
+                binding.emoji.clearFocus()
+                val imm: InputMethodManager = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as
+                    InputMethodManager
+                imm.hideSoftInputFromWindow(binding.emoji.windowToken, 0)
+            }
+            .build(binding.emoji)
+        binding.emoji.disableKeyboardInput(popup)
+        binding.emoji.forceSingleEmoji()
+
+        val adapter = ArrayAdapter<String>(requireContext(), android.R.layout.simple_spinner_item)
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+        adapter.add(getString(R.string.dontClear))
+        adapter.add(getString(R.string.thirtyMinutes))
+        adapter.add(getString(R.string.oneHour))
+        adapter.add(getString(R.string.fourHours))
+        adapter.add(getString(R.string.today))
+        adapter.add(getString(R.string.thisWeek))
+
+        binding.clearStatusAfterSpinner.apply {
+            this.adapter = adapter
+            onItemSelectedListener = object : OnItemSelectedListener {
+                override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
+                    setClearStatusAfterValue(position)
+                }
+
+                override fun onNothingSelected(parent: AdapterView<*>?) {
+                    // nothing to do
+                }
+            }
+        }
+
+        binding.clearStatus.setTextColor(resources.getColor(R.color.colorPrimary))
+        binding.setStatus.setBackgroundColor(resources.getColor(R.color.colorPrimary))
+
+        binding.customStatusInput.highlightColor = resources.getColor(R.color.colorPrimary)
+    }
+
+    @Suppress("ComplexMethod")
+    private fun setClearStatusAfterValue(item: Int) {
+
+        val currentTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
+
+        when (item) {
+            POS_DONT_CLEAR -> {
+                // don't clear
+                clearAt = null
+            }
+
+            POS_HALF_AN_HOUR -> {
+                // 30 minutes
+                clearAt = currentTime + THIRTY_MINUTES * ONE_MINUTE_IN_SECONDS
+            }
+
+            POS_AN_HOUR -> {
+                // one hour
+                clearAt = currentTime + ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
+            }
+
+            POS_FOUR_HOURS -> {
+                // four hours
+                clearAt = currentTime + FOUR_HOURS * ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
+            }
+
+            POS_TODAY -> {
+                // today
+                val date = Calendar.getInstance().apply {
+                    set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY)
+                    set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR)
+                    set(Calendar.SECOND, LAST_SECOND_OF_MINUTE)
+                }
+                clearAt = date.timeInMillis / ONE_SECOND_IN_MILLIS
+            }
+
+            POS_END_OF_WEEK -> {
+                // end of week
+                val date = Calendar.getInstance().apply {
+                    set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY)
+                    set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR)
+                    set(Calendar.SECOND, LAST_SECOND_OF_MINUTE)
+                }
+
+                while (date.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) {
+                    date.add(Calendar.DAY_OF_YEAR, 1)
+                }
+
+                clearAt = date.timeInMillis / ONE_SECOND_IN_MILLIS
+            }
+        }
+    }
+
+    @Suppress("ReturnCount")
+    private fun clearAtToUnixTime(clearAt: ClearAt?): Long {
+        if (clearAt != null) {
+            if (clearAt.type.equals("period")) {
+                return System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + clearAt.time.toLong()
+            } else if (clearAt.type.equals("end-of")) {
+                if (clearAt.time.equals("day")) {
+                    val date = Calendar.getInstance().apply {
+                        set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY)
+                        set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR)
+                        set(Calendar.SECOND, LAST_SECOND_OF_MINUTE)
+                    }
+                    return date.timeInMillis / ONE_SECOND_IN_MILLIS
+                }
+            }
+        }
+
+        return -1
+    }
+
+    private fun openEmojiPopup() {
+        popup.show()
+    }
+
+    private fun clearStatus() {
+        val credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)
+        ncApi.statusDeleteMessage(credentials, ApiUtils.getUrlForStatusMessage(currentUser?.baseUrl))
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread()).subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {}
+                override fun onNext(statusOverall: GenericOverall) {}
+                override fun onError(e: Throwable) {
+                    Log.e(logTag, "Failed to clear status", e)
+                }
+
+                override fun onComplete() {
+                    dismiss()
+                }
+            })
+    }
+
+    private fun setStatus(statusType: StatusType) {
+        visualizeStatus(statusType)
+
+        ncApi.setStatusType(credentials, ApiUtils.getUrlForSetStatusType(currentUser?.baseUrl), statusType.string)
+            .subscribeOn(
+                Schedulers
+                    .io()
+            )
+            .observeOn(AndroidSchedulers.mainThread()).subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {}
+                override fun onNext(statusOverall: GenericOverall) {
+                    Log.d(logTag, "statusType successfully set")
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(logTag, "Failed to set statusType", e)
+                    clearTopStatus()
+                }
+
+                override fun onComplete() {}
+            })
+    }
+
+    private fun visualizeStatus(statusType: String) {
+        StatusType.values().firstOrNull { it.name == statusType.uppercase(Locale.ROOT) }?.let { visualizeStatus(it) }
+    }
+
+    private fun visualizeStatus(statusType: StatusType) {
+        clearTopStatus()
+        when (statusType) {
+            StatusType.ONLINE -> {
+                binding.onlineStatus.setCardBackgroundColor(resources.getColor(R.color.colorPrimary))
+                binding.onlineHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text_dark_background))
+            }
+            StatusType.AWAY -> {
+                binding.awayStatus.setCardBackgroundColor(resources.getColor(R.color.colorPrimary))
+                binding.awayHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text_dark_background))
+            }
+            StatusType.DND -> {
+                binding.dndStatus.setCardBackgroundColor(resources.getColor(R.color.colorPrimary))
+                binding.dndHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text_dark_background))
+            }
+            StatusType.INVISIBLE -> {
+                binding.invisibleStatus.setCardBackgroundColor(resources.getColor(R.color.colorPrimary))
+                binding.invisibleHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text_dark_background))
+            }
+            else -> Log.d(logTag, "unknown status")
+        }
+    }
+
+    private fun clearTopStatus() {
+        context?.let {
+            val grey = it.resources.getColor(R.color.grey_200)
+            binding.onlineStatus.setCardBackgroundColor(grey)
+            binding.awayStatus.setCardBackgroundColor(grey)
+            binding.dndStatus.setCardBackgroundColor(grey)
+            binding.invisibleStatus.setCardBackgroundColor(grey)
+
+            binding.onlineHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text))
+            binding.awayHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text))
+            binding.dndHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text))
+            binding.invisibleHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text))
+        }
+    }
+
+    private fun setStatusMessage() {
+        var inputText = binding.customStatusInput.text.toString()
+        if (inputText.isEmpty()) {
+            inputText = " "
+        }
+
+        ncApi.setCustomStatusMessage(
+            credentials,
+            ApiUtils.getUrlForSetCustomStatus(currentUser?.baseUrl),
+            binding.emoji.text.toString(),
+            inputText,
+            clearAt
+        )
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<GenericOverall> {
+
+                override fun onSubscribe(d: Disposable) {
+                }
+
+                override fun onNext(t: GenericOverall) {
+                    Log.d(logTag, "CustomStatusMessage successfully set")
+                    dismiss()
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(logTag, "failed to set CustomStatusMessage", e)
+                }
+
+                override fun onComplete() {}
+            })
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        return binding.root
+    }
+
+    override fun onClick(predefinedStatus: PredefinedStatus) {
+        clearAt = clearAtToUnixTime(predefinedStatus.clearAt)
+        binding.emoji.setText(predefinedStatus.icon)
+        binding.customStatusInput.text?.clear()
+        binding.customStatusInput.text?.append(predefinedStatus.message)
+
+        binding.remainingClearTime.visibility = View.GONE
+        binding.clearStatusAfterSpinner.visibility = View.VISIBLE
+        binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_after)
+
+        if (predefinedStatus.clearAt == null) {
+            binding.clearStatusAfterSpinner.setSelection(0)
+        } else {
+            val clearAt = predefinedStatus.clearAt!!
+            if (clearAt.type.equals("period")) {
+                when (clearAt.time) {
+                    "1800" -> binding.clearStatusAfterSpinner.setSelection(POS_HALF_AN_HOUR)
+                    "3600" -> binding.clearStatusAfterSpinner.setSelection(POS_AN_HOUR)
+                    "14400" -> binding.clearStatusAfterSpinner.setSelection(POS_FOUR_HOURS)
+                    else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR)
+                }
+            } else if (clearAt.type.equals("end-of")) {
+                when (clearAt.time) {
+                    "day" -> binding.clearStatusAfterSpinner.setSelection(POS_TODAY)
+                    "week" -> binding.clearStatusAfterSpinner.setSelection(POS_END_OF_WEEK)
+                    else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR)
+                }
+            }
+        }
+        setClearStatusAfterValue(binding.clearStatusAfterSpinner.selectedItemPosition)
+    }
+
+    /**
+     * Fragment creator
+     */
+    companion object {
+        @JvmStatic
+        fun newInstance(user: User, status: Status): SetStatusDialogFragment {
+            val args = Bundle()
+            args.putParcelable(ARG_CURRENT_USER_PARAM, user)
+            args.putParcelable(ARG_CURRENT_STATUS_PARAM, status)
+
+            val dialogFragment = SetStatusDialogFragment()
+            dialogFragment.arguments = args
+            return dialogFragment
+        }
+    }
+}

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

@@ -416,4 +416,32 @@ public class ApiUtils {
     public static String getUrlForSetChatReadMarker(int version, String baseUrl, String roomToken) {
         return getUrlForChat(version, baseUrl, roomToken) + "/read";
     }
+
+    /*
+     * OCS Status API
+     */
+
+    public static String getUrlForStatus(String baseUrl) {
+        return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/user_status";
+    }
+
+    public static String getUrlForSetStatusType(String baseUrl) {
+        return getUrlForStatus(baseUrl) + "/status";
+    }
+
+    public static String getUrlForPredefinedStatuses(String baseUrl) {
+        return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/predefined_statuses";
+    }
+
+    public static String getUrlForStatusMessage(String baseUrl) {
+        return getUrlForStatus(baseUrl) + "/message";
+    }
+
+    public static String getUrlForSetCustomStatus(String baseUrl) {
+        return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/user_status/message/custom";
+    }
+
+    public static String getUrlForUserStatuses(String baseUrl) {
+        return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/statuses";
+    }
 }

+ 67 - 0
app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java

@@ -44,6 +44,7 @@ import android.text.SpannableString;
 import android.text.Spanned;
 import android.text.TextPaint;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.text.method.LinkMovementMethod;
 import android.text.style.AbsoluteSizeSpan;
 import android.text.style.ClickableSpan;
@@ -86,6 +87,8 @@ import org.greenrobot.eventbus.EventBus;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.text.DateFormat;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Matcher;
@@ -124,6 +127,8 @@ public class DisplayUtils {
     private static final String HTTP_PROTOCOL = "http://";
     private static final String HTTPS_PROTOCOL = "https://";
 
+    private static final int DATE_TIME_PARTS_SIZE = 2;
+
     public static void setClickableString(String string, String url, TextView textView) {
         SpannableString spannableString = new SpannableString(string);
         spannableString.setSpan(new ClickableSpan() {
@@ -605,4 +610,66 @@ public class DisplayUtils {
                 return R.string.menu_item_sort_by_name_a_z;
         }
     }
+
+    /**
+     * calculates the relative time string based on the given modification timestamp.
+     *
+     * @param context the app's context
+     * @param modificationTimestamp the UNIX timestamp of the file modification time in milliseconds.
+     * @return a relative time string
+     */
+
+    public static CharSequence getRelativeTimestamp(Context context, long modificationTimestamp, boolean showFuture) {
+        return getRelativeDateTimeString(context,
+                                         modificationTimestamp,
+                                         android.text.format.DateUtils.SECOND_IN_MILLIS,
+                                         DateUtils.WEEK_IN_MILLIS,
+                                         0,
+                                         showFuture);
+    }
+
+    public static CharSequence getRelativeDateTimeString(Context c,
+                                                         long time,
+                                                         long minResolution,
+                                                         long transitionResolution,
+                                                         int flags,
+                                                         boolean showFuture) {
+
+        CharSequence dateString = "";
+
+        // in Future
+        if (!showFuture && time > System.currentTimeMillis()) {
+            return DisplayUtils.unixTimeToHumanReadable(time);
+        }
+        // < 60 seconds -> seconds ago
+        long diff = System.currentTimeMillis() - time;
+        if (diff > 0 && diff < 60 * 1000 && minResolution == DateUtils.SECOND_IN_MILLIS) {
+            return c.getString(R.string.secondsAgo);
+        } else {
+            dateString = DateUtils.getRelativeDateTimeString(c, time, minResolution, transitionResolution, flags);
+        }
+
+        String[] parts = dateString.toString().split(",");
+        if (parts.length == DATE_TIME_PARTS_SIZE) {
+            if (parts[1].contains(":") && !parts[0].contains(":")) {
+                return parts[0];
+            } else if (parts[0].contains(":") && !parts[1].contains(":")) {
+                return parts[1];
+            }
+        }
+        // dateString contains unexpected format. fallback: use relative date time string from android api as is.
+        return dateString.toString();
+    }
+
+    /**
+     * Converts Unix time to human readable format
+     *
+     * @param milliseconds that have passed since 01/01/1970
+     * @return The human readable time for the users locale
+     */
+    public static String unixTimeToHumanReadable(long milliseconds) {
+        Date date = new Date(milliseconds);
+        DateFormat df = DateFormat.getDateTimeInstance();
+        return df.format(date);
+    }
 }

+ 34 - 0
app/src/main/res/drawable/ic_edit.xml

@@ -0,0 +1,34 @@
+<!--
+  ~
+  ~ 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/>.
+  -->
+
+<vector xmlns:tools="http://schemas.android.com/tools"
+    android:autoMirrored="true"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    tools:ignore="VectorRaster">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
+</vector>

+ 32 - 0
app/src/main/res/drawable/ic_user_status_away.xml

@@ -0,0 +1,32 @@
+<!--
+  Nextcloud Android client application
+
+  @author Tobias Kaminsky
+  Copyright (C) 2020 Tobias Kaminsky
+  Copyright (C) 2020 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
+  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/>.
+-->
+<vector xmlns:tools="http://schemas.android.com/tools"
+    android:autoMirrored="true"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    tools:ignore="VectorRaster">
+    <path
+        android:fillColor="#f4a331"
+        android:pathData="m10.615,2.1094c-4.8491,0.6811 -8.6152,4.8615 -8.6152,9.8906 0,5.5 4.5,10 10,10 5.0292,0 9.2096,-3.7661 9.8906,-8.6152 -1.4654,1.601 -3.5625,2.6152 -5.8906,2.6152 -4.4,0 -8,-3.6 -8,-8 0,-2.3281 1.0143,-4.4252 2.6152,-5.8906z" />
+</vector>

+ 38 - 0
app/src/main/res/drawable/ic_user_status_dnd.xml

@@ -0,0 +1,38 @@
+<!--
+  Nextcloud Android client application
+
+  @author Tobias Kaminsky
+  Copyright (C) 2020 Tobias Kaminsky
+  Copyright (C) 2020 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
+  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/>.
+-->
+<vector xmlns:tools="http://schemas.android.com/tools"
+    android:autoMirrored="true"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    tools:ignore="VectorRaster">
+    <path
+        android:fillColor="#ed484c"
+        android:pathData="m12,2c-5.52,0 -10,4.48 -10,10s4.48,10 10,10 10,-4.48 10,-10 -4.48,-10 -10,-10z" />
+    <path
+        android:fillColor="#fdffff"
+        android:pathData="m8,10h8c1.108,0 2,0.892 2,2s-0.892,2 -2,2h-8c-1.108,0 -2,-0.892 -2,-2s0.892,-2 2,-2z"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="round"
+        android:strokeWidth="2" />
+</vector>

+ 34 - 0
app/src/main/res/drawable/ic_user_status_invisible.xml

@@ -0,0 +1,34 @@
+<!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2020 Tobias Kaminsky
+  ~ Copyright (C) 2020 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/>.
+  -->
+
+<vector xmlns:tools="http://schemas.android.com/tools"
+    android:autoMirrored="true"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    tools:ignore="VectorRaster">
+    <path
+        android:fillColor="#000000"
+        android:pathData="m12,2c-5.52,0 -10,4.48 -10,10s4.48,10 10,10 10,-4.48 10,-10 -4.48,-10 -10,-10zM12,6a6,6 0,0 1,6 6,6 6,0 0,1 -6,6 6,6 0,0 1,-6 -6,6 6,0 0,1 6,-6z" />
+</vector>

+ 32 - 0
app/src/main/res/drawable/online_status.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+    Nextcloud Android client application
+
+    @author Andy Scherzinger
+    Copyright (C) 2019 Andy Scherzinger
+
+    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 <http://www.gnu.org/licenses/>.
+-->
+<vector xmlns:tools="http://schemas.android.com/tools"
+    android:autoMirrored="true"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    tools:ignore="VectorRaster">
+    <path
+        android:fillColor="#49b382"
+        android:pathData="m12,2c-5.52,0 -10,4.48 -10,10s4.48,10 10,10 10,-4.48 10,-10 -4.48,-10 -10,-10z" />
+</vector>
+

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

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+    Nextcloud Android client application
+
+    @author Andy Scherzinger
+    Copyright (C) 2019 Andy Scherzinger
+
+    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 <http://www.gnu.org/licenses/>.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="#00ff00" />
+
+    <stroke android:width="1.3dp"
+        android:color="@color/bg_default"/>
+</shape>

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

@@ -107,6 +107,7 @@
                 android:maxLines="1"
                 android:textColor="?android:attr/textColorSecondary"
                 android:visibility="gone"
+                tools:visibility="visible"
                 tools:text="☁️ My custom status" />
 
             <TextView

+ 43 - 5
app/src/main/res/layout/dialog_choose_account.xml

@@ -17,6 +17,7 @@
 -->
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
@@ -31,15 +32,52 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
+    <LinearLayout
+        android:id="@+id/statusView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:visibility="gone"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/current_account"
+        tools:visibility="visible">
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="4dp"
+            android:background="@color/list_divider_background" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/set_status"
+            style="@style/Nextcloud.Material.TextButton"
+            android:layout_width="match_parent"
+            android:layout_height="50dp"
+            android:layout_marginStart="12dp"
+            android:layout_marginEnd="12dp"
+            android:paddingStart="18dp"
+            android:paddingEnd="0dp"
+            android:text="@string/set_status"
+            android:textAlignment="textStart"
+            android:textAllCaps="false"
+            android:textColor="@color/fontAppbar"
+            android:enabled="false"
+            app:icon="@drawable/ic_edit"
+            app:iconGravity="start"
+            app:iconPadding="22dp"
+            app:iconTint="@color/fontAppbar" />
+    </LinearLayout>
+
     <View
         android:id="@+id/separator_line"
         android:layout_width="0dp"
         android:layout_height="1dp"
         android:layout_marginTop="4dp"
-        android:background="@color/controller_chat_separator"
+        android:background="@color/list_divider_background"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/current_account" />
+        app:layout_constraintTop_toBottomOf="@id/statusView" />
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/accounts_list"
@@ -60,7 +98,7 @@
         android:layout_height="50dp"
         android:layout_marginStart="12dp"
         android:layout_marginEnd="12dp"
-        android:paddingStart="14dp"
+        android:paddingStart="18dp"
         android:paddingEnd="4dp"
         android:text="@string/nc_account_chooser_add_account"
         android:textAlignment="textStart"
@@ -82,7 +120,7 @@
         android:layout_marginStart="12dp"
         android:layout_marginEnd="12dp"
         android:layout_marginBottom="12dp"
-        android:paddingStart="16dp"
+        android:paddingStart="18dp"
         android:paddingEnd="4dp"
         android:text="@string/nc_settings"
         android:textAlignment="textStart"
@@ -90,7 +128,7 @@
         android:textColor="@color/fontAppbar"
         app:icon="@drawable/ic_settings"
         app:iconGravity="start"
-        app:iconPadding="20dp"
+        app:iconPadding="22dp"
         app:iconTint="@color/fontAppbar"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"

+ 458 - 0
app/src/main/res/layout/dialog_set_status.xml

@@ -0,0 +1,458 @@
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2020 Andy Scherzinger
+  Copyright (C) 2020 Tobias Kaminsky
+  Copyright (C) 2020 Nextcloud GmbH
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License version 2,
+  as published by the Free Software Foundation.
+
+  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/>.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:padding="@dimen/standard_padding">
+
+    <TextView
+        android:id="@+id/onlineStatusView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="@dimen/standard_half_margin"
+        android:text="@string/online_status"
+        android:textColor="@color/high_emphasis_text"
+        android:textSize="@dimen/activity_list_item_title_header_text_size"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <LinearLayout
+        android:id="@+id/statusView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:baselineAligned="false"
+        android:orientation="vertical"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/onlineStatusView">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/standard_margin"
+            android:orientation="horizontal">
+
+            <com.google.android.material.card.MaterialCardView
+                android:id="@+id/onlineStatus"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/online_status_item_height"
+                android:layout_gravity="center_vertical"
+                android:layout_marginEnd="@dimen/standard_half_margin"
+                android:layout_weight="1"
+                android:orientation="horizontal"
+                app:cardBackgroundColor="@color/grey_200"
+                app:cardElevation="0dp"
+                app:cardCornerRadius="@dimen/button_corner_radius">
+
+                <RelativeLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginStart="@dimen/standard_margin"
+                    android:layout_marginEnd="@dimen/standard_margin"
+                    android:orientation="horizontal"
+                    tools:ignore="UnusedAttribute">
+
+                    <ImageView
+                        android:id="@+id/online_icon"
+                        android:layout_width="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_height="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_alignParentStart="true"
+                        android:layout_centerVertical="true"
+                        android:layout_gravity="top|start"
+                        android:layout_marginEnd="@dimen/standard_half_margin"
+                        android:contentDescription="@null"
+                        android:src="@drawable/online_status"
+                        app:tint="@color/hwSecurityGreen" />
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:layout_toEndOf="@id/online_icon"
+                        android:orientation="vertical">
+
+                        <TextView
+                            android:id="@+id/online_headline"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:ellipsize="end"
+                            android:gravity="bottom"
+                            android:maxLines="1"
+                            android:text="@string/online"
+                            android:textAppearance="?android:attr/textAppearanceListItem" />
+
+                        <TextView
+                            android:id="@+id/online_text"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_marginStart="@dimen/standard_half_margin"
+                            android:layout_marginBottom="@dimen/standard_quarter_margin"
+                            android:ellipsize="end"
+                            android:gravity="top"
+                            android:maxLines="1"
+                            android:textColor="?android:attr/textColorSecondary"
+                            android:visibility="gone" />
+
+                    </LinearLayout>
+
+                </RelativeLayout>
+
+            </com.google.android.material.card.MaterialCardView>
+
+            <com.google.android.material.card.MaterialCardView
+                android:id="@+id/awayStatus"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/online_status_item_height"
+                android:layout_gravity="center_vertical"
+                android:layout_marginStart="@dimen/standard_half_margin"
+                android:layout_weight="1"
+                android:orientation="horizontal"
+                app:cardBackgroundColor="@color/grey_200"
+                app:cardElevation="0dp"
+                app:cardCornerRadius="@dimen/button_corner_radius">
+
+                <RelativeLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginStart="@dimen/standard_margin"
+                    android:layout_marginEnd="@dimen/standard_margin"
+                    android:orientation="horizontal"
+                    tools:ignore="UnusedAttribute">
+
+                    <ImageView
+                        android:id="@+id/away_icon"
+                        android:layout_width="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_height="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_alignParentStart="true"
+                        android:layout_centerVertical="true"
+                        android:layout_gravity="top|start"
+                        android:layout_marginEnd="@dimen/standard_half_margin"
+                        android:contentDescription="@null"
+                        android:src="@drawable/ic_user_status_away"
+                        app:tint="#f4a331" />
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:layout_toEndOf="@id/away_icon"
+                        android:orientation="vertical">
+
+                        <TextView
+                            android:id="@+id/away_headline"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:ellipsize="end"
+                            android:gravity="bottom"
+                            android:maxLines="1"
+                            android:text="@string/away"
+                            android:textAppearance="?android:attr/textAppearanceListItem" />
+
+                        <TextView
+                            android:id="@+id/away_text"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_marginStart="@dimen/standard_half_margin"
+                            android:layout_marginBottom="@dimen/standard_quarter_margin"
+                            android:ellipsize="end"
+                            android:gravity="top"
+                            android:maxLines="1"
+                            android:textColor="?android:attr/textColorSecondary"
+                            android:visibility="gone" />
+
+                    </LinearLayout>
+
+                </RelativeLayout>
+
+            </com.google.android.material.card.MaterialCardView>
+
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/standard_margin"
+            android:orientation="horizontal">
+
+            <com.google.android.material.card.MaterialCardView
+                android:id="@+id/dndStatus"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/online_status_item_height"
+                android:layout_gravity="center_vertical"
+                android:layout_marginEnd="@dimen/standard_half_margin"
+                android:layout_weight="1"
+                android:orientation="horizontal"
+                app:cardBackgroundColor="@color/grey_200"
+                app:cardElevation="0dp"
+                app:cardCornerRadius="@dimen/button_corner_radius">
+
+                <RelativeLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginStart="@dimen/standard_margin"
+                    android:layout_marginEnd="@dimen/standard_margin"
+                    android:orientation="horizontal"
+                    tools:ignore="UnusedAttribute">
+
+                    <ImageView
+                        android:id="@+id/dnd_icon"
+                        android:layout_width="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_height="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_alignParentStart="true"
+                        android:layout_centerVertical="true"
+                        android:layout_gravity="top|start"
+                        android:layout_marginEnd="@dimen/standard_half_margin"
+                        android:contentDescription="@null"
+                        android:src="@drawable/ic_user_status_dnd" />
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:layout_toEndOf="@id/dnd_icon"
+                        android:orientation="vertical">
+
+                        <TextView
+                            android:id="@+id/dnd_headline"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:ellipsize="end"
+                            android:gravity="bottom"
+                            android:maxLines="1"
+                            android:text="@string/dnd"
+                            android:textAppearance="?android:attr/textAppearanceListItem" />
+
+                    </LinearLayout>
+
+                </RelativeLayout>
+
+            </com.google.android.material.card.MaterialCardView>
+
+            <com.google.android.material.card.MaterialCardView
+                android:id="@+id/invisibleStatus"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/online_status_item_height"
+                android:layout_gravity="center_vertical"
+                android:layout_marginStart="@dimen/standard_half_margin"
+
+                android:layout_weight="1"
+                android:orientation="horizontal"
+                app:cardBackgroundColor="@color/grey_200"
+                app:cardElevation="0dp"
+                app:cardCornerRadius="@dimen/button_corner_radius">
+
+                <RelativeLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginStart="@dimen/standard_margin"
+                    android:layout_marginEnd="@dimen/standard_margin"
+                    android:orientation="horizontal"
+                    tools:ignore="UnusedAttribute">
+
+                    <ImageView
+                        android:id="@+id/invisible_icon"
+                        android:layout_width="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_height="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_alignParentStart="true"
+                        android:layout_centerVertical="true"
+                        android:layout_gravity="top|start"
+                        android:layout_marginEnd="@dimen/standard_half_margin"
+                        android:contentDescription="@null"
+                        android:src="@drawable/ic_user_status_invisible" />
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:layout_toEndOf="@id/invisible_icon"
+                        android:orientation="vertical">
+
+                        <TextView
+                            android:id="@+id/invisible_headline"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:ellipsize="end"
+                            android:gravity="bottom"
+                            android:maxLines="1"
+                            android:text="@string/invisible"
+                            android:textAppearance="?android:attr/textAppearanceListItem" />
+
+                    </LinearLayout>
+
+                </RelativeLayout>
+
+            </com.google.android.material.card.MaterialCardView>
+
+        </LinearLayout>
+    </LinearLayout>
+
+
+    <View
+        android:id="@+id/separator_line"
+        android:layout_width="0dp"
+        android:layout_height="1dp"
+        android:layout_marginTop="@dimen/standard_quarter_margin"
+        android:background="@color/list_divider_background"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/statusView" />
+
+    <LinearLayout
+        android:id="@+id/fragment_container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/statusMessage"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/standard_half_margin"
+            android:text="@string/status_message"
+            android:textColor="@color/high_emphasis_text"
+            android:textSize="@dimen/activity_list_item_title_header_text_size"
+            app:layout_constraintTop_toBottomOf="@+id/statusView" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+
+            <com.google.android.material.card.MaterialCardView
+                android:id="@+id/emojiCard"
+                android:layout_width="@dimen/activity_row_layout_height"
+                android:layout_height="@dimen/activity_row_layout_height"
+                android:layout_gravity="center"
+                android:layout_marginTop="@dimen/standard_eighth_margin"
+                android:layout_marginEnd="@dimen/standard_margin"
+                android:orientation="horizontal"
+                app:cardBackgroundColor="@color/grey_200"
+                app:cardCornerRadius="24dp"
+                app:cardElevation="0dp">
+
+                <com.vanniktech.emoji.EmojiEditText
+                    android:id="@+id/emoji"
+                    android:layout_width="@dimen/activity_row_layout_height"
+                    android:layout_height="@dimen/activity_row_layout_height"
+                    android:background="@color/grey_200"
+                    android:cursorVisible="false"
+                    android:gravity="center"
+                    android:text="@string/default_emoji"
+                    android:textSize="24sp" />
+
+            </com.google.android.material.card.MaterialCardView>
+
+            <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/customStatusInput_container"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:hint="@string/whats_your_status">
+
+                <com.google.android.material.textfield.TextInputEditText
+                    android:id="@+id/customStatusInput"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:ems="10"
+                    android:importantForAutofill="no"
+                    android:inputType="textAutoCorrect"
+                    android:scrollbars="vertical">
+
+                </com.google.android.material.textfield.TextInputEditText>
+
+            </com.google.android.material.textfield.TextInputLayout>
+
+        </LinearLayout>
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/predefinedStatusList"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            tools:itemCount="5"
+            tools:listitem="@layout/predefined_status" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/standard_half_margin"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/clearStatusMessageTextView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/clear_status_message_after"
+            android:textColor="@color/high_emphasis_text" />
+
+        <Spinner
+            android:id="@+id/clearStatusAfterSpinner"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:id="@+id/remainingClearTime"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/high_emphasis_text"
+            android:layout_marginStart="4dp"
+            android:visibility="gone" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/standard_half_margin"
+        android:orientation="horizontal">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/clearStatus"
+            style="@style/OutlinedButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="@dimen/standard_half_margin"
+            android:layout_weight="1"
+            android:text="@string/clear_status_message"
+            app:cornerRadius="@dimen/button_corner_radius" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/setStatus"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="@string/set_status_message"
+            android:theme="@style/Button.Primary"
+            app:cornerRadius="@dimen/button_corner_radius" />
+
+    </LinearLayout>
+
+</LinearLayout>

+ 62 - 0
app/src/main/res/layout/predefined_status.xml

@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+
+  Nextcloud Android client application
+
+  Copyright (C) 2020 Andy Scherzinger
+  Copyright (C) 2020 Tobias Kaminsky
+  Copyright (C) 2020 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="48dp">
+
+    <TextView
+        android:id="@+id/icon"
+        android:layout_width="48dp"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:textSize="25sp"
+        tools:text="📆" />
+
+    <TextView
+        android:id="@+id/name"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        tools:text="In a meeting" />
+
+    <TextView
+        android:id="@+id/divider"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_margin="@dimen/standard_half_margin"
+        android:gravity="center_vertical"
+        android:text="@string/divider"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:textColor="?android:attr/textColorSecondary" />
+
+    <TextView
+        android:id="@+id/clearAt"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:textColor="?android:attr/textColorSecondary"
+        tools:text="an hour" />
+</LinearLayout>

+ 2 - 2
app/src/main/res/layout/rv_item_contact.xml

@@ -48,7 +48,7 @@
         android:layout_height="wrap_content"
         android:layout_centerVertical="true"
         android:layout_toStartOf="@id/checkedImageView"
-        android:layout_toEndOf="@id/simple_drawee_view"
+        android:layout_toEndOf="@id/avatar_drawee_view"
         android:ellipsize="end"
         android:lines="1"
         android:textAlignment="viewStart"
@@ -56,7 +56,7 @@
         tools:text="Jane Doe" />
 
     <com.facebook.drawee.view.SimpleDraweeView
-        android:id="@+id/simple_drawee_view"
+        android:id="@+id/avatar_drawee_view"
         android:layout_width="@dimen/avatar_size"
         android:layout_height="@dimen/avatar_size"
         android:layout_centerVertical="true"

+ 87 - 49
app/src/main/res/layout/rv_item_conversation_info_participant.xml

@@ -18,67 +18,105 @@
   ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="@dimen/item_height"
-    android:orientation="vertical">
+    android:layout_height="wrap_content"
+    android:layout_marginBottom="@dimen/standard_half_margin"
+    android:layout_marginTop="@dimen/standard_margin">
 
 
     <com.facebook.drawee.view.SimpleDraweeView
-        android:id="@+id/simple_drawee_view"
-        android:layout_width="@dimen/avatar_size"
-        android:layout_height="@dimen/avatar_size"
-        android:layout_centerVertical="true"
-        android:layout_marginStart="@dimen/activity_horizontal_margin"
+        android:id="@+id/avatar_drawee_view"
+        android:layout_width="@dimen/small_item_height"
+        android:layout_height="@dimen/small_item_height"
+        android:layout_marginStart="@dimen/standard_margin"
+        android:contentDescription="@null"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
         app:roundAsCircle="true" />
 
+    <com.vanniktech.emoji.EmojiEditText
+        android:id="@+id/participant_status_emoji"
+        android:layout_width="22dp"
+        android:layout_height="wrap_content"
+        android:background="@color/transparent"
+        android:cursorVisible="false"
+        android:gravity="center|start"
+        android:text="@string/default_emoji"
+        android:textSize="16sp"
+        app:layout_constraintStart_toStartOf="@+id/name_text"
+        app:layout_constraintTop_toBottomOf="@+id/name_text" />
+
     <ImageView
-        android:id="@+id/videoCallIcon"
-        android:layout_width="24dp"
-        android:layout_height="24dp"
-        android:layout_alignParentEnd="true"
-        android:layout_centerVertical="true"
-        android:layout_centerInParent="true"
-        android:layout_marginEnd="@dimen/activity_horizontal_margin"
-        android:src="@drawable/ic_videocam_grey_600_24dp"
-        android:contentDescription="@null"
-        android:visibility="gone"
-        tools:visibility="visible" />
+        android:id="@+id/user_status_image"
+        android:layout_width="18dp"
+        android:layout_height="18dp"
+        android:layout_gravity="bottom|end"
+        android:contentDescription="@string/nc_account_chooser_active_user"
+        app:layout_constraintBottom_toBottomOf="@+id/avatar_drawee_view"
+        app:layout_constraintEnd_toEndOf="@+id/avatar_drawee_view"
+        tools:src="@drawable/emoji_one_category_smileysandpeople"/>
+
+    <androidx.emoji.widget.EmojiTextView
+        android:id="@+id/name_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/standard_margin"
+        android:layout_marginTop="2dp"
+        android:singleLine="true"
+        android:textAlignment="viewStart"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:textColor="@color/conversation_item_header"
+        app:layout_constraintStart_toEndOf="@id/avatar_drawee_view"
+        app:layout_constraintTop_toTopOf="@+id/avatar_drawee_view"
+        tools:text="Jane Doe" />
 
-    <LinearLayout
-        android:id="@+id/linear_layout"
-        android:layout_width="match_parent"
+    <androidx.emoji.widget.EmojiTextView
+        android:id="@+id/conversation_info_status_message"
+        android:layout_width="0dp"
         android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:layout_marginStart="@dimen/margin_between_elements"
-        android:layout_marginEnd="@dimen/margin_between_elements"
-        android:layout_toEndOf="@id/simple_drawee_view"
-        android:layout_toStartOf="@id/videoCallIcon"
-        android:orientation="vertical">
+        android:layout_marginBottom="4dp"
+        android:ellipsize="end"
+        android:maxLines="3"
+        android:textAlignment="viewStart"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:textColor="?android:attr/textColorSecondary"
+        android:layout_marginEnd="@dimen/side_margin"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/participant_status_emoji"
+        app:layout_constraintTop_toBottomOf="@+id/name_text"
+        tools:text="this is a very long status message. server allows only 81 chars here. 0123456789" />
 
-        <androidx.emoji.widget.EmojiTextView
-            android:id="@+id/name_text"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:ellipsize="middle"
-            android:singleLine="true"
-            android:textAlignment="viewStart"
-            android:textAppearance="?android:attr/textAppearanceListItem"
-            android:textColor="@color/conversation_item_header"
-            tools:text="Jane Doe" />
+    <androidx.emoji.widget.EmojiTextView
+        android:id="@+id/secondary_text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="8dp"
+        android:singleLine="true"
+        android:textAlignment="viewStart"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:textColor="?android:attr/textColorSecondary"
+        android:layout_marginEnd="@dimen/side_margin"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/name_text"
+        app:layout_constraintTop_toTopOf="@+id/name_text"
+        tools:text="Moderator (or userid for autocomplete mention)" />
 
-        <androidx.emoji.widget.EmojiTextView
-            android:id="@+id/secondary_text"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:ellipsize="middle"
-            android:singleLine="true"
-            android:textAlignment="viewStart"
-            android:textColor="?android:attr/textColorSecondary"
-            tools:text="@string/nc_moderator" />
 
-    </LinearLayout>
+    <ImageView
+        android:id="@+id/videoCallIcon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/standard_half_margin"
+        android:layout_marginEnd="@dimen/standard_half_margin"
+        android:contentDescription="@null"
+        android:src="@drawable/ic_videocam_grey_600_24dp"
+        android:visibility="gone"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/secondary_text"
+        tools:visibility="visible" />
 
-</RelativeLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 8 - 2
app/src/main/res/layout/rv_item_conversation_with_last_message.xml

@@ -43,8 +43,7 @@
             android:layout_width="@dimen/small_item_height"
             android:layout_height="@dimen/small_item_height"
             android:contentDescription="@null"
-            app:roundAsCircle="true"
-            tools:src="@drawable/ic_call_black_24dp" />
+            app:roundAsCircle="true" />
 
         <ImageView
             android:id="@+id/favoriteConversationImageView"
@@ -56,6 +55,13 @@
             app:tint="@color/favorite_icon_tint"
             app:tintMode="src_in" />
 
+        <ImageView
+            android:id="@+id/user_status_image"
+            android:layout_width="18dp"
+            android:layout_height="18dp"
+            android:layout_gravity="bottom|end"
+            android:contentDescription="@string/nc_account_chooser_active_user"
+            tools:src="@drawable/emoji_one_category_smileysandpeople"/>
     </FrameLayout>
 
     <RelativeLayout

+ 0 - 77
app/src/main/res/layout/rv_item_mention.xml

@@ -1,77 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ Nextcloud Talk application
-  ~
-  ~ @author Mario Danic
-  ~ @author Andy Scherzinger
-  ~ Copyright (C) 2017-2018 Mario Danic
-  ~ Copyright (C) 2017 Andy Scherzinger
-  ~
-  ~ 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/>.
-  -->
-
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/item_height"
-    android:orientation="vertical">
-
-    <FrameLayout
-        android:id="@+id/frame_layout"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_centerVertical="true"
-        android:layout_marginStart="@dimen/activity_horizontal_margin">
-
-        <com.facebook.drawee.view.SimpleDraweeView
-            android:id="@+id/simple_drawee_view"
-            android:layout_width="@dimen/avatar_size"
-            android:layout_height="@dimen/avatar_size"
-            app:roundAsCircle="true" />
-
-    </FrameLayout>
-
-    <LinearLayout
-        android:id="@+id/linear_layout"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:layout_marginStart="@dimen/margin_between_elements"
-        android:layout_marginEnd="@dimen/margin_between_elements"
-        android:layout_toEndOf="@id/frame_layout"
-        android:orientation="vertical">
-
-        <androidx.emoji.widget.EmojiTextView
-            android:id="@+id/name_text"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:ellipsize="middle"
-            android:singleLine="true"
-            android:textAlignment="viewStart"
-            android:textAppearance="@style/ListItem"
-            tools:text="Call item text" />
-
-        <androidx.emoji.widget.EmojiTextView
-            android:id="@+id/secondary_text"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:ellipsize="middle"
-            android:singleLine="true"
-            android:textAlignment="viewStart"
-            android:textColor="@color/textColorMaxContrast"
-            tools:text="A week ago" />
-
-    </LinearLayout>
-
-</RelativeLayout>

+ 6 - 0
app/src/main/res/values-night/colors.xml

@@ -38,6 +38,7 @@
     <color name="high_emphasis_text">#deffffff</color>
     <color name="medium_emphasis_text">#99ffffff</color>
     <color name="low_emphasis_text">#61ffffff</color>
+    <color name="high_emphasis_text_inverse">#de000000</color>
 
     <color name="bg_default">#121212</color>
     <color name="bg_default_semitransparent">#99121212</color>
@@ -65,4 +66,9 @@
     <!-- shimmer element colors -->
     <color name="nc_shimmer_default_color">#4B4B4B</color>
     <color name="nc_shimmer_darker_color">#282828</color>
+
+    <color name="list_divider_background">#222222</color>
+    <color name="grey_200">#818181</color>
+
+    <color name="dialog_background">#353535</color>
 </resources>

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

@@ -39,6 +39,7 @@
     <color name="high_emphasis_text">#de000000</color>
     <color name="medium_emphasis_text">#99000000</color>
     <color name="low_emphasis_text">#61000000</color>
+    <color name="high_emphasis_text_inverse">#deffffff</color>
 
     <!-- general text colors for dark background -->
     <color name="high_emphasis_text_dark_background">#deffffff</color>
@@ -97,4 +98,11 @@
 
     <color name="camera_bg_tint">#99121212</color>
 
+    <color name="list_divider_background">#eeeeee</color>
+    <color name="grey_200">#EEEEEE</color>
+
+    <!-- this is just a helper for status icon background because getting the background color of a dialog is not
+    possible?! don't use this to set the background of dialogs -->
+    <color name="dialog_background">#FFFFFF</color>
+
 </resources>

+ 6 - 0
app/src/main/res/values/dimens.xml

@@ -63,4 +63,10 @@
     <dimen name="call_grid_item_min_height">180dp</dimen>
     <dimen name="call_controls_height">110dp</dimen>
     <dimen name="zero">0dp</dimen>
+
+    <dimen name="online_status_item_height">52dp</dimen>
+    <dimen name="standard_quarter_margin">4dp</dimen>
+    <dimen name="activity_list_item_title_header_text_size">16sp</dimen>
+    <dimen name="activity_row_layout_height">48dp</dimen>
+    <dimen name="standard_eighth_margin">2dp</dimen>
 </resources>

+ 23 - 1
app/src/main/res/values/strings.xml

@@ -268,6 +268,28 @@
     <string name="nc_remove_group_and_members">Remove group and members</string>
     <string name="nc_attendee_pin">Pin: %1$s</string>
 
+    <!-- User Status -->
+    <string name="set_status">Set status</string>
+    <string name="online_status">Online status</string>
+    <string name="status_message">Status message</string>
+    <string name="whats_your_status">What is your status?</string>
+    <string name="clear_status_message_after">Clear status message after</string>
+    <string name="clear_status_message">Clear status message</string>
+    <string name="set_status_message">Set status message</string>
+    <string name="online">Online</string>
+    <string name="dnd">Do not disturb</string>
+    <string name="away">Away</string>
+    <string name="invisible">Invisible</string>
+    <string translatable="false" name="divider">—</string>
+    <string translatable="false" name="default_emoji">😃</string>
+    <string name="dontClear">Don\'t clear</string>
+    <string name="today">Today</string>
+    <string name="thirtyMinutes">30 minutes</string>
+    <string name="oneHour">1 hour</string>
+    <string name="fourHours">4 hours</string>
+    <string name="thisWeek">This week</string>
+    <string name="secondsAgo">seconds ago</string>
+
     <!-- Conversations List-->
     <string name="nc_new_mention">Unread mentions</string>
     <string name="conversations">Conversations</string>
@@ -435,8 +457,8 @@
     <string name="nc_phone_book_integration_account_not_found">Account not found</string>
 
     <string name="starred">Favorite</string>
+    <string name="user_status">Status</string>
     <string name="encrypted">Encrypted</string>
-    <string name="password_protected">Password protected</string>
 
     <string name="avatar">Avatar</string>
     <string name="account_icon">Account icon</string>

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

@@ -240,4 +240,12 @@
         <item name="android:windowSoftInputMode">adjustResize</item>
     </style>
 
+    <style name="OutlinedButton" parent="Widget.MaterialComponents.Button.OutlinedButton">
+        <item name="colorAccent">@color/transparent</item>
+        <item name="android:textColor">@color/colorPrimaryDark</item>
+        <item name="android:textAllCaps">false</item>
+        <item name="android:typeface">sans</item>
+        <item name="android:textStyle">bold</item>
+    </style>
+
 </resources>

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-497
+492

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 1 error and 208 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 1 error and 205 warnings</span>