diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 6bf7f984c..498a73678 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -453,6 +453,11 @@ public interface NcApi { @Url String url, @Body RequestBody body); + @POST + Observable setTypingStatusPrivacy(@Header("Authorization") String authorization, + @Url String url, + @Body RequestBody body); + @POST Observable searchContactsByPhoneNumber(@Header("Authorization") String authorization, @Url String url, diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 0a23661bf..a8a686be7 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -44,6 +44,7 @@ import android.media.MediaRecorder import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.CountDownTimer import android.os.Handler import android.os.Parcelable import android.os.SystemClock @@ -51,6 +52,7 @@ import android.provider.ContactsContract import android.provider.MediaStore import android.text.Editable import android.text.InputFilter +import android.text.SpannableStringBuilder import android.text.TextUtils import android.text.TextWatcher import android.util.Log @@ -60,6 +62,7 @@ import android.view.Menu import android.view.MenuItem import android.view.MotionEvent import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.LinearInterpolator @@ -75,6 +78,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.PermissionChecker import androidx.core.graphics.drawable.toBitmap +import androidx.core.text.bold import androidx.core.widget.doAfterTextChanged import androidx.emoji2.text.EmojiCompat import androidx.emoji2.widget.EmojiTextView @@ -124,7 +128,7 @@ import com.nextcloud.talk.callbacks.MentionAutocompleteCallback import com.nextcloud.talk.conversationinfo.ConversationInfoActivity import com.nextcloud.talk.conversationlist.ConversationsListActivity import com.nextcloud.talk.data.user.model.User -import com.nextcloud.talk.databinding.ControllerChatBinding +import com.nextcloud.talk.databinding.ActivityChatBinding import com.nextcloud.talk.events.UserMentionClickEvent import com.nextcloud.talk.events.WebSocketCommunicationEvent import com.nextcloud.talk.extensions.loadAvatarOrImagePreview @@ -144,12 +148,14 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.mention.Mention +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage import com.nextcloud.talk.polls.ui.PollCreateDialogFragment import com.nextcloud.talk.presenters.MentionAutocompletePresenter import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.shareditems.activities.SharedItemsActivity import com.nextcloud.talk.signaling.SignalingMessageReceiver +import com.nextcloud.talk.signaling.SignalingMessageSender import com.nextcloud.talk.translate.TranslateActivity import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet import com.nextcloud.talk.ui.dialog.AttachmentDialog @@ -231,7 +237,7 @@ class ChatActivity : var active = false - private lateinit var binding: ControllerChatBinding + private lateinit var binding: ActivityChatBinding @Inject lateinit var ncApi: NcApi @@ -278,7 +284,8 @@ class ChatActivity : private var conversationVideoMenuItem: MenuItem? = null private var conversationSharedItemsItem: MenuItem? = null - var webSocketInstance: WebSocketInstance? = null + private var webSocketInstance: WebSocketInstance? = null + private var signalingMessageSender: SignalingMessageSender? = null var getRoomInfoTimerHandler: Handler? = null var pastPreconditionFailed = false @@ -299,6 +306,9 @@ class ChatActivity : private var videoURI: Uri? = null + var typingTimer: CountDownTimer? = null + val typingParticipants = HashMap() + private val localParticipantMessageListener = object : SignalingMessageReceiver.LocalParticipantMessageListener { override fun onSwitchTo(token: String?) { if (token != null) { @@ -311,11 +321,34 @@ class ChatActivity : } } + private val conversationMessageListener = object : SignalingMessageReceiver.ConversationMessageListener { + override fun onStartTyping(session: String) { + if (!CapabilitiesUtilNew.isTypingStatusPrivate(conversationUser!!)) { + var name = webSocketInstance?.getDisplayNameForSession(session) + + if (name != null && !typingParticipants.contains(session)) { + if (name == "") { + name = context.resources?.getString(R.string.nc_guest)!! + } + typingParticipants[session] = name + updateTypingIndicator() + } + } + } + + override fun onStopTyping(session: String) { + if (!CapabilitiesUtilNew.isTypingStatusPrivate(conversationUser!!)) { + typingParticipants.remove(session) + updateTypingIndicator() + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) - binding = ControllerChatBinding.inflate(layoutInflater) + binding = ActivityChatBinding.inflate(layoutInflater) setupActionBar() setContentView(binding.root) setupSystemColors() @@ -398,6 +431,7 @@ class ChatActivity : setupWebsocket() webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener) + webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener) if (conversationUser?.userId != "?" && CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "mention-flag") @@ -496,6 +530,8 @@ class ChatActivity : @Suppress("Detekt.TooGenericExceptionCaught") override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + sendStartTypingMessage() + if (s.length >= lengthFilter) { binding?.messageInputView?.inputEditText?.error = String.format( Objects.requireNonNull(resources).getString(R.string.nc_limit_hit), @@ -872,6 +908,134 @@ class ChatActivity : } } + @Suppress("MagicNumber") + private fun updateTypingIndicator() { + fun ellipsize(text: String): String { + return DisplayUtils.ellipsize(text, TYPING_INDICATOR_MAX_NAME_LENGTH) + } + + val participantNames = ArrayList(typingParticipants.values) + + val typingString: SpannableStringBuilder + when (typingParticipants.size) { + 0 -> typingString = SpannableStringBuilder().append(binding.typingIndicator.text) + + // person1 is typing + 1 -> typingString = SpannableStringBuilder() + .bold { append(ellipsize(participantNames[0])) } + .append(WHITESPACE + context.resources?.getString(R.string.typing_is_typing)) + + // person1 and person2 are typing + 2 -> typingString = SpannableStringBuilder() + .bold { append(ellipsize(participantNames[0])) } + .append(WHITESPACE + context.resources?.getString(R.string.nc_common_and) + WHITESPACE) + .bold { append(ellipsize(participantNames[1])) } + .append(WHITESPACE + context.resources?.getString(R.string.typing_are_typing)) + + // person1, person2 and person3 are typing + 3 -> typingString = SpannableStringBuilder() + .bold { append(ellipsize(participantNames[0])) } + .append(COMMA) + .bold { append(ellipsize(participantNames[1])) } + .append(WHITESPACE + context.resources?.getString(R.string.nc_common_and) + WHITESPACE) + .bold { append(ellipsize(participantNames[2])) } + .append(WHITESPACE + context.resources?.getString(R.string.typing_are_typing)) + + // person1, person2, person3 and 1 other is typing + 4 -> typingString = SpannableStringBuilder() + .bold { append(participantNames[0]) } + .append(COMMA) + .bold { append(participantNames[1]) } + .append(COMMA) + .bold { append(participantNames[2]) } + .append(WHITESPACE + context.resources?.getString(R.string.typing_1_other)) + + // person1, person2, person3 and x others are typing + else -> { + val moreTypersAmount = typingParticipants.size - 3 + val othersTyping = context.resources?.getString(R.string.typing_x_others)?.let { + String.format(it, moreTypersAmount) + } + typingString = SpannableStringBuilder() + .bold { append(participantNames[0]) } + .append(COMMA) + .bold { append(participantNames[1]) } + .append(COMMA) + .bold { append(participantNames[2]) } + .append(othersTyping) + } + } + + runOnUiThread { + binding.typingIndicator.text = typingString + + if (participantNames.size > 0) { + binding.typingIndicatorWrapper.animate() + .translationY(binding.messageInputView.y - DisplayUtils.convertDpToPixel(18f, context)) + .setInterpolator(AccelerateDecelerateInterpolator()) + .duration = TYPING_INDICATOR_ANIMATION_DURATION + } else { + if (binding.typingIndicator.lineCount == 1) { + binding.typingIndicatorWrapper.animate() + .translationY(binding.messageInputView.y) + .setInterpolator(AccelerateDecelerateInterpolator()) + .duration = TYPING_INDICATOR_ANIMATION_DURATION + } else if (binding.typingIndicator.lineCount == 2) { + binding.typingIndicatorWrapper.animate() + .translationY(binding.messageInputView.y + DisplayUtils.convertDpToPixel(15f, context)) + .setInterpolator(AccelerateDecelerateInterpolator()) + .duration = TYPING_INDICATOR_ANIMATION_DURATION + } + } + } + } + + fun sendStartTypingMessage() { + if (webSocketInstance == null) { + return + } + + if (!CapabilitiesUtilNew.isTypingStatusPrivate(conversationUser!!)) { + if (typingTimer == null) { + for ((sessionId, participant) in webSocketInstance?.getUserMap()!!) { + val ncSignalingMessage = NCSignalingMessage() + ncSignalingMessage.to = sessionId + ncSignalingMessage.type = TYPING_STARTED_SIGNALING_MESSAGE_TYPE + signalingMessageSender!!.send(ncSignalingMessage) + } + + typingTimer = object : CountDownTimer( + TYPING_DURATION_BEFORE_SENDING_STOP, + TYPING_DURATION_BEFORE_SENDING_STOP + ) { + override fun onTick(millisUntilFinished: Long) { + // unused atm + } + + override fun onFinish() { + sendStopTypingMessage() + } + }.start() + } else { + typingTimer?.cancel() + typingTimer?.start() + } + } + } + + fun sendStopTypingMessage() { + if (!CapabilitiesUtilNew.isTypingStatusPrivate(conversationUser!!)) { + typingTimer = null + + for ((sessionId, participant) in webSocketInstance?.getUserMap()!!) { + val ncSignalingMessage = NCSignalingMessage() + ncSignalingMessage.to = sessionId + ncSignalingMessage.type = TYPING_STOPPED_SIGNALING_MESSAGE_TYPE + signalingMessageSender!!.send(ncSignalingMessage) + } + } + } + private fun getRoomInfo() { logConversationInfos("getRoomInfo") @@ -1980,6 +2144,7 @@ class ChatActivity : eventBus.unregister(this) webSocketInstance?.getSignalingMessageReceiver()?.removeListener(localParticipantMessageListener) + webSocketInstance?.getSignalingMessageReceiver()?.removeListener(conversationMessageListener) findViewById(R.id.toolbar)?.setOnClickListener(null) @@ -2228,6 +2393,7 @@ class ChatActivity : } binding?.messageInputView?.inputEditText?.setText("") + sendStopTypingMessage() val replyMessageId: Int? = findViewById(R.id.quotedChatMessageView)?.tag as Int? sendMessage( editable, @@ -2303,6 +2469,8 @@ class ChatActivity : if (webSocketInstance == null) { Log.d(TAG, "webSocketInstance not set up. This should only happen when not using the HPB") } + + signalingMessageSender = webSocketInstance?.signalingMessageSender } fun pullChatMessages( @@ -3627,5 +3795,13 @@ class ChatActivity : private const val LOOKING_INTO_FUTURE_TIMEOUT = 30 private const val CHUNK_SIZE: Int = 10 private const val ONE_SECOND_IN_MILLIS = 1000 + + private const val WHITESPACE = " " + private const val COMMA = ", " + private const val TYPING_INDICATOR_ANIMATION_DURATION = 200L + private const val TYPING_INDICATOR_MAX_NAME_LENGTH = 14 + private const val TYPING_DURATION_BEFORE_SENDING_STOP = 4000L + private const val TYPING_STARTED_SIGNALING_MESSAGE_TYPE = "startedTyping" + private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping" } } diff --git a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt index 2b2b1d720..55a0f9385 100644 --- a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt @@ -56,6 +56,7 @@ import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo import androidx.work.WorkManager import autodagger.AutoInjector import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -69,6 +70,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.setAppT import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivitySettingsBinding import com.nextcloud.talk.jobs.AccountRemovalWorker +import com.nextcloud.talk.jobs.CapabilitiesWorker import com.nextcloud.talk.jobs.ContactAddressBookWorker import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.checkPermission import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.deleteAll @@ -122,6 +124,7 @@ class SettingsActivity : BaseActivity() { private var screenLockTimeoutChangeListener: OnPreferenceValueChangedListener? = null private var themeChangeListener: OnPreferenceValueChangedListener? = null private var readPrivacyChangeListener: OnPreferenceValueChangedListener? = null + private var typingStatusChangeListener: OnPreferenceValueChangedListener? = null private var phoneBookIntegrationChangeListener: OnPreferenceValueChangedListener? = null private var profileQueryDisposable: Disposable? = null private var dbQueryDisposable: Disposable? = null @@ -172,6 +175,8 @@ class SettingsActivity : BaseActivity() { supportActionBar?.show() dispose(null) + loadCapabilitiesAndUpdateSettings() + binding.settingsVersion.setOnClickListener { sendLogs() } @@ -224,6 +229,19 @@ class SettingsActivity : BaseActivity() { themeSwitchPreferences() } + private fun loadCapabilitiesAndUpdateSettings() { + val capabilitiesWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java).build() + WorkManager.getInstance(context).enqueue(capabilitiesWork) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(capabilitiesWork.id) + .observe(this) { workInfo -> + if (workInfo?.state == WorkInfo.State.SUCCEEDED) { + getCurrentUser() + setupCheckables() + } + } + } + private fun setupActionBar() { setSupportActionBar(binding.settingsToolbar) binding.settingsToolbar.setNavigationOnClickListener { @@ -402,6 +420,11 @@ class SettingsActivity : BaseActivity() { readPrivacyChangeListener = it } ) + appPreferences.registerTypingStatusChangeListener( + TypingStatusChangeListener().also { + typingStatusChangeListener = it + } + ) } fun sendLogs() { @@ -470,6 +493,7 @@ class SettingsActivity : BaseActivity() { settingsIncognitoKeyboard, settingsPhoneBookIntegration, settingsReadPrivacy, + settingsTypingStatus, settingsProxyUseCredentials ).forEach(viewThemeUtils.talk::colorSwitchPreference) } @@ -636,13 +660,20 @@ class SettingsActivity : BaseActivity() { (binding.settingsIncognitoKeyboard.findViewById(R.id.mp_checkable) as Checkable).isChecked = appPreferences.isKeyboardIncognito - if (CapabilitiesUtilNew.isReadStatusAvailable(userManager.currentUser.blockingGet())) { + if (CapabilitiesUtilNew.isReadStatusAvailable(currentUser!!)) { (binding.settingsReadPrivacy.findViewById(R.id.mp_checkable) as Checkable).isChecked = !CapabilitiesUtilNew.isReadStatusPrivate(currentUser!!) } else { binding.settingsReadPrivacy.visibility = View.GONE } + if (CapabilitiesUtilNew.isTypingStatusAvailable(currentUser!!)) { + (binding.settingsTypingStatus.findViewById(R.id.mp_checkable) as Checkable).isChecked = + !CapabilitiesUtilNew.isTypingStatusPrivate(currentUser!!) + } else { + binding.settingsTypingStatus.visibility = View.GONE + } + (binding.settingsPhoneBookIntegration.findViewById(R.id.mp_checkable) as Checkable).isChecked = appPreferences.isPhoneBookIntegrationEnabled } @@ -680,6 +711,7 @@ class SettingsActivity : BaseActivity() { appPreferences.unregisterScreenLockTimeoutListener(screenLockTimeoutChangeListener) appPreferences.unregisterThemeChangeListener(themeChangeListener) appPreferences.unregisterReadPrivacyChangeListener(readPrivacyChangeListener) + appPreferences.unregisterTypingStatusChangeListener(typingStatusChangeListener) appPreferences.unregisterPhoneBookIntegrationChangeListener(phoneBookIntegrationChangeListener) super.onDestroy() @@ -1009,6 +1041,39 @@ class SettingsActivity : BaseActivity() { } } + private inner class TypingStatusChangeListener : OnPreferenceValueChangedListener { + override fun onChanged(newValue: Boolean) { + val booleanValue = if (newValue) "0" else "1" + val json = "{\"key\": \"typing_privacy\", \"value\" : $booleanValue}" + ncApi.setTypingStatusPrivacy( + ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token), + ApiUtils.getUrlForUserSettings(currentUser!!.baseUrl), + RequestBody.create("application/json".toMediaTypeOrNull(), json) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + // unused atm + } + + override fun onError(e: Throwable) { + appPreferences.setTypingStatus(!newValue) + (binding.settingsTypingStatus.findViewById(R.id.mp_checkable) as Checkable).isChecked = + !newValue + } + + override fun onComplete() { + // unused atm + } + }) + } + } + companion object { private const val TAG = "SettingsController" private const val DURATION: Long = 2500 diff --git a/app/src/main/java/com/nextcloud/talk/signaling/ConversationMessageNotifier.kt b/app/src/main/java/com/nextcloud/talk/signaling/ConversationMessageNotifier.kt new file mode 100644 index 000000000..6b8fac543 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/ConversationMessageNotifier.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.signaling + +import com.nextcloud.talk.signaling.SignalingMessageReceiver.ConversationMessageListener + +internal class ConversationMessageNotifier { + private val conversationMessageListeners: MutableSet = LinkedHashSet() + + @Synchronized + fun addListener(listener: ConversationMessageListener?) { + requireNotNull(listener) { "conversationMessageListener can not be null" } + conversationMessageListeners.add(listener) + } + + @Synchronized + fun removeListener(listener: ConversationMessageListener) { + conversationMessageListeners.remove(listener) + } + + @Synchronized + fun notifyStartTyping(sessionId: String?) { + for (listener in ArrayList(conversationMessageListeners)) { + listener.onStartTyping(sessionId) + } + } + + fun notifyStopTyping(sessionId: String?) { + for (listener in ArrayList(conversationMessageListeners)) { + listener.onStopTyping(sessionId) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java b/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java index 8853af425..1455b1ec5 100644 --- a/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java +++ b/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java @@ -50,6 +50,18 @@ import java.util.Map; */ public abstract class SignalingMessageReceiver { + private final ParticipantListMessageNotifier participantListMessageNotifier = new ParticipantListMessageNotifier(); + + private final LocalParticipantMessageNotifier localParticipantMessageNotifier = new LocalParticipantMessageNotifier(); + + private final CallParticipantMessageNotifier callParticipantMessageNotifier = new CallParticipantMessageNotifier(); + + private final ConversationMessageNotifier conversationMessageNotifier = new ConversationMessageNotifier(); + + private final OfferMessageNotifier offerMessageNotifier = new OfferMessageNotifier(); + + private final WebRtcMessageNotifier webRtcMessageNotifier = new WebRtcMessageNotifier(); + /** * Listener for participant list messages. * @@ -153,6 +165,14 @@ public abstract class SignalingMessageReceiver { void onUnshareScreen(); } + /** + * Listener for conversation messages. + */ + public interface ConversationMessageListener { + void onStartTyping(String session); + void onStopTyping(String session); + } + /** * Listener for WebRTC offers. * @@ -179,16 +199,6 @@ public abstract class SignalingMessageReceiver { void onEndOfCandidates(); } - private final ParticipantListMessageNotifier participantListMessageNotifier = new ParticipantListMessageNotifier(); - - private final LocalParticipantMessageNotifier localParticipantMessageNotifier = new LocalParticipantMessageNotifier(); - - private final CallParticipantMessageNotifier callParticipantMessageNotifier = new CallParticipantMessageNotifier(); - - private final OfferMessageNotifier offerMessageNotifier = new OfferMessageNotifier(); - - private final WebRtcMessageNotifier webRtcMessageNotifier = new WebRtcMessageNotifier(); - /** * Adds a listener for participant list messages. * @@ -236,6 +246,14 @@ public abstract class SignalingMessageReceiver { callParticipantMessageNotifier.removeListener(listener); } + public void addListener(ConversationMessageListener listener) { + conversationMessageNotifier.addListener(listener); + } + + public void removeListener(ConversationMessageListener listener) { + conversationMessageNotifier.removeListener(listener); + } + /** * Adds a listener for all offer messages. * @@ -563,6 +581,14 @@ public abstract class SignalingMessageReceiver { return; } + if ("startedTyping".equals(type)) { + conversationMessageNotifier.notifyStartTyping(sessionId); + } + + if ("stoppedTyping".equals(type)) { + conversationMessageNotifier.notifyStopTyping(sessionId); + } + if ("reaction".equals(type)) { // Message schema (external signaling server): // { diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index ceeea1aa5..aac979cba 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java @@ -553,4 +553,11 @@ public class DisplayUtils { DateFormat df = DateFormat.getDateTimeInstance(); return df.format(date); } + + public static String ellipsize(String text, int maxLength) { + if (text.length() > maxLength) { + return text.substring(0, maxLength - 1) + "…"; + } + return text; + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt index d91a4178f..d8ef399f9 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt @@ -98,7 +98,24 @@ object CapabilitiesUtilNew { return (map["read-privacy"]!!.toString()).toInt() == 1 } } + return false + } + fun isTypingStatusAvailable(user: User): Boolean { + if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) { + val map = user.capabilities!!.spreedCapability!!.config!!["chat"] + return map != null && map.containsKey("typing-privacy") + } + return false + } + + fun isTypingStatusPrivate(user: User): Boolean { + if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) { + val map = user.capabilities!!.spreedCapability!!.config!!["chat"] + if (map?.containsKey("typing-privacy") == true) { + return (map["typing-privacy"]!!.toString()).toInt() == 1 + } + } return false } diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java index 96b53631d..1170a5863 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java @@ -37,6 +37,7 @@ import net.orange_box.storebox.annotations.option.SaveOption; import net.orange_box.storebox.enums.SaveMode; import net.orange_box.storebox.listeners.OnPreferenceValueChangedListener; + @SaveOption(SaveMode.APPLY) public interface AppPreferences { @@ -312,6 +313,9 @@ public interface AppPreferences { @KeyByResource(R.string.nc_settings_read_privacy_key) void setReadPrivacy(boolean value); + + @KeyByString("typing_status") + void setTypingStatus(boolean value); @KeyByResource(R.string.nc_settings_read_privacy_key) @RegisterChangeListenerMethod @@ -321,6 +325,14 @@ public interface AppPreferences { @UnregisterChangeListenerMethod void unregisterReadPrivacyChangeListener(OnPreferenceValueChangedListener listener); + @KeyByString("typing_status") + @RegisterChangeListenerMethod + void registerTypingStatusChangeListener(OnPreferenceValueChangedListener listener); + + @KeyByString("typing_status") + @UnregisterChangeListenerMethod + void unregisterTypingStatusChangeListener(OnPreferenceValueChangedListener listener); + @KeyByResource(R.string.nc_file_browser_sort_by_key) void setSorting(String value); diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt b/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt index f66e2431a..6b249f18a 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt +++ b/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt @@ -206,6 +206,8 @@ class WebSocketInstance internal constructor( processRoomMessageMessage(eventOverallWebSocketMessage) } else if ("join" == eventOverallWebSocketMessage.eventMap!!["type"]) { processRoomJoinMessage(eventOverallWebSocketMessage) + } else if ("leave" == eventOverallWebSocketMessage.eventMap!!["type"]) { + processRoomLeaveMessage(eventOverallWebSocketMessage) } signalingMessageReceiver.process(eventOverallWebSocketMessage.eventMap) } @@ -271,6 +273,17 @@ class WebSocketInstance internal constructor( } } + private fun processRoomLeaveMessage(eventOverallWebSocketMessage: EventOverallWebSocketMessage) { + val leaveEventList = eventOverallWebSocketMessage.eventMap?.get("leave") as List? + for (i in leaveEventList!!.indices) { + usersHashMap.remove(leaveEventList[i]) + } + } + + fun getUserMap(): HashMap { + return usersHashMap + } + @Throws(IOException::class) private fun processJoinedRoomMessage(text: String) { val (_, roomWebSocketMessage) = LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage::class.java) diff --git a/app/src/main/res/layout/controller_chat.xml b/app/src/main/res/layout/activity_chat.xml similarity index 85% rename from app/src/main/res/layout/controller_chat.xml rename to app/src/main/res/layout/activity_chat.xml index 26adaf994..97f2d91ad 100644 --- a/app/src/main/res/layout/controller_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -30,7 +30,8 @@ android:layout_height="match_parent" android:animateLayoutChanges="true" android:background="@color/bg_default" - android:orientation="vertical"> + android:orientation="vertical" + tools:ignore="Overdraw"> + app:textAutoLink="all" + tools:visibility="visible"/> + + + + + + + + + - - + + Yes No + and Skip Set Dismiss @@ -150,6 +151,8 @@ How to translate with transifex: Locked Share my read-status and show the read-status of others Read status + Share my typing-status and show the typing-status of others + Typing status 30 seconds 1 minute @@ -447,6 +450,10 @@ How to translate with transifex: Open in Files app You are not allowed to share content to this chat + is typing … + are typing … + and 1 other is typing … + and %1$s others are typing … Add to conversation