mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-20 12:09:45 +01:00
Merge pull request #3029 from nextcloud/feature/2930/typingIndicators
✍️ Typing indicators
This commit is contained in:
commit
aba34ed6a0
@ -453,6 +453,11 @@ public interface NcApi {
|
|||||||
@Url String url,
|
@Url String url,
|
||||||
@Body RequestBody body);
|
@Body RequestBody body);
|
||||||
|
|
||||||
|
@POST
|
||||||
|
Observable<GenericOverall> setTypingStatusPrivacy(@Header("Authorization") String authorization,
|
||||||
|
@Url String url,
|
||||||
|
@Body RequestBody body);
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
Observable<ContactsByNumberOverall> searchContactsByPhoneNumber(@Header("Authorization") String authorization,
|
Observable<ContactsByNumberOverall> searchContactsByPhoneNumber(@Header("Authorization") String authorization,
|
||||||
@Url String url,
|
@Url String url,
|
||||||
|
@ -44,6 +44,7 @@ import android.media.MediaRecorder
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.CountDownTimer
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
@ -51,6 +52,7 @@ import android.provider.ContactsContract
|
|||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -60,6 +62,7 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.view.animation.AlphaAnimation
|
import android.view.animation.AlphaAnimation
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.view.animation.LinearInterpolator
|
import android.view.animation.LinearInterpolator
|
||||||
@ -75,6 +78,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.content.PermissionChecker
|
import androidx.core.content.PermissionChecker
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import androidx.core.text.bold
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import androidx.emoji2.text.EmojiCompat
|
import androidx.emoji2.text.EmojiCompat
|
||||||
import androidx.emoji2.widget.EmojiTextView
|
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.conversationinfo.ConversationInfoActivity
|
||||||
import com.nextcloud.talk.conversationlist.ConversationsListActivity
|
import com.nextcloud.talk.conversationlist.ConversationsListActivity
|
||||||
import com.nextcloud.talk.data.user.model.User
|
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.UserMentionClickEvent
|
||||||
import com.nextcloud.talk.events.WebSocketCommunicationEvent
|
import com.nextcloud.talk.events.WebSocketCommunicationEvent
|
||||||
import com.nextcloud.talk.extensions.loadAvatarOrImagePreview
|
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.conversations.RoomsOverall
|
||||||
import com.nextcloud.talk.models.json.generic.GenericOverall
|
import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||||
import com.nextcloud.talk.models.json.mention.Mention
|
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.polls.ui.PollCreateDialogFragment
|
||||||
import com.nextcloud.talk.presenters.MentionAutocompletePresenter
|
import com.nextcloud.talk.presenters.MentionAutocompletePresenter
|
||||||
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
|
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
|
||||||
import com.nextcloud.talk.repositories.reactions.ReactionsRepository
|
import com.nextcloud.talk.repositories.reactions.ReactionsRepository
|
||||||
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
|
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
|
||||||
import com.nextcloud.talk.signaling.SignalingMessageReceiver
|
import com.nextcloud.talk.signaling.SignalingMessageReceiver
|
||||||
|
import com.nextcloud.talk.signaling.SignalingMessageSender
|
||||||
import com.nextcloud.talk.translate.TranslateActivity
|
import com.nextcloud.talk.translate.TranslateActivity
|
||||||
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
|
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
|
||||||
import com.nextcloud.talk.ui.dialog.AttachmentDialog
|
import com.nextcloud.talk.ui.dialog.AttachmentDialog
|
||||||
@ -231,7 +237,7 @@ class ChatActivity :
|
|||||||
|
|
||||||
var active = false
|
var active = false
|
||||||
|
|
||||||
private lateinit var binding: ControllerChatBinding
|
private lateinit var binding: ActivityChatBinding
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var ncApi: NcApi
|
lateinit var ncApi: NcApi
|
||||||
@ -278,7 +284,8 @@ class ChatActivity :
|
|||||||
private var conversationVideoMenuItem: MenuItem? = null
|
private var conversationVideoMenuItem: MenuItem? = null
|
||||||
private var conversationSharedItemsItem: 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 getRoomInfoTimerHandler: Handler? = null
|
||||||
var pastPreconditionFailed = false
|
var pastPreconditionFailed = false
|
||||||
@ -299,6 +306,9 @@ class ChatActivity :
|
|||||||
|
|
||||||
private var videoURI: Uri? = null
|
private var videoURI: Uri? = null
|
||||||
|
|
||||||
|
var typingTimer: CountDownTimer? = null
|
||||||
|
val typingParticipants = HashMap<String, String>()
|
||||||
|
|
||||||
private val localParticipantMessageListener = object : SignalingMessageReceiver.LocalParticipantMessageListener {
|
private val localParticipantMessageListener = object : SignalingMessageReceiver.LocalParticipantMessageListener {
|
||||||
override fun onSwitchTo(token: String?) {
|
override fun onSwitchTo(token: String?) {
|
||||||
if (token != null) {
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||||
|
|
||||||
binding = ControllerChatBinding.inflate(layoutInflater)
|
binding = ActivityChatBinding.inflate(layoutInflater)
|
||||||
setupActionBar()
|
setupActionBar()
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
setupSystemColors()
|
setupSystemColors()
|
||||||
@ -398,6 +431,7 @@ class ChatActivity :
|
|||||||
|
|
||||||
setupWebsocket()
|
setupWebsocket()
|
||||||
webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener)
|
webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener)
|
||||||
|
webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
|
||||||
|
|
||||||
if (conversationUser?.userId != "?" &&
|
if (conversationUser?.userId != "?" &&
|
||||||
CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "mention-flag")
|
CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "mention-flag")
|
||||||
@ -496,6 +530,8 @@ class ChatActivity :
|
|||||||
|
|
||||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||||
|
sendStartTypingMessage()
|
||||||
|
|
||||||
if (s.length >= lengthFilter) {
|
if (s.length >= lengthFilter) {
|
||||||
binding?.messageInputView?.inputEditText?.error = String.format(
|
binding?.messageInputView?.inputEditText?.error = String.format(
|
||||||
Objects.requireNonNull<Resources>(resources).getString(R.string.nc_limit_hit),
|
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() {
|
private fun getRoomInfo() {
|
||||||
logConversationInfos("getRoomInfo")
|
logConversationInfos("getRoomInfo")
|
||||||
|
|
||||||
@ -1980,6 +2144,7 @@ class ChatActivity :
|
|||||||
eventBus.unregister(this)
|
eventBus.unregister(this)
|
||||||
|
|
||||||
webSocketInstance?.getSignalingMessageReceiver()?.removeListener(localParticipantMessageListener)
|
webSocketInstance?.getSignalingMessageReceiver()?.removeListener(localParticipantMessageListener)
|
||||||
|
webSocketInstance?.getSignalingMessageReceiver()?.removeListener(conversationMessageListener)
|
||||||
|
|
||||||
findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
|
findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
|
||||||
|
|
||||||
@ -2228,6 +2393,7 @@ class ChatActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding?.messageInputView?.inputEditText?.setText("")
|
binding?.messageInputView?.inputEditText?.setText("")
|
||||||
|
sendStopTypingMessage()
|
||||||
val replyMessageId: Int? = findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.tag as Int?
|
val replyMessageId: Int? = findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.tag as Int?
|
||||||
sendMessage(
|
sendMessage(
|
||||||
editable,
|
editable,
|
||||||
@ -2303,6 +2469,8 @@ class ChatActivity :
|
|||||||
if (webSocketInstance == null) {
|
if (webSocketInstance == null) {
|
||||||
Log.d(TAG, "webSocketInstance not set up. This should only happen when not using the HPB")
|
Log.d(TAG, "webSocketInstance not set up. This should only happen when not using the HPB")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signalingMessageSender = webSocketInstance?.signalingMessageSender
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullChatMessages(
|
fun pullChatMessages(
|
||||||
@ -3627,5 +3795,13 @@ class ChatActivity :
|
|||||||
private const val LOOKING_INTO_FUTURE_TIMEOUT = 30
|
private const val LOOKING_INTO_FUTURE_TIMEOUT = 30
|
||||||
private const val CHUNK_SIZE: Int = 10
|
private const val CHUNK_SIZE: Int = 10
|
||||||
private const val ONE_SECOND_IN_MILLIS = 1000
|
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.content.ContextCompat
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import autodagger.AutoInjector
|
import autodagger.AutoInjector
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
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.data.user.model.User
|
||||||
import com.nextcloud.talk.databinding.ActivitySettingsBinding
|
import com.nextcloud.talk.databinding.ActivitySettingsBinding
|
||||||
import com.nextcloud.talk.jobs.AccountRemovalWorker
|
import com.nextcloud.talk.jobs.AccountRemovalWorker
|
||||||
|
import com.nextcloud.talk.jobs.CapabilitiesWorker
|
||||||
import com.nextcloud.talk.jobs.ContactAddressBookWorker
|
import com.nextcloud.talk.jobs.ContactAddressBookWorker
|
||||||
import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.checkPermission
|
import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.checkPermission
|
||||||
import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.deleteAll
|
import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.deleteAll
|
||||||
@ -122,6 +124,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
private var screenLockTimeoutChangeListener: OnPreferenceValueChangedListener<String?>? = null
|
private var screenLockTimeoutChangeListener: OnPreferenceValueChangedListener<String?>? = null
|
||||||
private var themeChangeListener: OnPreferenceValueChangedListener<String?>? = null
|
private var themeChangeListener: OnPreferenceValueChangedListener<String?>? = null
|
||||||
private var readPrivacyChangeListener: OnPreferenceValueChangedListener<Boolean>? = null
|
private var readPrivacyChangeListener: OnPreferenceValueChangedListener<Boolean>? = null
|
||||||
|
private var typingStatusChangeListener: OnPreferenceValueChangedListener<Boolean>? = null
|
||||||
private var phoneBookIntegrationChangeListener: OnPreferenceValueChangedListener<Boolean>? = null
|
private var phoneBookIntegrationChangeListener: OnPreferenceValueChangedListener<Boolean>? = null
|
||||||
private var profileQueryDisposable: Disposable? = null
|
private var profileQueryDisposable: Disposable? = null
|
||||||
private var dbQueryDisposable: Disposable? = null
|
private var dbQueryDisposable: Disposable? = null
|
||||||
@ -172,6 +175,8 @@ class SettingsActivity : BaseActivity() {
|
|||||||
supportActionBar?.show()
|
supportActionBar?.show()
|
||||||
dispose(null)
|
dispose(null)
|
||||||
|
|
||||||
|
loadCapabilitiesAndUpdateSettings()
|
||||||
|
|
||||||
binding.settingsVersion.setOnClickListener {
|
binding.settingsVersion.setOnClickListener {
|
||||||
sendLogs()
|
sendLogs()
|
||||||
}
|
}
|
||||||
@ -224,6 +229,19 @@ class SettingsActivity : BaseActivity() {
|
|||||||
themeSwitchPreferences()
|
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() {
|
private fun setupActionBar() {
|
||||||
setSupportActionBar(binding.settingsToolbar)
|
setSupportActionBar(binding.settingsToolbar)
|
||||||
binding.settingsToolbar.setNavigationOnClickListener {
|
binding.settingsToolbar.setNavigationOnClickListener {
|
||||||
@ -402,6 +420,11 @@ class SettingsActivity : BaseActivity() {
|
|||||||
readPrivacyChangeListener = it
|
readPrivacyChangeListener = it
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
appPreferences.registerTypingStatusChangeListener(
|
||||||
|
TypingStatusChangeListener().also {
|
||||||
|
typingStatusChangeListener = it
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendLogs() {
|
fun sendLogs() {
|
||||||
@ -470,6 +493,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
settingsIncognitoKeyboard,
|
settingsIncognitoKeyboard,
|
||||||
settingsPhoneBookIntegration,
|
settingsPhoneBookIntegration,
|
||||||
settingsReadPrivacy,
|
settingsReadPrivacy,
|
||||||
|
settingsTypingStatus,
|
||||||
settingsProxyUseCredentials
|
settingsProxyUseCredentials
|
||||||
).forEach(viewThemeUtils.talk::colorSwitchPreference)
|
).forEach(viewThemeUtils.talk::colorSwitchPreference)
|
||||||
}
|
}
|
||||||
@ -636,13 +660,20 @@ class SettingsActivity : BaseActivity() {
|
|||||||
(binding.settingsIncognitoKeyboard.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
|
(binding.settingsIncognitoKeyboard.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
|
||||||
appPreferences.isKeyboardIncognito
|
appPreferences.isKeyboardIncognito
|
||||||
|
|
||||||
if (CapabilitiesUtilNew.isReadStatusAvailable(userManager.currentUser.blockingGet())) {
|
if (CapabilitiesUtilNew.isReadStatusAvailable(currentUser!!)) {
|
||||||
(binding.settingsReadPrivacy.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
|
(binding.settingsReadPrivacy.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
|
||||||
!CapabilitiesUtilNew.isReadStatusPrivate(currentUser!!)
|
!CapabilitiesUtilNew.isReadStatusPrivate(currentUser!!)
|
||||||
} else {
|
} else {
|
||||||
binding.settingsReadPrivacy.visibility = View.GONE
|
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 =
|
(binding.settingsPhoneBookIntegration.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
|
||||||
appPreferences.isPhoneBookIntegrationEnabled
|
appPreferences.isPhoneBookIntegrationEnabled
|
||||||
}
|
}
|
||||||
@ -680,6 +711,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
appPreferences.unregisterScreenLockTimeoutListener(screenLockTimeoutChangeListener)
|
appPreferences.unregisterScreenLockTimeoutListener(screenLockTimeoutChangeListener)
|
||||||
appPreferences.unregisterThemeChangeListener(themeChangeListener)
|
appPreferences.unregisterThemeChangeListener(themeChangeListener)
|
||||||
appPreferences.unregisterReadPrivacyChangeListener(readPrivacyChangeListener)
|
appPreferences.unregisterReadPrivacyChangeListener(readPrivacyChangeListener)
|
||||||
|
appPreferences.unregisterTypingStatusChangeListener(typingStatusChangeListener)
|
||||||
appPreferences.unregisterPhoneBookIntegrationChangeListener(phoneBookIntegrationChangeListener)
|
appPreferences.unregisterPhoneBookIntegrationChangeListener(phoneBookIntegrationChangeListener)
|
||||||
|
|
||||||
super.onDestroy()
|
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 {
|
companion object {
|
||||||
private const val TAG = "SettingsController"
|
private const val TAG = "SettingsController"
|
||||||
private const val DURATION: Long = 2500
|
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 {
|
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.
|
* Listener for participant list messages.
|
||||||
*
|
*
|
||||||
@ -153,6 +165,14 @@ public abstract class SignalingMessageReceiver {
|
|||||||
void onUnshareScreen();
|
void onUnshareScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for conversation messages.
|
||||||
|
*/
|
||||||
|
public interface ConversationMessageListener {
|
||||||
|
void onStartTyping(String session);
|
||||||
|
void onStopTyping(String session);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener for WebRTC offers.
|
* Listener for WebRTC offers.
|
||||||
*
|
*
|
||||||
@ -179,16 +199,6 @@ public abstract class SignalingMessageReceiver {
|
|||||||
void onEndOfCandidates();
|
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.
|
* Adds a listener for participant list messages.
|
||||||
*
|
*
|
||||||
@ -236,6 +246,14 @@ public abstract class SignalingMessageReceiver {
|
|||||||
callParticipantMessageNotifier.removeListener(listener);
|
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.
|
* Adds a listener for all offer messages.
|
||||||
*
|
*
|
||||||
@ -563,6 +581,14 @@ public abstract class SignalingMessageReceiver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("startedTyping".equals(type)) {
|
||||||
|
conversationMessageNotifier.notifyStartTyping(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("stoppedTyping".equals(type)) {
|
||||||
|
conversationMessageNotifier.notifyStopTyping(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
if ("reaction".equals(type)) {
|
if ("reaction".equals(type)) {
|
||||||
// Message schema (external signaling server):
|
// Message schema (external signaling server):
|
||||||
// {
|
// {
|
||||||
|
@ -553,4 +553,11 @@ public class DisplayUtils {
|
|||||||
DateFormat df = DateFormat.getDateTimeInstance();
|
DateFormat df = DateFormat.getDateTimeInstance();
|
||||||
return df.format(date);
|
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 (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
|
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.enums.SaveMode;
|
||||||
import net.orange_box.storebox.listeners.OnPreferenceValueChangedListener;
|
import net.orange_box.storebox.listeners.OnPreferenceValueChangedListener;
|
||||||
|
|
||||||
|
|
||||||
@SaveOption(SaveMode.APPLY)
|
@SaveOption(SaveMode.APPLY)
|
||||||
public interface AppPreferences {
|
public interface AppPreferences {
|
||||||
|
|
||||||
@ -313,6 +314,9 @@ public interface AppPreferences {
|
|||||||
@KeyByResource(R.string.nc_settings_read_privacy_key)
|
@KeyByResource(R.string.nc_settings_read_privacy_key)
|
||||||
void setReadPrivacy(boolean value);
|
void setReadPrivacy(boolean value);
|
||||||
|
|
||||||
|
@KeyByString("typing_status")
|
||||||
|
void setTypingStatus(boolean value);
|
||||||
|
|
||||||
@KeyByResource(R.string.nc_settings_read_privacy_key)
|
@KeyByResource(R.string.nc_settings_read_privacy_key)
|
||||||
@RegisterChangeListenerMethod
|
@RegisterChangeListenerMethod
|
||||||
void registerReadPrivacyChangeListener(OnPreferenceValueChangedListener<Boolean> listener);
|
void registerReadPrivacyChangeListener(OnPreferenceValueChangedListener<Boolean> listener);
|
||||||
@ -321,6 +325,14 @@ public interface AppPreferences {
|
|||||||
@UnregisterChangeListenerMethod
|
@UnregisterChangeListenerMethod
|
||||||
void unregisterReadPrivacyChangeListener(OnPreferenceValueChangedListener<Boolean> listener);
|
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)
|
@KeyByResource(R.string.nc_file_browser_sort_by_key)
|
||||||
void setSorting(String value);
|
void setSorting(String value);
|
||||||
|
|
||||||
|
@ -206,6 +206,8 @@ class WebSocketInstance internal constructor(
|
|||||||
processRoomMessageMessage(eventOverallWebSocketMessage)
|
processRoomMessageMessage(eventOverallWebSocketMessage)
|
||||||
} else if ("join" == eventOverallWebSocketMessage.eventMap!!["type"]) {
|
} else if ("join" == eventOverallWebSocketMessage.eventMap!!["type"]) {
|
||||||
processRoomJoinMessage(eventOverallWebSocketMessage)
|
processRoomJoinMessage(eventOverallWebSocketMessage)
|
||||||
|
} else if ("leave" == eventOverallWebSocketMessage.eventMap!!["type"]) {
|
||||||
|
processRoomLeaveMessage(eventOverallWebSocketMessage)
|
||||||
}
|
}
|
||||||
signalingMessageReceiver.process(eventOverallWebSocketMessage.eventMap)
|
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)
|
@Throws(IOException::class)
|
||||||
private fun processJoinedRoomMessage(text: String) {
|
private fun processJoinedRoomMessage(text: String) {
|
||||||
val (_, roomWebSocketMessage) = LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage::class.java)
|
val (_, roomWebSocketMessage) = LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage::class.java)
|
||||||
|
@ -30,7 +30,8 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:animateLayoutChanges="true"
|
android:animateLayoutChanges="true"
|
||||||
android:background="@color/bg_default"
|
android:background="@color/bg_default"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical"
|
||||||
|
tools:ignore="Overdraw">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/chat_appbar"
|
android:id="@+id/chat_appbar"
|
||||||
@ -80,7 +81,8 @@
|
|||||||
android:id="@+id/messagesListView"
|
android:id="@+id/messagesListView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:paddingBottom="0dp"
|
android:paddingBottom="20dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:dateHeaderTextSize="13sp"
|
app:dateHeaderTextSize="13sp"
|
||||||
app:incomingBubblePaddingBottom="@dimen/message_bubble_corners_vertical_padding"
|
app:incomingBubblePaddingBottom="@dimen/message_bubble_corners_vertical_padding"
|
||||||
@ -108,19 +110,20 @@
|
|||||||
app:outcomingTextLinkColor="@color/high_emphasis_text"
|
app:outcomingTextLinkColor="@color/high_emphasis_text"
|
||||||
app:outcomingTextSize="@dimen/chat_text_size"
|
app:outcomingTextSize="@dimen/chat_text_size"
|
||||||
app:outcomingTimeTextSize="12sp"
|
app:outcomingTimeTextSize="12sp"
|
||||||
app:textAutoLink="all" />
|
app:textAutoLink="all"
|
||||||
|
tools:visibility="visible"/>
|
||||||
|
|
||||||
<com.nextcloud.ui.popupbubble.PopupBubble
|
<com.nextcloud.ui.popupbubble.PopupBubble
|
||||||
android:id="@+id/popupBubbleView"
|
android:id="@+id/popupBubbleView"
|
||||||
android:theme="@style/Button.Primary"
|
android:theme="@style/Button.Primary"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignBottom="@id/typing_indicator_wrapper"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:layout_marginStart="64dp"
|
android:layout_marginStart="64dp"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginEnd="64dp"
|
android:layout_marginEnd="64dp"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="26dp"
|
||||||
android:minHeight="@dimen/min_size_clickable_area"
|
android:minHeight="@dimen/min_size_clickable_area"
|
||||||
android:layout_toStartOf="@+id/scrollDownButton"
|
android:layout_toStartOf="@+id/scrollDownButton"
|
||||||
android:text="@string/nc_new_messages"
|
android:text="@string/nc_new_messages"
|
||||||
@ -148,6 +151,36 @@
|
|||||||
app:iconPadding="0dp"
|
app:iconPadding="0dp"
|
||||||
app:iconSize="24dp" />
|
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>
|
</RelativeLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -155,12 +188,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
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
|
<com.nextcloud.talk.ui.MessageInput
|
||||||
android:id="@+id/messageInputView"
|
android:id="@+id/messageInputView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
@ -264,6 +264,14 @@
|
|||||||
apc:mp_key="@string/nc_settings_read_privacy_key"
|
apc:mp_key="@string/nc_settings_read_privacy_key"
|
||||||
apc:mp_summary="@string/nc_settings_read_privacy_desc"
|
apc:mp_summary="@string/nc_settings_read_privacy_desc"
|
||||||
apc:mp_title="@string/nc_settings_read_privacy_title" />
|
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>
|
||||||
|
|
||||||
<com.yarolegovich.mp.MaterialPreferenceCategory
|
<com.yarolegovich.mp.MaterialPreferenceCategory
|
||||||
|
@ -44,6 +44,7 @@ How to translate with transifex:
|
|||||||
<!-- Common -->
|
<!-- Common -->
|
||||||
<string name="nc_yes">Yes</string>
|
<string name="nc_yes">Yes</string>
|
||||||
<string name="nc_no">No</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_skip">Skip</string>
|
||||||
<string name="nc_common_set">Set</string>
|
<string name="nc_common_set">Set</string>
|
||||||
<string name="nc_common_dismiss">Dismiss</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_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_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_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_30">30 seconds</string>
|
||||||
<string name="nc_screen_lock_timeout_60">1 minute</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="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="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 -->
|
<!-- Upload -->
|
||||||
<string name="nc_add_file">Add to conversation</string>
|
<string name="nc_add_file">Add to conversation</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user