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

add user status
This commit is contained in:
Marcel Hibbe 2022-02-25 13:09:35 +01:00 committed by GitHub
commit 2ccbbc5969
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 2979 additions and 538 deletions

View File

@ -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}"

View File

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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
/*
* Nextcloud Talk application
*
* @author Tobias Kaminsky
* Copyright (C) 2020 Tobias Kaminsky
* Copyright (C) 2020 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters
import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
interface PredefinedStatusClickListener {
fun onClick(predefinedStatus: PredefinedStatus)
}

View File

@ -0,0 +1,49 @@
/*
* Nextcloud Talk application
*
* @author Tobias Kaminsky
* Copyright (C) 2020 Tobias Kaminsky
* Copyright (C) 2020 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.talk.databinding.PredefinedStatusBinding
import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
class PredefinedStatusListAdapter(
private val clickListener: PredefinedStatusClickListener,
val context: Context
) : RecyclerView.Adapter<PredefinedStatusViewHolder>() {
internal var list: List<PredefinedStatus> = emptyList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PredefinedStatusViewHolder {
val itemBinding = PredefinedStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PredefinedStatusViewHolder(itemBinding)
}
override fun onBindViewHolder(holder: PredefinedStatusViewHolder, position: Int) {
holder.bind(list[position], clickListener, context)
}
override fun getItemCount(): Int {
return list.size
}
}

View File

@ -0,0 +1,58 @@
/*
* Nextcloud Talk application
*
* @author Tobias Kaminsky
* Copyright (C) 2020 Tobias Kaminsky
* Copyright (C) 2020 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.talk.R
import com.nextcloud.talk.databinding.PredefinedStatusBinding
import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
import com.nextcloud.talk.utils.DisplayUtils
private const val ONE_SECOND_IN_MILLIS = 1000
class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(status: PredefinedStatus, clickListener: PredefinedStatusClickListener, context: Context) {
binding.root.setOnClickListener { clickListener.onClick(status) }
binding.icon.text = status.icon
binding.name.text = status.message
if (status.clearAt == null) {
binding.clearAt.text = context.getString(R.string.dontClear)
} else {
val clearAt = status.clearAt!!
if (clearAt.type.equals("period")) {
binding.clearAt.text = DisplayUtils.getRelativeTimestamp(
context,
System.currentTimeMillis() + clearAt.time.toInt() * ONE_SECOND_IN_MILLIS,
true
)
} else {
// end-of
if (clearAt.time.equals("day")) {
binding.clearAt.text = context.getString(R.string.today)
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,110 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.mention;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
import org.parceler.Parcel;
@Parcel
@JsonObject
public class Mention {
@JsonField(name = "id")
String id;
@JsonField(name = "label")
String label;
// type of user (guests or users or calls)
@JsonField(name = "source")
String source;
public String getId() {
return this.id;
}
public String getLabel() {
return this.label;
}
public String getSource() {
return this.source;
}
public void setId(String id) {
this.id = id;
}
public void setLabel(String label) {
this.label = label;
}
public void setSource(String source) {
this.source = source;
}
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Mention)) {
return false;
}
final Mention other = (Mention) o;
if (!other.canEqual((Object) this)) {
return false;
}
final Object this$id = this.getId();
final Object other$id = other.getId();
if (this$id == null ? other$id != null : !this$id.equals(other$id)) {
return false;
}
final Object this$label = this.getLabel();
final Object other$label = other.getLabel();
if (this$label == null ? other$label != null : !this$label.equals(other$label)) {
return false;
}
final Object this$source = this.getSource();
final Object other$source = other.getSource();
return this$source == null ? other$source == null : this$source.equals(other$source);
}
protected boolean canEqual(final Object other) {
return other instanceof Mention;
}
public int hashCode() {
final int PRIME = 59;
int result = 1;
final Object $id = this.getId();
result = result * PRIME + ($id == null ? 43 : $id.hashCode());
final Object $label = this.getLabel();
result = result * PRIME + ($label == null ? 43 : $label.hashCode());
final Object $source = this.getSource();
result = result * PRIME + ($source == null ? 43 : $source.hashCode());
return result;
}
public String toString() {
return "Mention(id=" + this.getId() + ", label=" + this.getLabel() + ", source=" + this.getSource() + ")";
}
}

View File

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

View File

@ -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;

View File

@ -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")
}

View File

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

View File

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

View File

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

View File

@ -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");
}

View File

@ -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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,133 @@
/*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* @author Marcel Hibbe
* Copyright (C) 2020 Tobias Kaminsky
* Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
* Copyright (C) 2020 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import com.nextcloud.talk.R;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.res.ResourcesCompat;
/**
* A Drawable object that draws a status
*/
public class StatusDrawable extends Drawable {
private String text;
private @DrawableRes int icon = -1;
private Paint textPaint;
private int backgroundColor;
private final float radius;
private Context context;
public StatusDrawable(String status, String statusIcon, float statusSize, int backgroundColor, Context context) {
radius = statusSize;
this.backgroundColor = backgroundColor;
if ("dnd".equals(status)) {
icon = R.drawable.ic_user_status_dnd;
this.context = context;
} else if (TextUtils.isEmpty(statusIcon) && status != null) {
switch (status) {
case "online":
icon = R.drawable.online_status;
this.context = context;
break;
case "away":
icon = R.drawable.ic_user_status_away;
this.context = context;
break;
default:
// do not show
break;
}
} else {
text = statusIcon;
textPaint = new Paint();
textPaint.setTextSize(statusSize);
textPaint.setAntiAlias(true);
textPaint.setTextAlign(Paint.Align.CENTER);
}
}
/**
* Draw in its bounds (set via setBounds) respecting optional effects such as alpha (set via setAlpha) and color
* filter (set via setColorFilter) a circular background with a user's first character.
*
* @param canvas The canvas to draw into
*/
@Override
public void draw(@NonNull Canvas canvas) {
if (text != null) {
textPaint.setTextSize(1.6f * radius);
canvas.drawText(text, radius, radius - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint);
}
if (icon != -1) {
Paint backgroundPaint = new Paint();
backgroundPaint.setStyle(Paint.Style.FILL);
backgroundPaint.setAntiAlias(true);
backgroundPaint.setColor(backgroundColor);
canvas.drawCircle(radius, radius, radius, backgroundPaint);
Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), icon, null);
if (drawable != null) {
drawable.setBounds(0,
0,
(int) (2 * radius),
(int) (2 * radius));
drawable.draw(canvas);
}
}
}
@Override
public void setAlpha(int alpha) {
textPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
textPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,34 @@
<!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2019 Tobias Kaminsky
~ Copyright (C) 2019 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:tools="http://schemas.android.com/tools"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="VectorRaster">
<path
android:fillColor="#FF000000"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>

View File

@ -0,0 +1,32 @@
<!--
Nextcloud Android client application
@author Tobias Kaminsky
Copyright (C) 2020 Tobias Kaminsky
Copyright (C) 2020 Nextcloud GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
License as published by the Free Software Foundation; either
version 3 of the License, or any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU AFFERO GENERAL PUBLIC LICENSE for more details.
You should have received a copy of the GNU Affero General Public
License along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector xmlns:tools="http://schemas.android.com/tools"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="VectorRaster">
<path
android:fillColor="#f4a331"
android:pathData="m10.615,2.1094c-4.8491,0.6811 -8.6152,4.8615 -8.6152,9.8906 0,5.5 4.5,10 10,10 5.0292,0 9.2096,-3.7661 9.8906,-8.6152 -1.4654,1.601 -3.5625,2.6152 -5.8906,2.6152 -4.4,0 -8,-3.6 -8,-8 0,-2.3281 1.0143,-4.4252 2.6152,-5.8906z" />
</vector>

View File

@ -0,0 +1,38 @@
<!--
Nextcloud Android client application
@author Tobias Kaminsky
Copyright (C) 2020 Tobias Kaminsky
Copyright (C) 2020 Nextcloud GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
License as published by the Free Software Foundation; either
version 3 of the License, or any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU AFFERO GENERAL PUBLIC LICENSE for more details.
You should have received a copy of the GNU Affero General Public
License along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector xmlns:tools="http://schemas.android.com/tools"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="VectorRaster">
<path
android:fillColor="#ed484c"
android:pathData="m12,2c-5.52,0 -10,4.48 -10,10s4.48,10 10,10 10,-4.48 10,-10 -4.48,-10 -10,-10z" />
<path
android:fillColor="#fdffff"
android:pathData="m8,10h8c1.108,0 2,0.892 2,2s-0.892,2 -2,2h-8c-1.108,0 -2,-0.892 -2,-2s0.892,-2 2,-2z"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="2" />
</vector>

View File

@ -0,0 +1,34 @@
<!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2020 Tobias Kaminsky
~ Copyright (C) 2020 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:tools="http://schemas.android.com/tools"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="VectorRaster">
<path
android:fillColor="#000000"
android:pathData="m12,2c-5.52,0 -10,4.48 -10,10s4.48,10 10,10 10,-4.48 10,-10 -4.48,-10 -10,-10zM12,6a6,6 0,0 1,6 6,6 6,0 0,1 -6,6 6,6 0,0 1,-6 -6,6 6,0 0,1 6,-6z" />
</vector>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?><!--
Nextcloud Android client application
@author Andy Scherzinger
Copyright (C) 2019 Andy Scherzinger
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector xmlns:tools="http://schemas.android.com/tools"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="VectorRaster">
<path
android:fillColor="#49b382"
android:pathData="m12,2c-5.52,0 -10,4.48 -10,10s4.48,10 10,10 10,-4.48 10,-10 -4.48,-10 -10,-10z" />
</vector>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?><!--
Nextcloud Android client application
Copyright (C) 2020 Andy Scherzinger
Copyright (C) 2020 Tobias Kaminsky
Copyright (C) 2020 Nextcloud GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp">
<TextView
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="25sp"
tools:text="📆" />
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceListItem"
tools:text="In a meeting" />
<TextView
android:id="@+id/divider"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_margin="@dimen/standard_half_margin"
android:gravity="center_vertical"
android:text="@string/divider"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/clearAt"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
tools:text="an hour" />
</LinearLayout>

View File

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

View File

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

View File

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

View File

@ -1,77 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ @author Andy Scherzinger
~ Copyright (C) 2017-2018 Mario Danic
~ Copyright (C) 2017 Andy Scherzinger
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/item_height"
android:orientation="vertical">
<FrameLayout
android:id="@+id/frame_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/activity_horizontal_margin">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/simple_drawee_view"
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
app:roundAsCircle="true" />
</FrameLayout>
<LinearLayout
android:id="@+id/linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginStart="@dimen/margin_between_elements"
android:layout_marginEnd="@dimen/margin_between_elements"
android:layout_toEndOf="@id/frame_layout"
android:orientation="vertical">
<androidx.emoji.widget.EmojiTextView
android:id="@+id/name_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:singleLine="true"
android:textAlignment="viewStart"
android:textAppearance="@style/ListItem"
tools:text="Call item text" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/secondary_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="@color/textColorMaxContrast"
tools:text="A week ago" />
</LinearLayout>
</RelativeLayout>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
497
492

View File

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