diff --git a/app/build.gradle b/app/build.gradle index 6b894348c5..1f6568db9c 100644 --- a/app/build.gradle +++ b/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}" diff --git a/app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt b/app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt index ea55052db6..0989bffaa8 100644 --- a/app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt +++ b/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 }) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java index 36f6b9c697..f6d6850050 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java @@ -160,6 +160,9 @@ @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 @@ private void basicInitialization() { 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 @@ private void basicInitialization() { 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 @@ private void initGridAdapter() { 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 @@ private void initGridAdapter() { 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 void onGlobalLayout() { } }); - 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 @@ private void cameraInitialization() { //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 void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) { @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 void onNext(@io.reactivex.annotations.NonNull SignalingSettingsOverall si 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 @@ private void processMessage(NCSignalingMessage ncSignalingMessage) { 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 void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) { 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 @@ private void processUsersInRoom(List> users) { 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 @@ private void processUsersInRoom(List> users) { 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 @@ private void getPeersForCall() { 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() { @Override @@ -1790,7 +1822,8 @@ private void deletePeerConnection(PeerConnectionWrapper peerConnectionWrapper) { 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 @@ private PeerConnectionWrapper getPeerConnectionWrapperForSessionId(String sessio 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 @@ private void endPeerConnection(String sessionId, boolean justScreen) { 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 void onMessageEvent(ConfigurationChangeEvent configurationChangeEvent) { 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 @@ private void updateSelfVideoViewPosition() { 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 void onMessageEvent(SessionDescriptionSendEvent sessionDescriptionSend) t 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 void onRequestPermissionsResult(int requestCode, @NonNull String[] permis 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 boolean onTouch(View v, MotionEvent event) { @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 void updateUiForNormalMode() { 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(); diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java index dc3dc0889e..97b29f915a 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java @@ -215,7 +215,7 @@ private void checkIfAnyParticipantsRemainInRoom() { 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)) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusClickListener.kt b/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusClickListener.kt new file mode 100644 index 0000000000..ff25288a47 --- /dev/null +++ b/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 . + */ + +package com.nextcloud.talk.adapters + +import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus + +interface PredefinedStatusClickListener { + fun onClick(predefinedStatus: PredefinedStatus) +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusListAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusListAdapter.kt new file mode 100644 index 0000000000..48617748ea --- /dev/null +++ b/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 . + */ + +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() { + internal var list: List = 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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusViewHolder.kt new file mode 100644 index 0000000000..1dae3149fc --- /dev/null +++ b/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 . + */ + +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) + } + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java index d1eb5beb3b..19a0b9d4bf 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java +++ b/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 * 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.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.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 @@ public class AdvancedUserItem extends AbstractFlexibleItem implements IFilterable { - 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 UserItemViewHolder createViewHolder(View view, FlexibleAdapter adapter) { @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); } } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java index f63b1f3cfe..f81664e072 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java +++ b/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 * Copyright (C) 2021 Andy Scherzinger * Copyright (C) 2017-2018 Mario Danic * @@ -46,6 +48,9 @@ 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.viewholders.FlexibleViewHolder; public class ConversationItem extends AbstractFlexibleItem implements ISectionable, - IFilterable { + IFilterable { + 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 ConversationItemViewHolder createViewHolder(View view, FlexibleAdapter adapter, ConversationItemViewHolder holder, int position, List 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 void bindViewHolder(FlexibleAdapter adapter, ConversationItemV 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 void bindViewHolder(FlexibleAdapter adapter, ConversationItemV 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 void bindViewHolder(FlexibleAdapter adapter, ConversationItemV 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 void bindViewHolder(FlexibleAdapter adapter, ConversationItemV 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 void bindViewHolder(FlexibleAdapter adapter, ConversationItemV @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 @@ static class ConversationItemViewHolder extends FlexibleViewHolder { Chip dialogUnreadBubble; @BindView(R.id.favoriteConversationImageView) ImageView pinnedConversationImageView; + @BindView(R.id.user_status_image) + ImageView userStatusImage; ConversationItemViewHolder(View view, FlexibleAdapter adapter) { super(view, adapter); diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java index 2d1fec2911..4795851e77 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java +++ b/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 * * This program is free software: you can redistribute it and/or modify @@ -29,12 +31,17 @@ 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 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem implements IFilterable { + 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 boolean equals(Object o) { @Override public int getLayoutRes() { - return R.layout.rv_item_mention; + return R.layout.rv_item_conversation_info_participant; } @Override @@ -118,14 +132,14 @@ public void bindViewHolder( 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 void bindViewHolder( } 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 void bindViewHolder( 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 && diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.java index 3be3d30e39..78ce884c5d 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.java +++ b/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.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.viewholders.FlexibleViewHolder; public class UserItem extends AbstractFlexibleItem implements - ISectionable, IFilterable { + ISectionable, IFilterable { + 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 boolean equals(Object o) { 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 UserItemViewHolder createViewHolder(View view, FlexibleAdapter adapter) { 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 void bindViewHolder(FlexibleAdapter adapter, UserItemViewHolder holder, i } } + 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 void bindViewHolder(FlexibleAdapter adapter, UserItemViewHolder holder, i 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 void bindViewHolder(FlexibleAdapter adapter, UserItemViewHolder holder, i 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 @@ static class UserItemViewHolder extends FlexibleViewHolder { @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 @@ static class UserItemViewHolder extends FlexibleViewHolder { @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. diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 6f61c6f97f..f05910afae 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -40,6 +40,8 @@ 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 @@ Observable addParticipant(@Header("Authorization") String Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken */ @GET - Observable getPeersForCall(@Header("Authorization") String authorization, @Url String url); + Observable getPeersForCall(@Header("Authorization") String authorization, @Url String url, + @QueryMap Map fields); @FormUrlEncoded @POST @@ -333,7 +336,8 @@ Observable sendChatMessage(@Header("Authorization") String autho @GET Observable getMentionAutocompleteSuggestions(@Header("Authorization") String authorization, @Url String url, @Query("search") String query, - @Nullable @Query("limit") Integer limit); + @Nullable @Query("limit") Integer limit, + @QueryMap Map fields); // Url is: /api/{apiVersion}/room/{token}/pin @POST @@ -443,4 +447,42 @@ Observable setChatReadMarker(@Header("Authorization") String aut @GET Observable getOpenConversations(@Header("Authorization") String authorization, @Url String url); + + /* + * OCS Status API + */ + @GET + Observable status(@Header("Authorization") String authorization, @Url String url); + + @GET + Observable getPredefinedStatuses(@Header("Authorization") String authorization, @Url String url); + + @DELETE + Observable statusDeleteMessage(@Header("Authorization") String authorization, @Url String url); + + + @FormUrlEncoded + @PUT + Observable setPredefinedStatusMessage(@Header("Authorization") String authorization, + @Url String url, + @Field("messageId") String selectedPredefinedMessageId, + @Field("clearAt") Long clearAt); + + @FormUrlEncoded + @PUT + Observable setCustomStatusMessage(@Header("Authorization") String authorization, + @Url String url, + @Field("statusIcon") String statusIcon, + @Field("message") String message, + @Field("clearAt") Long clearAt); + + @FormUrlEncoded + @PUT + Observable setStatusType(@Header("Authorization") String authorization, + @Url String url, + @Field("statusType") String statusType); + + @GET + Observable getUserStatuses(@Header("Authorization") String authorization, @Url String url); + } diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt index 4c391d3b59..2ae6de462f 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/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) } diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.java index 95115fac7d..9486cd8df3 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.java +++ b/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 * Copyright (C) 2017-2018 Mario Danic * * This program is free software: you can redistribute it and/or modify @@ -24,20 +26,14 @@ 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 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.viewholders.FlexibleViewHolder; @AutoInjector(NextcloudTalkApplication.class) -public class BrowserFileItem extends AbstractFlexibleItem implements IFilterable { +public class BrowserFileItem extends AbstractFlexibleItem implements IFilterable { @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 int getLayoutRes() { } @Override - public ViewHolder createViewHolder(View view, FlexibleAdapter adapter) { - return new ViewHolder(view, adapter); - + public BrowserFileItemViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new BrowserFileItemViewHolder(view, adapter); } private boolean isSelected() { @@ -108,8 +102,11 @@ private void setSelected(boolean selected) { } @Override - public void bindViewHolder(FlexibleAdapter adapter, ViewHolder holder, int position, List payloads) { - holder.fileIconImageView.setController(null); + public void bindViewHolder(FlexibleAdapter adapter, + BrowserFileItemViewHolder holder, + int position, + List 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 void bindViewHolder(FlexibleAdapter adapter, ViewHolder holder } 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 void bindViewHolder(FlexibleAdapter adapter, ViewHolder holder .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 void onClick(View v) { }); } - holder.filenameTextView.setSelected(true); - holder.fileModifiedTextView.setSelected(true); + holder.binding.filenameTextView.setSelected(true); + holder.binding.fileModifiedInfo.setSelected(true); } @Override @@ -196,24 +197,13 @@ public boolean filter(String constraint) { 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); } } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java index 3adbb09034..1ca545dfce 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java @@ -551,6 +551,7 @@ public void onNext(ResponseBody responseBody) { } UserItem newContactItem = new UserItem( + getApplicationContext(), participant, currentUser, userHeaderItems.get(headerTitle) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt index 37ab07ee8e..5c0f77a60f 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/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 * Copyright (C) 2021 Andy Scherzinger (info@andy-scherzinger.de) * Copyright (C) 2017-2018 Mario Danic @@ -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? = null - private var recyclerViewItems: MutableList = ArrayList() + private var userItems: MutableList = 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() + 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() - for (userItem in recyclerViewItems) { + for (userItem in userItems) { if (userItem.model.getActorType() == USERS) { existingParticipantsId.add(userItem.model.getActorId()) } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java index 0c3b5f707b..c45430bed3 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java @@ -64,7 +64,6 @@ 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.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.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 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 userStatuses = new HashMap<>(); + public ConversationsListController(Bundle bundle) { super(); setHasOptionsMenu(true); @@ -473,6 +472,37 @@ public void showSearchView(MainActivity activity, SearchView searchView, MenuIte @SuppressLint("LongLogTag") public void fetchData() { + fetchUserStatuses(); + } + + private void fetchUserStatuses() { + ncApi.getUserStatuses(credentials, ApiUtils.getUrlForUserStatuses(currentUser.getBaseUrl())) + .subscribe(new Observer() { + @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 void fetchData() { 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 @@ private void fetchOpenConversations(int apiVersion){ conversation, currentUser, getActivity(), - callHeaderItems.get(headerTitle)); + callHeaderItems.get(headerTitle), + userStatuses.get(conversation.name)); openConversationItems.add(conversationItem); } diff --git a/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java b/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java index b5ba0a04bc..1babff115d 100644 --- a/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java +++ b/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java @@ -56,7 +56,7 @@ public static boolean hasExternalCapability(@Nullable UserEntity user, String ca 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 static boolean isReadStatusPrivate(@Nullable UserEntity user) { 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 { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt index 0bda480182..6682469b56 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt +++ b/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>?, @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) } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt new file mode 100644 index 0000000000..eda9cb399b --- /dev/null +++ b/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 + * Copyright (C) 2017-2019 Mario Danic + * + * 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 . + */ +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) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.java b/app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.java deleted file mode 100644 index 7770eb13f2..0000000000 --- a/app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2018 Mario Danic - * - * 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 . - */ -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() + ")"; - } -} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.kt b/app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.kt new file mode 100644 index 0000000000..470ba3c8f5 --- /dev/null +++ b/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 + * + * 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 . + */ +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("", "", "", "", "", "") +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.java b/app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.java index 36dbaa482b..9465e09361 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.java +++ b/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; diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/ClearAt.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/ClearAt.kt new file mode 100644 index 0000000000..34e3ea19cb --- /dev/null +++ b/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") +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/Status.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/Status.kt new file mode 100644 index 0000000000..f66a097247 --- /dev/null +++ b/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 + * + * 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 . + */ +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) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/StatusOCS.java b/app/src/main/java/com/nextcloud/talk/models/json/status/StatusOCS.java new file mode 100644 index 0000000000..620e1084a9 --- /dev/null +++ b/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 + * + * 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 . + */ +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 + + '}'; + } + +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/StatusOverall.java b/app/src/main/java/com/nextcloud/talk/models/json/status/StatusOverall.java new file mode 100644 index 0000000000..1107fc91cd --- /dev/null +++ b/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 + * + * 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 . + */ +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 + + '}'; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/StatusType.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/StatusType.kt new file mode 100644 index 0000000000..4e8eda9b45 --- /dev/null +++ b/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"); +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatus.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatus.kt new file mode 100644 index 0000000000..170d0cb9ad --- /dev/null +++ b/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) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOCS.kt new file mode 100644 index 0000000000..2f2b6682b8 --- /dev/null +++ b/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 . + */ + +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? +) : GenericOCS(), Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOverall.kt new file mode 100644 index 0000000000..030bd49265 --- /dev/null +++ b/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 + * Copyright (C) 2017-2018 Mario Danic + * + * 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 . + */ +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) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/statuses/StatusesOCS.java b/app/src/main/java/com/nextcloud/talk/models/json/statuses/StatusesOCS.java new file mode 100644 index 0000000000..31ad65e56c --- /dev/null +++ b/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 + * + * 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 . + */ +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 data; + + public List getData() { + return this.data; + } + + public void setData(List 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 + + '}'; + } + +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/statuses/StatusesOverall.java b/app/src/main/java/com/nextcloud/talk/models/json/statuses/StatusesOverall.java new file mode 100644 index 0000000000..b3a547ecb0 --- /dev/null +++ b/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 + * + * 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 . + */ +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 + + '}'; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java b/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java index a70f81f84f..56730f2078 100644 --- a/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java +++ b/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 * Copyright (C) 2017-2018 Mario Danic * @@ -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 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 @@ @AutoInjector(NextcloudTalkApplication.class) public class MentionAutocompletePresenter extends RecyclerViewPresenter implements FlexibleAdapter.OnItemClickListener { + private static final String TAG = "MentionAutocompletePresenter"; @Inject NcApi ncApi; @Inject @@ -88,6 +96,14 @@ protected RecyclerView.Adapter instantiateAdapter() { 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 @@ protected void onQuery(@Nullable CharSequence query) { int apiVersion = ApiUtils.getChatApiVersion(currentUser, new int[] {1}); adapter.setFilter(queryString); + + Map 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 void onNext(@NotNull MentionOverall mentionOverall) { for (Mention mention : mentionsList) { internalAbstractFlexibleItemList.add( new MentionAutocompleteItem( - mention.getId(), - mention.getLabel(), - mention.getSource(), + mention, currentUser, context)); } @@ -140,9 +158,11 @@ public void onNext(@NotNull MentionOverall mentionOverall) { } } + @SuppressLint("LongLogTag") @Override public void onError(@NotNull Throwable e) { adapter.clear(); + Log.e(TAG, "failed to get MentionAutocompleteSuggestions", e); } @Override diff --git a/app/src/main/java/com/nextcloud/talk/ui/StatusDrawable.java b/app/src/main/java/com/nextcloud/talk/ui/StatusDrawable.java new file mode 100644 index 0000000000..2582633279 --- /dev/null +++ b/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 . + */ + +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; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java index 2e72e943cf..52b9b869ee 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java +++ b/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 * Copyright (C) 2017 Mario Danic + * 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.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 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 adapter; private final List userItems = new ArrayList<>(); + private Status status; + @SuppressLint("InflateParams") @NonNull @Override @@ -106,24 +122,26 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { 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 void onViewCreated(@NonNull View view, Bundle savedInstanceState) { }); } + 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 void onViewCreated(@NonNull View view, Bundle savedInstanceState) { 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() { + + @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 void onDestroyView() { } 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() { @Override public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) { @@ -223,7 +287,7 @@ public void onNext(@io.reactivex.annotations.NonNull UserEntity userEntity) { userUtils.disableAllUsersWithoutId(userEntity.getId()); if (getActivity() != null) { getActivity().runOnUiThread( - () -> ((MainActivity) getActivity()).resetConversationsList()); + () -> ((MainActivity) getActivity()).resetConversationsList()); } dismiss(); } @@ -238,9 +302,30 @@ public void onComplete() { // 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); } - }; + } } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt new file mode 100644 index 0000000000..898564b8a3 --- /dev/null +++ b/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 . + */ + +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() + + 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 { + + 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(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 { + 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 { + 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 { + + 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 + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 01c5c7f743..89ed1f6679 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -416,4 +416,32 @@ public static String getUrlForHoverCard(String baseUrl, String userId) { return 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"; + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index 7309ad7019..efcd91436a 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java @@ -44,6 +44,7 @@ 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 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 @@ int getSortOrderStringId(FileSortOrder sortOrder) { 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); + } } diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000000..406f0b5f6f --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,34 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_user_status_away.xml b/app/src/main/res/drawable/ic_user_status_away.xml new file mode 100644 index 0000000000..ab5ca96420 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_away.xml @@ -0,0 +1,32 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user_status_dnd.xml b/app/src/main/res/drawable/ic_user_status_dnd.xml new file mode 100644 index 0000000000..27cfc1066a --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_dnd.xml @@ -0,0 +1,38 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_user_status_invisible.xml b/app/src/main/res/drawable/ic_user_status_invisible.xml new file mode 100644 index 0000000000..18a35e8e18 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_invisible.xml @@ -0,0 +1,34 @@ + + + + + diff --git a/app/src/main/res/drawable/online_status.xml b/app/src/main/res/drawable/online_status.xml new file mode 100644 index 0000000000..6d627e81de --- /dev/null +++ b/app/src/main/res/drawable/online_status.xml @@ -0,0 +1,32 @@ + + + + + diff --git a/app/src/main/res/drawable/online_status_with_border.xml b/app/src/main/res/drawable/online_status_with_border.xml new file mode 100644 index 0000000000..2b6fc8dcbe --- /dev/null +++ b/app/src/main/res/drawable/online_status_with_border.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/layout/current_account_item.xml b/app/src/main/res/layout/current_account_item.xml index 5e6ab4d572..680fb3a36e 100644 --- a/app/src/main/res/layout/current_account_item.xml +++ b/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" /> @@ -31,15 +32,52 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + app:layout_constraintTop_toBottomOf="@id/statusView" /> . +--> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/predefined_status.xml b/app/src/main/res/layout/predefined_status.xml new file mode 100644 index 0000000000..69e080c4ff --- /dev/null +++ b/app/src/main/res/layout/predefined_status.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/rv_item_contact.xml b/app/src/main/res/layout/rv_item_contact.xml index 57d6e68944..7bcefd7e79 100644 --- a/app/src/main/res/layout/rv_item_contact.xml +++ b/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" /> . --> - + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/standard_half_margin" + android:layout_marginTop="@dimen/standard_margin"> + + + 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"/> + + - + 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" /> - + - - + - + diff --git a/app/src/main/res/layout/rv_item_conversation_with_last_message.xml b/app/src/main/res/layout/rv_item_conversation_with_last_message.xml index 21d47f39b8..0428af2412 100644 --- a/app/src/main/res/layout/rv_item_conversation_with_last_message.xml +++ b/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" /> + - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 622e1a8543..ee62ad4ff0 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -38,6 +38,7 @@ #deffffff #99ffffff #61ffffff + #de000000 #121212 #99121212 @@ -65,4 +66,9 @@ #4B4B4B #282828 + + #222222 + #818181 + + #353535 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 8b059a897f..e67e5c6feb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -39,6 +39,7 @@ #de000000 #99000000 #61000000 + #deffffff #deffffff @@ -97,4 +98,11 @@ #99121212 + #eeeeee + #EEEEEE + + + #FFFFFF + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 2bcef5826c..753129d957 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -63,4 +63,10 @@ 180dp 110dp 0dp + + 52dp + 4dp + 16sp + 48dp + 2dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 345a31a829..0cc93d084b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -268,6 +268,28 @@ Remove group and members Pin: %1$s + + Set status + Online status + Status message + What is your status? + Clear status message after + Clear status message + Set status message + Online + Do not disturb + Away + Invisible + + 😃 + Don\'t clear + Today + 30 minutes + 1 hour + 4 hours + This week + seconds ago + Unread mentions Conversations @@ -435,8 +457,8 @@ Account not found Favorite + Status Encrypted - Password protected Avatar Account icon diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 89d41e79ba..d979260263 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -240,4 +240,12 @@ adjustResize + + diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index df2d2b6cee..4f09af7132 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -497 \ No newline at end of file +492 \ No newline at end of file diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index 9093325103..06528635e1 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 1 error and 208 warnings + Lint Report: 1 error and 205 warnings