mirror of
https://github.com/nextcloud/talk-android.git
synced 2024-11-24 22:15:41 +03:00
Merge pull request #3029 from nextcloud/feature/2930/typingIndicators
✍️ Typing indicators
This commit is contained in:
commit
aba34ed6a0
12 changed files with 439 additions and 26 deletions
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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):
|
||||
// {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,6 +151,36 @@
|
|||
app:iconPadding="0dp"
|
||||
app:iconSize="24dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/typing_indicator_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginBottom="-19dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/separator_1"
|
||||
android:layout_width="match_parent"
|
||||
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
|
||||
|
@ -155,12 +188,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/separator_1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/controller_chat_separator" />
|
||||
|
||||
<com.nextcloud.talk.ui.MessageInput
|
||||
android:id="@+id/messageInputView"
|
||||
android:layout_width="match_parent"
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue