Tombstone : add notification area and handle links
This commit is contained in:
parent
9e5c70dda3
commit
9a1e16a170
|
@ -26,8 +26,13 @@ import com.squareup.moshi.JsonClass
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MatrixError(
|
data class MatrixError(
|
||||||
@Json(name = "errcode") val code: String,
|
@Json(name = "errcode") val code: String,
|
||||||
@Json(name = "error") val message: String
|
@Json(name = "error") val message: String,
|
||||||
) {
|
|
||||||
|
@Json(name = "consent_uri") val consentUri: String? = null,
|
||||||
|
// RESOURCE_LIMIT_EXCEEDED data
|
||||||
|
@Json(name = "limit_type") val limitType: String? = null,
|
||||||
|
@Json(name = "admin_contact") val adminUri: String? = null) {
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val FORBIDDEN = "M_FORBIDDEN"
|
const val FORBIDDEN = "M_FORBIDDEN"
|
||||||
|
@ -55,5 +60,8 @@ data class MatrixError(
|
||||||
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
|
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
|
||||||
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
|
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
|
||||||
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||||
|
|
||||||
|
// Possible value for "limit_type"
|
||||||
|
const val LIMIT_TYPE_MAU = "monthly_active_user"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="42dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
tools:background="@color/vector_fuchsia_color"
|
||||||
|
tools:parentTag="android.widget.RelativeLayout">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/room_notification_icon"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:padding="5dp"
|
||||||
|
tools:src="@drawable/vector_typing" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/room_notification_message"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="64dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:accessibilityLiveRegion="polite"
|
||||||
|
android:textColor="?attr/vctr_room_notification_text_color"
|
||||||
|
tools:text="a text here" />
|
||||||
|
|
||||||
|
</merge>
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* 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.error
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.Html
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import im.vector.matrix.android.api.failure.MatrixError
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import me.gujun.android.span.span
|
||||||
|
|
||||||
|
class ResourceLimitErrorFormatter(private val context: Context) {
|
||||||
|
|
||||||
|
// 'hard' if the logged in user has been locked out, 'soft' if they haven't
|
||||||
|
sealed class Mode(@StringRes val mauErrorRes: Int, @StringRes val defaultErrorRes: Int, @StringRes val contactRes: Int) {
|
||||||
|
// User can still send message (will be used in a near future)
|
||||||
|
object Soft : Mode(R.string.resource_limit_soft_mau, R.string.resource_limit_soft_default, R.string.resource_limit_soft_contact)
|
||||||
|
|
||||||
|
// User cannot send message anymore
|
||||||
|
object Hard : Mode(R.string.resource_limit_hard_mau, R.string.resource_limit_hard_default, R.string.resource_limit_hard_contact)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun format(matrixError: MatrixError,
|
||||||
|
mode: Mode,
|
||||||
|
separator: CharSequence = " ",
|
||||||
|
clickable: Boolean = false): CharSequence {
|
||||||
|
val error = if (MatrixError.LIMIT_TYPE_MAU == matrixError.limitType) {
|
||||||
|
context.getString(mode.mauErrorRes)
|
||||||
|
} else {
|
||||||
|
context.getString(mode.defaultErrorRes)
|
||||||
|
}
|
||||||
|
val contact = if (clickable && matrixError.adminUri != null) {
|
||||||
|
val contactSubString = uriAsLink(matrixError.adminUri!!)
|
||||||
|
val contactFullString = context.getString(mode.contactRes, contactSubString)
|
||||||
|
Html.fromHtml(contactFullString)
|
||||||
|
} else {
|
||||||
|
val contactSubString = context.getString(R.string.resource_limit_contact_admin)
|
||||||
|
context.getString(mode.contactRes, contactSubString)
|
||||||
|
}
|
||||||
|
return span {
|
||||||
|
text = error
|
||||||
|
}
|
||||||
|
.append(separator)
|
||||||
|
.append(contact)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a HTML link with a uri
|
||||||
|
*/
|
||||||
|
private fun uriAsLink(uri: String): String {
|
||||||
|
val contactStr = context.getString(R.string.resource_limit_contact_admin)
|
||||||
|
return "<a href=\"$uri\">$contactStr</a>"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,324 @@
|
||||||
|
/*
|
||||||
|
* 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.platform
|
||||||
|
|
||||||
|
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 androidx.core.text.bold
|
||||||
|
import butterknife.BindView
|
||||||
|
import butterknife.ButterKnife
|
||||||
|
import im.vector.matrix.android.api.failure.MatrixError
|
||||||
|
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
||||||
|
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||||
|
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.error.ResourceLimitErrorFormatter
|
||||||
|
import im.vector.riotx.features.themes.ThemeUtils
|
||||||
|
import me.gujun.android.span.addSpan
|
||||||
|
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) {
|
||||||
|
val roomTombstoneContent = state.tombstoneContent
|
||||||
|
val roomLink = PermalinkFactory.createPermalink(roomTombstoneContent.replacementRoom)
|
||||||
|
?: return
|
||||||
|
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
imageView.setImageResource(R.drawable.error)
|
||||||
|
val textColorInt = ThemeUtils.getColor(context, R.attr.vctr_message_text_color)
|
||||||
|
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?.onUrlClicked(roomLink) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 tombstoneContent: RoomTombstoneContent) : 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 onUrlClicked(url: String)
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -76,6 +76,7 @@ import im.vector.riotx.core.extensions.observeEvent
|
||||||
import im.vector.riotx.core.extensions.setTextOrHide
|
import im.vector.riotx.core.extensions.setTextOrHide
|
||||||
import im.vector.riotx.core.files.addEntryToDownloadManager
|
import im.vector.riotx.core.files.addEntryToDownloadManager
|
||||||
import im.vector.riotx.core.glide.GlideApp
|
import im.vector.riotx.core.glide.GlideApp
|
||||||
|
import im.vector.riotx.core.platform.NotificationAreaView
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotx.core.utils.*
|
import im.vector.riotx.core.utils.*
|
||||||
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
||||||
|
@ -203,6 +204,7 @@ class RoomDetailFragment :
|
||||||
setupComposer()
|
setupComposer()
|
||||||
setupAttachmentButton()
|
setupAttachmentButton()
|
||||||
setupInviteView()
|
setupInviteView()
|
||||||
|
setupNotificationView()
|
||||||
roomDetailViewModel.subscribe { renderState(it) }
|
roomDetailViewModel.subscribe { renderState(it) }
|
||||||
textComposerViewModel.subscribe { renderTextComposerState(it) }
|
textComposerViewModel.subscribe { renderTextComposerState(it) }
|
||||||
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
|
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
|
||||||
|
@ -239,6 +241,36 @@ class RoomDetailFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupNotificationView() {
|
||||||
|
notificationAreaView.delegate = object : NotificationAreaView.Delegate {
|
||||||
|
|
||||||
|
override fun onUrlClicked(url: String) {
|
||||||
|
permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
|
||||||
|
override fun navToRoom(roomId: String, eventId: String?): Boolean {
|
||||||
|
requireActivity().finish()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resendUnsentEvents() {
|
||||||
|
TODO("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteUnsentEvents() {
|
||||||
|
TODO("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun closeScreen() {
|
||||||
|
TODO("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun jumpToBottom() {
|
||||||
|
TODO("not implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun exitSpecialMode() {
|
private fun exitSpecialMode() {
|
||||||
commandAutocompletePolicy.enabled = true
|
commandAutocompletePolicy.enabled = true
|
||||||
composerLayout.collapse()
|
composerLayout.collapse()
|
||||||
|
@ -259,17 +291,17 @@ class RoomDetailFragment :
|
||||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||||
val parser = Parser.builder().build()
|
val parser = Parser.builder().build()
|
||||||
val document = parser.parse(messageContent.formattedBody
|
val document = parser.parse(messageContent.formattedBody
|
||||||
?: messageContent.body)
|
?: messageContent.body)
|
||||||
formattedBody = eventHtmlRenderer.render(document)
|
formattedBody = eventHtmlRenderer.render(document)
|
||||||
}
|
}
|
||||||
composerLayout.composerRelatedMessageContent.text = formattedBody
|
composerLayout.composerRelatedMessageContent.text = formattedBody
|
||||||
?: nonFormattedBody
|
?: nonFormattedBody
|
||||||
|
|
||||||
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
|
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
|
||||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
||||||
|
|
||||||
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
||||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||||
|
|
||||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||||
composerLayout.expand {
|
composerLayout.expand {
|
||||||
|
@ -298,9 +330,9 @@ class RoomDetailFragment :
|
||||||
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
||||||
REACTION_SELECT_REQUEST_CODE -> {
|
REACTION_SELECT_REQUEST_CODE -> {
|
||||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
||||||
?: return
|
?: return
|
||||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||||
?: return
|
?: return
|
||||||
//TODO check if already reacted with that?
|
//TODO check if already reacted with that?
|
||||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
||||||
}
|
}
|
||||||
|
@ -335,26 +367,26 @@ class RoomDetailFragment :
|
||||||
|
|
||||||
if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) {
|
if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) {
|
||||||
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
|
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
|
||||||
R.drawable.ic_reply,
|
R.drawable.ic_reply,
|
||||||
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
||||||
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
||||||
(model as? AbsMessageItem)?.informationData?.let {
|
(model as? AbsMessageItem)?.informationData?.let {
|
||||||
val eventId = it.eventId
|
val eventId = it.eventId
|
||||||
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
||||||
return when (model) {
|
return when (model) {
|
||||||
is MessageFileItem,
|
is MessageFileItem,
|
||||||
is MessageImageVideoItem,
|
is MessageImageVideoItem,
|
||||||
is MessageTextItem -> {
|
is MessageTextItem -> {
|
||||||
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
|
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
val touchHelper = ItemTouchHelper(swipeCallback)
|
val touchHelper = ItemTouchHelper(swipeCallback)
|
||||||
touchHelper.attachToRecyclerView(recyclerView)
|
touchHelper.attachToRecyclerView(recyclerView)
|
||||||
}
|
}
|
||||||
|
@ -534,12 +566,14 @@ class RoomDetailFragment :
|
||||||
} else if (state.asyncInviter.complete) {
|
} else if (state.asyncInviter.complete) {
|
||||||
vectorBaseActivity.finish()
|
vectorBaseActivity.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.tombstoneContent == null) {
|
if (state.tombstoneContent == null) {
|
||||||
composerLayout.visibility = View.VISIBLE
|
composerLayout.visibility = View.VISIBLE
|
||||||
composerLayout.setRoomEncrypted(state.isEncrypted)
|
composerLayout.setRoomEncrypted(state.isEncrypted)
|
||||||
|
notificationAreaView.render(NotificationAreaView.State.Hidden)
|
||||||
} else {
|
} else {
|
||||||
composerLayout.visibility = View.GONE
|
composerLayout.visibility = View.GONE
|
||||||
showSnackWithMessage("TOMBSTONED", duration = Snackbar.LENGTH_INDEFINITE)
|
notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneContent))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -636,7 +670,7 @@ class RoomDetailFragment :
|
||||||
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view))
|
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view))
|
||||||
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||||
requireActivity(), view, ViewCompat.getTransitionName(view)
|
requireActivity(), view, ViewCompat.getTransitionName(view)
|
||||||
?: "").toBundle()
|
?: "").toBundle()
|
||||||
startActivity(intent, bundle)
|
startActivity(intent, bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -716,7 +750,17 @@ class RoomDetailFragment :
|
||||||
ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
|
ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
|
||||||
.show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
|
.show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
|
||||||
}
|
}
|
||||||
// AutocompleteUserPresenter.Callback
|
|
||||||
|
override fun onRoomCreateLinkClicked(url: String) {
|
||||||
|
permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor {
|
||||||
|
override fun navToRoom(roomId: String, eventId: String?): Boolean {
|
||||||
|
requireActivity().finish()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutocompleteUserPresenter.Callback
|
||||||
|
|
||||||
override fun onQueryUsers(query: CharSequence?) {
|
override fun onQueryUsers(query: CharSequence?) {
|
||||||
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
||||||
|
@ -730,7 +774,7 @@ class RoomDetailFragment :
|
||||||
}
|
}
|
||||||
MessageMenuViewModel.ACTION_VIEW_REACTIONS -> {
|
MessageMenuViewModel.ACTION_VIEW_REACTIONS -> {
|
||||||
val messageInformationData = actionData.data as? MessageInformationData
|
val messageInformationData = actionData.data as? MessageInformationData
|
||||||
?: return
|
?: return
|
||||||
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData)
|
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData)
|
||||||
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
|
|
||||||
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
|
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
|
||||||
fun onEventVisible(event: TimelineEvent)
|
fun onEventVisible(event: TimelineEvent)
|
||||||
|
fun onRoomCreateLinkClicked(url: String)
|
||||||
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
|
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
|
||||||
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
|
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
|
||||||
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
|
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
|
||||||
|
@ -158,7 +159,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
synchronized(modelCache) {
|
synchronized(modelCache) {
|
||||||
for (i in 0 until modelCache.size) {
|
for (i in 0 until modelCache.size) {
|
||||||
if (modelCache[i]?.eventId == eventIdToHighlight
|
if (modelCache[i]?.eventId == eventIdToHighlight
|
||||||
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|
||||||
modelCache[i] = null
|
modelCache[i] = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,8 +220,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
||||||
// We then are sure we always have items up to date.
|
// We then are sure we always have items up to date.
|
||||||
if (modelCache[position] == null
|
if (modelCache[position] == null
|
||||||
|| modelCache[position]?.mergedHeaderModel != null
|
|| modelCache[position]?.mergedHeaderModel != null
|
||||||
|| modelCache[position]?.formattedDayModel != null) {
|
|| modelCache[position]?.formattedDayModel != null) {
|
||||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -294,7 +295,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
// => handle case where paginating from mergeable events and we get more
|
// => handle case where paginating from mergeable events and we get more
|
||||||
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
||||||
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
|
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
|
||||||
?: true
|
?: true
|
||||||
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
|
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
collapsedEventIds.addAll(mergedEventIds)
|
collapsedEventIds.addAll(mergedEventIds)
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.factory
|
package im.vector.riotx.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
|
||||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
|
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
|
||||||
|
@ -37,21 +36,16 @@ class RoomCreateItemFactory @Inject constructor(private val colorProvider: Color
|
||||||
|
|
||||||
fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): RoomCreateItem? {
|
fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): RoomCreateItem? {
|
||||||
val createRoomContent = event.root.getClearContent().toModel<RoomCreateContent>()
|
val createRoomContent = event.root.getClearContent().toModel<RoomCreateContent>()
|
||||||
?: return null
|
?: return null
|
||||||
val predecessor = createRoomContent.predecessor ?: return null
|
val predecessor = createRoomContent.predecessor ?: return null
|
||||||
val roomLink = PermalinkFactory.createPermalink(predecessor.roomId) ?: return null
|
val roomLink = PermalinkFactory.createPermalink(predecessor.roomId) ?: return null
|
||||||
val urlSpan = MatrixPermalinkSpan(roomLink, object : MatrixPermalinkSpan.Callback {
|
|
||||||
override fun onUrlClicked(url: String) {
|
|
||||||
callback?.onUrlClicked(roomLink)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
val textColorInt = colorProvider.getColor(R.color.riot_primary_text_color_light)
|
|
||||||
val text = span {
|
val text = span {
|
||||||
text = stringProvider.getString(R.string.room_tombstone_continuation_description)
|
+stringProvider.getString(R.string.room_tombstone_continuation_description)
|
||||||
append("\n")
|
+"\n"
|
||||||
append(
|
span(stringProvider.getString(R.string.room_tombstone_predecessor_link)) {
|
||||||
stringProvider.getString(R.string.room_tombstone_predecessor_link)
|
textDecorationLine = "underline"
|
||||||
)
|
onClick = { callback?.onRoomCreateLinkClicked(roomLink) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return RoomCreateItem_()
|
return RoomCreateItem_()
|
||||||
.text(text)
|
.text(text)
|
||||||
|
|
|
@ -21,10 +21,10 @@ package im.vector.riotx.features.home.room.detail.timeline.item
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
import me.saket.bettermovementmethod.BetterLinkMovementMethod
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_create)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_create)
|
||||||
abstract class RoomCreateItem : VectorEpoxyModel<RoomCreateItem.Holder>() {
|
abstract class RoomCreateItem : VectorEpoxyModel<RoomCreateItem.Holder>() {
|
||||||
|
@ -32,6 +32,7 @@ abstract class RoomCreateItem : VectorEpoxyModel<RoomCreateItem.Holder>() {
|
||||||
@EpoxyAttribute lateinit var text: CharSequence
|
@EpoxyAttribute lateinit var text: CharSequence
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
|
holder.description.movementMethod = BetterLinkMovementMethod.getInstance()
|
||||||
holder.description.text = text
|
holder.description.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,12 +74,18 @@
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/recyclerView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/composerLayout"
|
app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
|
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
|
||||||
tools:listitem="@layout/item_timeline_event_base" />
|
tools:listitem="@layout/item_timeline_event_base" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/recyclerViewBarrier"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="top"
|
||||||
|
app:constraint_referenced_ids="composerLayout,notificationAreaView" />
|
||||||
|
|
||||||
<im.vector.riotx.features.home.room.detail.composer.TextComposerView
|
<im.vector.riotx.features.home.room.detail.composer.TextComposerView
|
||||||
android:id="@+id/composerLayout"
|
android:id="@+id/composerLayout"
|
||||||
|
@ -89,6 +95,16 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<im.vector.riotx.core.platform.NotificationAreaView
|
||||||
|
android:id="@+id/notificationAreaView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<im.vector.riotx.features.invite.VectorInviteView
|
<im.vector.riotx.features.invite.VectorInviteView
|
||||||
android:id="@+id/inviteView"
|
android:id="@+id/inviteView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
|
@ -12,9 +12,8 @@
|
||||||
android:layout_marginLeft="16dp"
|
android:layout_marginLeft="16dp"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
android:background="@drawable/bg_tombstone_predecessor"
|
android:background="?attr/riotx_keys_backup_banner_accent_color"
|
||||||
android:drawableStart="@drawable/error"
|
android:drawableStart="@drawable/error"
|
||||||
android:drawableLeft="@drawable/error"
|
|
||||||
android:drawablePadding="16dp"
|
android:drawablePadding="16dp"
|
||||||
android:gravity="center|start"
|
android:gravity="center|start"
|
||||||
android:minHeight="80dp"
|
android:minHeight="80dp"
|
||||||
|
|
Loading…
Reference in New Issue