forked from GitHub-Mirror/riotX-android
316 lines
12 KiB
Kotlin
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"
|
|
}
|
|
}
|