Merge pull request #3029 from nextcloud/feature/2930/typingIndicators

✍️ Typing indicators
This commit is contained in:
Marcel Hibbe 2023-05-23 13:52:35 +02:00 committed by GitHub
commit aba34ed6a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 439 additions and 26 deletions

View file

@ -453,6 +453,11 @@ public interface NcApi {
@Url String url,
@Body RequestBody body);
@POST
Observable<GenericOverall> setTypingStatusPrivacy(@Header("Authorization") String authorization,
@Url String url,
@Body RequestBody body);
@POST
Observable<ContactsByNumberOverall> searchContactsByPhoneNumber(@Header("Authorization") String authorization,
@Url String url,

View file

@ -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<String, String>()
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>(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<View>(R.id.toolbar)?.setOnClickListener(null)
@ -2228,6 +2393,7 @@ class ChatActivity :
}
binding?.messageInputView?.inputEditText?.setText("")
sendStopTypingMessage()
val replyMessageId: Int? = findViewById<RelativeLayout>(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"
}
}

View file

@ -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<String?>? = null
private var themeChangeListener: OnPreferenceValueChangedListener<String?>? = null
private var readPrivacyChangeListener: OnPreferenceValueChangedListener<Boolean>? = null
private var typingStatusChangeListener: OnPreferenceValueChangedListener<Boolean>? = null
private var phoneBookIntegrationChangeListener: OnPreferenceValueChangedListener<Boolean>? = 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<View>(R.id.mp_checkable) as Checkable).isChecked =
appPreferences.isKeyboardIncognito
if (CapabilitiesUtilNew.isReadStatusAvailable(userManager.currentUser.blockingGet())) {
if (CapabilitiesUtilNew.isReadStatusAvailable(currentUser!!)) {
(binding.settingsReadPrivacy.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
!CapabilitiesUtilNew.isReadStatusPrivate(currentUser!!)
} else {
binding.settingsReadPrivacy.visibility = View.GONE
}
if (CapabilitiesUtilNew.isTypingStatusAvailable(currentUser!!)) {
(binding.settingsTypingStatus.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
!CapabilitiesUtilNew.isTypingStatusPrivate(currentUser!!)
} else {
binding.settingsTypingStatus.visibility = View.GONE
}
(binding.settingsPhoneBookIntegration.findViewById<View>(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<Boolean> {
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<GenericOverall> {
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<View>(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

View file

@ -0,0 +1,50 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2023 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.signaling
import com.nextcloud.talk.signaling.SignalingMessageReceiver.ConversationMessageListener
internal class ConversationMessageNotifier {
private val conversationMessageListeners: MutableSet<ConversationMessageListener> = 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)
}
}
}

View file

@ -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):
// {

View file

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

View file

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

View file

@ -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 {
@ -313,6 +314,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
void registerReadPrivacyChangeListener(OnPreferenceValueChangedListener<Boolean> listener);
@ -321,6 +325,14 @@ public interface AppPreferences {
@UnregisterChangeListenerMethod
void unregisterReadPrivacyChangeListener(OnPreferenceValueChangedListener<Boolean> listener);
@KeyByString("typing_status")
@RegisterChangeListenerMethod
void registerTypingStatusChangeListener(OnPreferenceValueChangedListener<Boolean> listener);
@KeyByString("typing_status")
@UnregisterChangeListenerMethod
void unregisterTypingStatusChangeListener(OnPreferenceValueChangedListener<Boolean> listener);
@KeyByResource(R.string.nc_file_browser_sort_by_key)
void setSorting(String value);

View file

@ -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<String>?
for (i in leaveEventList!!.indices) {
usersHashMap.remove(leaveEventList[i])
}
}
fun getUserMap(): HashMap<String?, Participant> {
return usersHashMap
}
@Throws(IOException::class)
private fun processJoinedRoomMessage(text: String) {
val (_, roomWebSocketMessage) = LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage::class.java)

View file

@ -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">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/chat_appbar"
@ -80,7 +81,8 @@
android:id="@+id/messagesListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="0dp"
android:paddingBottom="20dp"
android:clipToPadding="false"
android:visibility="gone"
app:dateHeaderTextSize="13sp"
app:incomingBubblePaddingBottom="@dimen/message_bubble_corners_vertical_padding"
@ -108,19 +110,20 @@
app:outcomingTextLinkColor="@color/high_emphasis_text"
app:outcomingTextSize="@dimen/chat_text_size"
app:outcomingTimeTextSize="12sp"
app:textAutoLink="all" />
app:textAutoLink="all"
tools:visibility="visible"/>
<com.nextcloud.ui.popupbubble.PopupBubble
android:id="@+id/popupBubbleView"
android:theme="@style/Button.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignBottom="@id/typing_indicator_wrapper"
android:layout_centerHorizontal="true"
android:layout_marginStart="64dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="64dp"
android:layout_marginBottom="16dp"
android:layout_marginBottom="26dp"
android:minHeight="@dimen/min_size_clickable_area"
android:layout_toStartOf="@+id/scrollDownButton"
android:text="@string/nc_new_messages"
@ -148,12 +151,13 @@
app:iconPadding="0dp"
app:iconSize="24dp" />
</RelativeLayout>
<LinearLayout
android:id="@+id/typing_indicator_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:layout_alignParentBottom="true"
android:layout_marginBottom="-19dp">
<View
android:id="@+id/separator_1"
@ -161,6 +165,29 @@
android:layout_height="1dp"
android:background="@color/controller_chat_separator" />
<TextView
android:id="@+id/typing_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"
android:layout_marginStart="@dimen/side_margin"
android:layout_marginEnd="@dimen/side_margin"
android:background="@color/bg_default"
android:textColor="@color/low_emphasis_text"
tools:text="Marcel is typing"
tools:ignore="Overdraw">
</TextView>
</LinearLayout>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.nextcloud.talk.ui.MessageInput
android:id="@+id/messageInputView"
android:layout_width="match_parent"

View file

@ -264,6 +264,14 @@
apc:mp_key="@string/nc_settings_read_privacy_key"
apc:mp_summary="@string/nc_settings_read_privacy_desc"
apc:mp_title="@string/nc_settings_read_privacy_title" />
<com.yarolegovich.mp.MaterialSwitchPreference
android:id="@+id/settings_typing_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
apc:mp_key="@string/nc_settings_read_privacy_key"
apc:mp_summary="@string/nc_settings_typing_status_desc"
apc:mp_title="@string/nc_settings_typing_status_title" />
</com.yarolegovich.mp.MaterialPreferenceCategory>
<com.yarolegovich.mp.MaterialPreferenceCategory

View file

@ -44,6 +44,7 @@ How to translate with transifex:
<!-- Common -->
<string name="nc_yes">Yes</string>
<string name="nc_no">No</string>
<string name="nc_common_and">and</string>
<string name="nc_common_skip">Skip</string>
<string name="nc_common_set">Set</string>
<string name="nc_common_dismiss">Dismiss</string>
@ -150,6 +151,8 @@ How to translate with transifex:
<string name="nc_locked">Locked</string>
<string name="nc_settings_read_privacy_desc">Share my read-status and show the read-status of others</string>
<string name="nc_settings_read_privacy_title">Read status</string>
<string name="nc_settings_typing_status_desc">Share my typing-status and show the typing-status of others</string>
<string name="nc_settings_typing_status_title">Typing status</string>
<string name="nc_screen_lock_timeout_30">30 seconds</string>
<string name="nc_screen_lock_timeout_60">1 minute</string>
@ -447,6 +450,10 @@ How to translate with transifex:
<string name="open_in_files_app">Open in Files app</string>
<string name="send_to_forbidden">You are not allowed to share content to this chat</string>
<string name="typing_is_typing">is typing …</string>
<string name="typing_are_typing">are typing …</string>
<string name="typing_1_other">and 1 other is typing …</string>
<string name="typing_x_others">and %1$s others are typing …</string>
<!-- Upload -->
<string name="nc_add_file">Add to conversation</string>