BayernMessenger/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt

316 lines
12 KiB
Kotlin

/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.ui.views
import android.content.Context
import android.graphics.Color
import android.text.SpannableString
import android.text.TextPaint
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.R
import im.vector.riotx.core.error.ResourceLimitErrorFormatter
import im.vector.riotx.features.themes.ThemeUtils
import me.gujun.android.span.span
import me.saket.bettermovementmethod.BetterLinkMovementMethod
import timber.log.Timber
/**
* The view used to show some information about the room
* It does have a unique render method
*/
class NotificationAreaView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
@BindView(R.id.room_notification_icon)
lateinit var imageView: ImageView
@BindView(R.id.room_notification_message)
lateinit var messageView: TextView
var delegate: Delegate? = null
private var state: State = State.Initial
init {
setupView()
}
/**
* This methods is responsible for rendering the view according to the newState
*
* @param newState the newState representing the view
*/
fun render(newState: State) {
if (newState == state) {
Timber.d("State unchanged")
return
}
Timber.d("Rendering $newState")
cleanUp()
state = newState
when (newState) {
is State.Default -> renderDefault()
is State.Hidden -> renderHidden()
is State.Tombstone -> renderTombstone(newState)
is State.ResourceLimitExceededError -> renderResourceLimitExceededError(newState)
is State.ConnectionError -> renderConnectionError()
is State.Typing -> renderTyping(newState)
is State.UnreadPreview -> renderUnreadPreview()
is State.ScrollToBottom -> renderScrollToBottom(newState)
is State.UnsentEvents -> renderUnsent(newState)
}
}
// PRIVATE METHODS *****************************************************************************************************************************************
private fun setupView() {
inflate(context, R.layout.view_notification_area, this)
ButterKnife.bind(this)
}
private fun cleanUp() {
messageView.setOnClickListener(null)
imageView.setOnClickListener(null)
setBackgroundColor(Color.TRANSPARENT)
messageView.text = null
imageView.setImageResource(0)
}
private fun renderTombstone(state: State.Tombstone) {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.error)
val message = span {
+resources.getString(R.string.room_tombstone_versioned_description)
+"\n"
span(resources.getString(R.string.room_tombstone_continuation_link)) {
textDecorationLine = "underline"
onClick = { delegate?.onTombstoneEventClicked(state.tombstoneEvent) }
}
}
messageView.movementMethod = BetterLinkMovementMethod.getInstance()
messageView.text = message
}
private fun renderResourceLimitExceededError(state: State.ResourceLimitExceededError) {
visibility = View.VISIBLE
val resourceLimitErrorFormatter = ResourceLimitErrorFormatter(context)
val formatterMode: ResourceLimitErrorFormatter.Mode
val backgroundColor: Int
if (state.isSoft) {
backgroundColor = R.color.soft_resource_limit_exceeded
formatterMode = ResourceLimitErrorFormatter.Mode.Soft
} else {
backgroundColor = R.color.hard_resource_limit_exceeded
formatterMode = ResourceLimitErrorFormatter.Mode.Hard
}
val message = resourceLimitErrorFormatter.format(state.matrixError, formatterMode, clickable = true)
messageView.setTextColor(Color.WHITE)
messageView.text = message
messageView.movementMethod = LinkMovementMethod.getInstance()
messageView.setLinkTextColor(Color.WHITE)
setBackgroundColor(ContextCompat.getColor(context, backgroundColor))
}
private fun renderConnectionError() {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.error)
messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
messageView.text = SpannableString(resources.getString(R.string.room_offline_notification))
}
private fun renderTyping(state: State.Typing) {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.vector_typing)
messageView.text = SpannableString(state.message)
messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
}
private fun renderUnreadPreview() {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.scrolldown)
messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
imageView.setOnClickListener { delegate?.closeScreen() }
}
private fun renderScrollToBottom(state: State.ScrollToBottom) {
visibility = View.VISIBLE
if (state.unreadCount > 0) {
imageView.setImageResource(R.drawable.newmessages)
messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
messageView.text = SpannableString(resources.getQuantityString(R.plurals.room_new_messages_notification, state.unreadCount, state.unreadCount))
} else {
imageView.setImageResource(R.drawable.scrolldown)
messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
if (!TextUtils.isEmpty(state.message)) {
messageView.text = SpannableString(state.message)
}
}
messageView.setOnClickListener { delegate?.jumpToBottom() }
imageView.setOnClickListener { delegate?.jumpToBottom() }
}
private fun renderUnsent(state: State.UnsentEvents) {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.error)
val cancelAll = resources.getString(R.string.room_prompt_cancel)
val resendAll = resources.getString(R.string.room_prompt_resend)
val messageRes = if (state.hasUnknownDeviceEvents) R.string.room_unknown_devices_messages_notification else R.string.room_unsent_messages_notification
val message = context.getString(messageRes, resendAll, cancelAll)
val cancelAllPos = message.indexOf(cancelAll)
val resendAllPos = message.indexOf(resendAll)
val spannableString = SpannableString(message)
// cancelAllPos should always be > 0 but a GA crash reported here
if (cancelAllPos >= 0) {
spannableString.setSpan(CancelAllClickableSpan(), cancelAllPos, cancelAllPos + cancelAll.length, 0)
}
// resendAllPos should always be > 0 but a GA crash reported here
if (resendAllPos >= 0) {
spannableString.setSpan(ResendAllClickableSpan(), resendAllPos, resendAllPos + resendAll.length, 0)
}
messageView.movementMethod = LinkMovementMethod.getInstance()
messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
messageView.text = spannableString
}
private fun renderDefault() {
visibility = View.GONE
}
private fun renderHidden() {
visibility = View.GONE
}
/**
* Track the cancel all click.
*/
private inner class CancelAllClickableSpan : ClickableSpan() {
override fun onClick(widget: View) {
delegate?.deleteUnsentEvents()
render(state)
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color)
ds.bgColor = 0
ds.isUnderlineText = true
}
}
/**
* Track the resend all click.
*/
private inner class ResendAllClickableSpan : ClickableSpan() {
override fun onClick(widget: View) {
delegate?.resendUnsentEvents()
render(state)
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color)
ds.bgColor = 0
ds.isUnderlineText = true
}
}
/**
* The state representing the view
* It can take one state at a time
* Priority of state is managed in {@link VectorRoomActivity.refreshNotificationsArea() }
*/
sealed class State {
// Not yet rendered
object Initial : State()
// View will be Invisible
object Default : State()
// View will be Gone
object Hidden : State()
// Resource limit exceeded error will be displayed (only hard for the moment)
data class ResourceLimitExceededError(val isSoft: Boolean, val matrixError: MatrixError) : State()
// Server connection is lost
object ConnectionError : State()
// The room is dead
data class Tombstone(val tombstoneEvent: Event) : State()
// Somebody is typing
data class Typing(val message: String) : State()
// Some new messages are unread in preview
object UnreadPreview : State()
// Some new messages are unread (grey or red)
data class ScrollToBottom(val unreadCount: Int, val message: String? = null) : State()
// Some event has been unsent
data class UnsentEvents(val hasUndeliverableEvents: Boolean, val hasUnknownDeviceEvents: Boolean) : State()
}
/**
* An interface to delegate some actions to another object
*/
interface Delegate {
fun onTombstoneEventClicked(tombstoneEvent: Event)
fun resendUnsentEvents()
fun deleteUnsentEvents()
fun closeScreen()
fun jumpToBottom()
}
companion object {
/**
* Preference key.
*/
private const val SHOW_INFO_AREA_KEY = "SETTINGS_SHOW_INFO_AREA_KEY"
/**
* Always show the info area.
*/
private const val SHOW_INFO_AREA_VALUE_ALWAYS = "always"
/**
* Show the info area when it has messages or errors.
*/
private const val SHOW_INFO_AREA_VALUE_MESSAGES_AND_ERRORS = "messages_and_errors"
/**
* Show the info area only when it has errors.
*/
private const val SHOW_INFO_AREA_VALUE_ONLY_ERRORS = "only_errors"
}
}