Merge branch 'develop' into feature/markdown_off

This commit is contained in:
Benoit Marty 2019-11-04 10:22:20 +01:00 committed by GitHub
commit 8439c337f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 609 additions and 512 deletions

View File

@ -5,12 +5,14 @@ Features ✨:
-
Improvements 🙌:
-
- Handle code tags (#567)
Other changes:
- Markdown set to off by default (#412)
- Accessibility improvements to the attachment file type chooser
Bugfix 🐛:
- Fix issues with some member events rendering (#498)
- Passphrase does not match (Export room keys) (#644)
Translations 🗣:

View File

@ -86,6 +86,10 @@ Also, if possible, please test your change on a real device. Testing on Android
When adding new string resources, please only add new entries in file `value/strings.xml`. Translations will be added later by the community of translators with a specific tool named [Weblate](https://translate.riot.im/projects/riot-android/).
Do not hesitate to use plurals when appropriate.
### Accessibility
Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`.
### Layout
When adding or editing layouts, make sure the layout will render correctly if device uses a RTL (Right To Left) language.

View File

@ -16,7 +16,7 @@
package im.vector.matrix.android.api.session.events.model
import java.util.*
import java.util.UUID
object LocalEcho {

View File

@ -62,15 +62,11 @@ data class TimelineEvent(
}
fun getDisambiguatedDisplayName(): String {
return if (isUniqueDisplayName) {
senderName
} else {
senderName?.let { name ->
"$name (${root.senderId})"
}
return when {
senderName.isNullOrBlank() -> root.senderId ?: ""
isUniqueDisplayName -> senderName
else -> "$senderName (${root.senderId})"
}
?: root.senderId
?: ""
}
/**
@ -104,7 +100,7 @@ fun TimelineEvent.getEditedEventId(): String? {
* Get last MessageContent, after a possible edition
*/
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel()
?: root.getClearContent().toModel()
/**
* Get last Message body, after a possible edition
@ -113,7 +109,8 @@ fun TimelineEvent.getLastMessageBody(): String? {
val lastMessageContent = getLastMessageContent()
if (lastMessageContent != null) {
return lastMessageContent.newContent?.toModel<MessageContent>()?.body ?: lastMessageContent.body
return lastMessageContent.newContent?.toModel<MessageContent>()?.body
?: lastMessageContent.body
}
return null

View File

@ -66,7 +66,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
if (':' in userId) {
try {
synchronized(notReadyToRetryHS) {
res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1))
res = !notReadyToRetryHS.contains(userId.substringAfterLast(':'))
}
} catch (e: Exception) {
Timber.e(e, "## canRetryKeysDownload() failed")

View File

@ -216,7 +216,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
sendMessageToDevices(requestMessage, request.recipients, request.requestId, object : MatrixCallback<Unit> {
private fun onDone(state: OutgoingRoomKeyRequest.RequestState) {
if (request.state !== OutgoingRoomKeyRequest.RequestState.UNSENT) {
Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to " + request.state)
Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to ${request.state}")
} else {
request.state = state
cryptoStore.updateOutgoingRoomKeyRequest(request)

View File

@ -43,6 +43,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.Exception
import java.util.UUID
import javax.inject.Inject
import kotlin.collections.HashMap
@ -166,72 +167,59 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
return
}
// Download device keys prior to everything
checkKeysAreDownloaded(
otherUserId!!,
startReq,
success = {
Timber.v("## SAS onStartRequestReceived ${startReq.transactionID!!}")
val tid = startReq.transactionID!!
val existing = getExistingTransaction(otherUserId, tid)
val existingTxs = getExistingTransactionsForUser(otherUserId)
if (existing != null) {
// should cancel both!
Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}")
existing.cancel(CancelCode.UnexpectedMessage)
cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else if (existingTxs?.isEmpty() == false) {
Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}")
// Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time.
existingTxs.forEach {
it.cancel(CancelCode.UnexpectedMessage)
}
cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else {
// Ok we can create
if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) {
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
val tx = IncomingSASVerificationTransaction(
this,
setDeviceVerificationAction,
credentials,
cryptoStore,
sendToDeviceTask,
taskExecutor,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionID!!,
otherUserId)
addTransaction(tx)
tx.acceptToDeviceEvent(otherUserId, startReq)
} else {
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
cancelTransaction(tid, otherUserId, startReq.fromDevice
?: event.getSenderKey()!!, CancelCode.UnknownMethod)
}
}
},
error = {
cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
})
if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) {
Timber.v("## SAS onStartRequestReceived ${startReq.transactionID!!}")
val tid = startReq.transactionID!!
val existing = getExistingTransaction(otherUserId, tid)
val existingTxs = getExistingTransactionsForUser(otherUserId)
if (existing != null) {
// should cancel both!
Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}")
existing.cancel(CancelCode.UnexpectedMessage)
cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else if (existingTxs?.isEmpty() == false) {
Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}")
// Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time.
existingTxs.forEach {
it.cancel(CancelCode.UnexpectedMessage)
}
cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else {
// Ok we can create
if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) {
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
val tx = IncomingSASVerificationTransaction(
this,
setDeviceVerificationAction,
credentials,
cryptoStore,
sendToDeviceTask,
taskExecutor,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionID!!,
otherUserId)
addTransaction(tx)
tx.acceptToDeviceEvent(otherUserId, startReq)
} else {
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
cancelTransaction(tid, otherUserId, startReq.fromDevice
?: event.getSenderKey()!!, CancelCode.UnknownMethod)
}
}
} else {
cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
}
}
private suspend fun checkKeysAreDownloaded(otherUserId: String,
startReq: KeyVerificationStart,
success: (MXUsersDevicesMap<MXDeviceInfo>) -> Unit,
error: () -> Unit) {
runCatching {
deviceListManager.downloadKeys(listOf(otherUserId), true)
}.fold(
{
if (it.getUserDeviceIds(otherUserId)?.contains(startReq.fromDevice) == true) {
success(it)
} else {
error()
}
},
{
error()
}
)
startReq: KeyVerificationStart): MXUsersDevicesMap<MXDeviceInfo>? {
return try {
val keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null
keys.takeIf { deviceIds.contains(startReq.fromDevice) }
} catch (e: Exception) {
null
}
}
private suspend fun onCancelReceived(event: Event) {
@ -342,10 +330,8 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
private fun addTransaction(tx: VerificationTransaction) {
tx.otherUserId.let { otherUserId ->
synchronized(txMap) {
if (txMap[otherUserId] == null) {
txMap[otherUserId] = HashMap()
}
txMap[otherUserId]?.set(tx.transactionId, tx)
val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() }
txInnerMap[tx.transactionId] = tx
dispatchTxAdded(tx)
tx.addListener(this)
}

View File

@ -39,14 +39,17 @@ internal fun TimelineEventEntity.updateSenderData() {
val isUnlinked = chunkEntity.isUnlinked()
var senderMembershipEvent: EventEntity?
var senderRoomMemberContent: String?
var senderRoomMemberPrevContent: String?
when {
stateIndex <= 0 -> {
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root
senderRoomMemberContent = senderMembershipEvent?.prevContent
senderRoomMemberPrevContent = senderMembershipEvent?.content
}
else -> {
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root
senderRoomMemberContent = senderMembershipEvent?.content
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
}
}
@ -58,11 +61,27 @@ internal fun TimelineEventEntity.updateSenderData() {
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER)
.prev(since = stateIndex)
senderRoomMemberContent = senderMembershipEvent?.content
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
}
ContentMapper.map(senderRoomMemberContent).toModel<RoomMember>()?.also {
this.senderAvatar = it.avatarUrl
this.senderName = it.displayName
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
}
// We try to fallback on prev content if we got a room member state events with null fields
if (root?.type == EventType.STATE_ROOM_MEMBER) {
ContentMapper.map(senderRoomMemberPrevContent).toModel<RoomMember>()?.also {
if (this.senderAvatar == null && it.avatarUrl != null) {
this.senderAvatar = it.avatarUrl
}
if (this.senderName == null && it.displayName != null) {
this.senderName = it.displayName
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
}
}
}
val senderRoomMember: RoomMember? = ContentMapper.map(senderRoomMemberContent).toModel()
this.senderAvatar = senderRoomMember?.avatarUrl
this.senderName = senderRoomMember?.displayName
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(senderRoomMember?.displayName)
this.senderMembershipEvent = senderMembershipEvent
}

View File

@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import java.util.*
import java.util.UUID
import javax.inject.Inject
internal class RoomSummaryMapper @Inject constructor(

View File

@ -22,7 +22,7 @@ import com.novoda.merlin.MerlinsBeard
import im.vector.matrix.android.internal.di.MatrixScope
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import timber.log.Timber
import java.util.*
import java.util.Collections
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

View File

@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import java.util.*
import java.util.Date
import javax.inject.Inject
internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit>

View File

@ -32,7 +32,7 @@ import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.worker.WorkManagerUtil
import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import java.util.*
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject

View File

@ -39,7 +39,6 @@ import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.util.StringProvider
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import java.util.*
import javax.inject.Inject
/**
@ -119,7 +118,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
permalink,
stringProvider.getString(R.string.message_reply_to_prefix),
userLink,
originalEvent.senderName ?: originalEvent.root.senderId,
originalEvent.getDisambiguatedDisplayName(),
body.takeFormatted(),
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
)

View File

@ -52,7 +52,8 @@ import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import timber.log.Timber
import java.util.*
import java.util.Collections
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.ArrayList

View File

@ -31,7 +31,7 @@ import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.SecureRandom
import java.util.*
import java.util.Calendar
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec

View File

@ -36,7 +36,7 @@ import java.security.*
import java.security.cert.CertificateException
import java.security.spec.AlgorithmParameterSpec
import java.security.spec.RSAKeyGenParameterSpec
import java.util.*
import java.util.Calendar
import java.util.zip.GZIPOutputStream
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.util
import im.vector.matrix.android.api.MatrixPatterns
import timber.log.Timber
import java.util.*
import java.util.Locale
/**
* Convert a string to an UTF8 String

View File

@ -219,7 +219,7 @@ dependencies {
def epoxy_version = '3.8.0'
def arrow_version = "0.8.2"
def coroutines_version = "1.3.2"
def markwon_version = '3.1.0'
def markwon_version = '4.1.2'
def big_image_viewer_version = '1.5.6'
def glide_version = '4.10.0'
def moshi_version = '1.8.0'
@ -283,8 +283,8 @@ dependencies {
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.android.material:material:1.1.0-beta01'
implementation 'me.gujun.android:span:1.7'
implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version"
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version"
implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1'

View File

@ -55,7 +55,8 @@ import im.vector.riotx.features.version.VersionProvider
import im.vector.riotx.push.fcm.FcmHelper
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
import javax.inject.Inject
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {

View File

@ -30,15 +30,10 @@ import java.io.File
*/
@WorkerThread
fun writeToFile(str: String, file: File): Try<Unit> {
return Try {
val sink = file.sink()
val bufferedSink = sink.buffer()
bufferedSink.writeString(str, Charsets.UTF_8)
bufferedSink.close()
sink.close()
return Try<Unit> {
file.sink().buffer().use {
it.writeString(str, Charsets.UTF_8)
}
}
}
@ -47,15 +42,10 @@ fun writeToFile(str: String, file: File): Try<Unit> {
*/
@WorkerThread
fun writeToFile(data: ByteArray, file: File): Try<Unit> {
return Try {
val sink = file.sink()
val bufferedSink = sink.buffer()
bufferedSink.write(data)
bufferedSink.close()
sink.close()
return Try<Unit> {
file.sink().buffer().use {
it.write(data)
}
}
}

View File

@ -17,7 +17,6 @@
package im.vector.riotx.core.images
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import androidx.exifinterface.media.ExifInterface
@ -37,26 +36,24 @@ class ImageTools @Inject constructor(private val context: Context) {
if (uri.scheme == "content") {
val proj = arrayOf(MediaStore.Images.Media.DATA)
var cursor: Cursor? = null
try {
cursor = context.contentResolver.query(uri, proj, null, null, null)
if (cursor != null && cursor.count > 0) {
cursor.moveToFirst()
val idxData = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val path = cursor.getString(idxData)
if (path.isNullOrBlank()) {
Timber.w("Cannot find path in media db for uri $uri")
return orientation
val cursor = context.contentResolver.query(uri, proj, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val idxData = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val path = it.getString(idxData)
if (path.isNullOrBlank()) {
Timber.w("Cannot find path in media db for uri $uri")
return orientation
}
val exif = ExifInterface(path)
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
}
val exif = ExifInterface(path)
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
}
} catch (e: Exception) {
// eg SecurityException from com.google.android.apps.photos.content.GooglePhotosImageProvider URIs
// eg IOException from trying to parse the returned path as a file when it is an http uri.
Timber.e(e, "Cannot get orientation for bitmap")
} finally {
cursor?.close()
}
} else if (uri.scheme == "file") {
try {

View File

@ -17,28 +17,17 @@
package im.vector.riotx.core.intent
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
var result: String? = null
if (context != null && uri.scheme == "content") {
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
return it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
} finally {
cursor?.close()
}
}
if (result == null) {
result = uri.path
val cut = result?.lastIndexOf('/') ?: -1
if (cut != -1) {
result = result?.substring(cut + 1)
}
}
return result
return uri.path?.substringAfterLast('/')
}

View File

@ -15,8 +15,6 @@
*/
package im.vector.riotx.core.linkify
import java.util.regex.Pattern
/**
* Better support for geo URi
*/
@ -26,7 +24,7 @@ object VectorAutoLinkPatterns {
private const val LAT_OR_LONG_OR_ALT_NUMBER = "-?\\d+(?:\\.\\d+)?"
private const val COORDINATE_SYSTEM = ";crs=[\\w-]+"
val GEO_URI: Pattern = Pattern.compile("(?:geo:)?" +
val GEO_URI: Regex = Regex("(?:geo:)?" +
"(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" +
"," +
"(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" +
@ -35,5 +33,5 @@ object VectorAutoLinkPatterns {
"(?:" + ";u=\\d+(?:\\.\\d+)?" + ")?" + // uncertainty in meters
"(?:" +
";[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+" + // dafuk
")*", Pattern.CASE_INSENSITIVE)
")*", RegexOption.IGNORE_CASE)
}

View File

@ -19,7 +19,6 @@ import android.text.Spannable
import android.text.style.URLSpan
import android.text.util.Linkify
import androidx.core.text.util.LinkifyCompat
import java.util.*
object VectorLinkify {
/**
@ -95,7 +94,7 @@ object VectorLinkify {
createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
}
LinkifyCompat.addLinks(spannable, VectorAutoLinkPatterns.GEO_URI, "geo:", arrayOf("geo:"), geoMatchFilter, null)
LinkifyCompat.addLinks(spannable, VectorAutoLinkPatterns.GEO_URI.toPattern(), "geo:", arrayOf("geo:"), geoMatchFilter, null)
spannable.forEachSpanIndexed { _, urlSpan, start, end ->
spannable.removeSpan(urlSpan)
createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
@ -108,7 +107,7 @@ object VectorLinkify {
}
private fun pruneOverlaps(links: ArrayList<LinkSpec>) {
Collections.sort(links, COMPARATOR)
links.sortWith(COMPARATOR)
var len = links.size
var i = 0
while (i < len - 1) {

View File

@ -16,17 +16,15 @@
package im.vector.riotx.core.platform
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.widget.ScrollView
import androidx.core.widget.NestedScrollView
import im.vector.riotx.R
private const val DEFAULT_MAX_HEIGHT = 200
class MaxHeightScrollView : ScrollView {
class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
: NestedScrollView(context, attrs, defStyle) {
var maxHeight: Int = 0
set(value) {
@ -34,28 +32,7 @@ class MaxHeightScrollView : ScrollView {
requestLayout()
}
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
if (!isInEditMode) {
init(context, attrs)
}
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
if (!isInEditMode) {
init(context, attrs)
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
if (!isInEditMode) {
init(context, attrs)
}
}
private fun init(context: Context, attrs: AttributeSet?) {
init {
if (attrs != null) {
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView)
maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)

View File

@ -30,7 +30,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.utils.DimensionConverter
import java.util.*
import java.util.UUID
/**
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)

View File

@ -22,8 +22,9 @@ import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.AppNameProvider
import im.vector.riotx.core.resources.LocaleProvider
import im.vector.riotx.core.resources.StringProvider
import java.util.*
import java.util.UUID
import javax.inject.Inject
import kotlin.math.abs
private const val DEFAULT_PUSHER_FILE_TAG = "mobile"
@ -36,7 +37,7 @@ class PushersManager @Inject constructor(
fun registerPusherWithFcmKey(pushKey: String): UUID {
val currentSession = activeSessionHolder.getActiveSession()
var profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + Math.abs(currentSession.myUserId.hashCode())
val profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(currentSession.myUserId.hashCode())
return currentSession.addHttpPusher(
pushKey,

View File

@ -18,7 +18,7 @@ package im.vector.riotx.core.resources
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat
import java.util.*
import java.util.Locale
import javax.inject.Inject
class LocaleProvider @Inject constructor(private val resources: Resources) {

View File

@ -16,7 +16,7 @@
package im.vector.riotx.core.utils
import android.view.View
import java.util.*
import java.util.WeakHashMap
/**
* Simple Debounced OnClickListener

View File

@ -59,9 +59,10 @@ fun initKnownEmojiHashSet(context: Context, done: (() -> Unit)? = null) {
val jsonAdapter = moshi.adapter(EmojiDataSource.EmojiData::class.java)
val inputAsString = input.bufferedReader().use { it.readText() }
val source = jsonAdapter.fromJson(inputAsString)
knownEmojiSet = HashSet<String>()
source?.emojis?.values?.forEach {
knownEmojiSet?.add(it.emojiString())
knownEmojiSet = HashSet<String>().also {
source?.emojis?.mapTo(it) { (_, value) ->
value.emojiString()
}
}
done?.invoke()
}

View File

@ -32,7 +32,8 @@ import im.vector.riotx.R
import timber.log.Timber
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
/**
* Open a url in the internet browser of the system

View File

@ -31,7 +31,7 @@ import im.vector.riotx.R
import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.settings.VectorLocale
import timber.log.Timber
import java.util.*
import java.util.Locale
/**
* Tells if the application ignores battery optimizations.

View File

@ -19,7 +19,7 @@ package im.vector.riotx.core.utils
import android.content.Context
import android.os.Build
import android.text.format.Formatter
import java.util.*
import java.util.TreeMap
object TextUtils {

View File

@ -24,7 +24,7 @@ import im.vector.riotx.features.settings.FontScale
import im.vector.riotx.features.settings.VectorLocale
import im.vector.riotx.features.themes.ThemeUtils
import timber.log.Timber
import java.util.*
import java.util.Locale
import javax.inject.Inject
/**

View File

@ -18,10 +18,10 @@ package im.vector.riotx.features.crypto.keys
import android.content.Context
import android.os.Environment
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.files.writeToFile
import kotlinx.coroutines.Dispatchers
@ -36,28 +36,20 @@ class KeysExporter(private val session: Session) {
* Export keys and return the file path with the callback
*/
fun export(context: Context, password: String, callback: MatrixCallback<String>) {
session.exportRoomKeys(password, object : MatrixCallback<ByteArray> {
override fun onSuccess(data: ByteArray) {
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
Try {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")
GlobalScope.launch(Dispatchers.Main) {
runCatching {
val data = awaitCallback<ByteArray> { session.exportRoomKeys(password, it) }
withContext(Dispatchers.IO) {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")
writeToFile(data, file)
writeToFile(data, file)
addEntryToDownloadManager(context, file, "text/plain")
addEntryToDownloadManager(context, file, "text/plain")
file.absolutePath
}
}
.foldToCallback(callback)
file.absolutePath
}
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
})
}.foldToCallback(callback)
}
}
}

View File

@ -18,10 +18,11 @@ package im.vector.riotx.features.crypto.keys
import android.content.Context
import android.net.Uri
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.core.intent.getMimeTypeFromUri
import im.vector.riotx.core.resources.openResource
import kotlinx.coroutines.Dispatchers
@ -41,8 +42,8 @@ class KeysImporter(private val session: Session) {
password: String,
callback: MatrixCallback<ImportRoomKeysResult>) {
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
Try {
runCatching {
withContext(Dispatchers.IO) {
val resource = openResource(context, uri, mimetype ?: getMimeTypeFromUri(context, uri))
if (resource?.mContentStream == null) {
@ -51,33 +52,17 @@ class KeysImporter(private val session: Session) {
val data: ByteArray
try {
data = ByteArray(resource.mContentStream!!.available())
resource.mContentStream!!.read(data)
resource.mContentStream!!.close()
data
data = resource.mContentStream!!.use { it.readBytes() }
} catch (e: Exception) {
try {
resource.mContentStream!!.close()
} catch (e2: Exception) {
Timber.e(e2, "## importKeys()")
}
Timber.e(e, "## importKeys()")
throw e
}
awaitCallback<ImportRoomKeysResult> {
session.importRoomKeys(data, password, null, it)
}
}
}
.fold(
{
callback.onFailure(it)
},
{ byteArray ->
session.importRoomKeys(byteArray,
password,
null,
callback)
}
)
}.foldToCallback(callback)
}
}
}

View File

@ -31,7 +31,7 @@ import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.GenericItem
import im.vector.riotx.core.ui.list.genericItem
import java.util.*
import java.util.UUID
import javax.inject.Inject
class KeysBackupSettingsRecyclerViewController @Inject constructor(private val stringProvider: StringProvider,

View File

@ -170,8 +170,8 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
private fun exportRecoveryKeyToFile(data: String) {
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
Try {
Try {
withContext(Dispatchers.IO) {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")

View File

@ -409,7 +409,7 @@ class RoomDetailFragment :
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
avatarRenderer.render(event.senderAvatar, event.root.senderId ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
avatarRenderer.render(event.senderAvatar, event.root.senderId ?: "", event.getDisambiguatedDisplayName(), composerLayout.composerRelatedMessageAvatar)
composerLayout.expand {
// need to do it here also when not using quick reply
focusComposerAndShowKeyboard()
@ -480,7 +480,7 @@ class RoomDetailFragment :
jumpToReadMarkerView.render(show, readMarkerId)
}
}
recyclerView.setController(timelineEventController)
recyclerView.adapter = timelineEventController.adapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {

View File

@ -42,7 +42,8 @@ import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventForm
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
/**
* Quick reactions state

View File

@ -38,7 +38,7 @@ import im.vector.riotx.features.html.EventHtmlRenderer
import me.gujun.android.span.span
import name.fraser.neil.plaintext.diff_match_patch
import timber.log.Timber
import java.util.*
import java.util.Calendar
/**
* Epoxy controller for edit history list
@ -94,7 +94,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
val body = cContent.second?.let { eventHtmlRenderer.render(it) }
?: cContent.first
val nextEvent = if (index + 1 <= sourceEvents.lastIndex) sourceEvents[index + 1] else null
val nextEvent = sourceEvents.getOrNull(index + 1)
var spannedDiff: Spannable? = null
if (nextEvent != null && cContent.second == null /*No diff for html*/) {

View File

@ -30,7 +30,7 @@ import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import timber.log.Timber
import java.util.*
import java.util.UUID
data class ViewEditHistoryViewState(
val eventId: String,

View File

@ -27,8 +27,6 @@ import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
@ -41,13 +39,13 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?): NoticeItem? {
val text = buildNoticeText(event.root, event.senderName) ?: return null
val text = buildNoticeText(event.root, event.getDisambiguatedDisplayName()) ?: return null
val informationData = MessageInformationData(
eventId = event.root.eventId ?: "?",
senderId = event.root.senderId ?: "",
sendState = event.root.sendState,
avatarUrl = event.senderAvatar(),
memberName = event.senderName(),
avatarUrl = event.senderAvatar,
memberName = event.getDisambiguatedDisplayName(),
showInformation = false
)
val attributes = NoticeItem.Attributes(

View File

@ -64,12 +64,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
if (!showReadMarker && mergedEvent.hasReadMarker && readMarkerVisible) {
showReadMarker = true
}
val senderAvatar = mergedEvent.senderAvatar()
val senderName = mergedEvent.senderName()
val senderAvatar = mergedEvent.senderAvatar
val senderName = mergedEvent.getDisambiguatedDisplayName()
val data = MergedHeaderItem.Data(
userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar,
memberName = senderName ?: "",
memberName = senderName,
localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: ""
)

View File

@ -46,9 +46,11 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle
import im.vector.riotx.features.home.room.detail.timeline.helper.*
import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.CodeVisitor
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import me.gujun.android.span.span
import org.commonmark.node.Document
import javax.inject.Inject
class MessageItemFactory @Inject constructor(
@ -97,16 +99,8 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent()
// val ev = all.toModel<Event>()
return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData,
highlight,
callback,
attributes)
is MessageTextContent -> buildTextMessageItem(messageContent,
informationData,
highlight,
callback,
attributes)
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
@ -229,34 +223,75 @@ class MessageItemFactory @Inject constructor(
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
}
private fun buildTextMessageItem(messageContent: MessageTextContent,
private fun buildItemForTextContent(messageContent: MessageTextContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
return if (isFormatted) {
val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document
val codeVisitor = CodeVisitor()
codeVisitor.visit(localFormattedBody)
when (codeVisitor.codeKind) {
CodeVisitor.Kind.BLOCK -> {
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
buildCodeBlockItem(codeFormattedBlock, informationData, highlight, callback, attributes)
}
CodeVisitor.Kind.INLINE -> {
val codeFormatted = htmlRenderer.get().render(localFormattedBody)
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
}
CodeVisitor.Kind.NONE -> {
val formattedBody = htmlRenderer.get().render(messageContent.formattedBody!!)
buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
}
}
} else {
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
}
}
private fun buildMessageTextItem(body: CharSequence,
isFormatted: Boolean,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
val bodyToUse = messageContent.formattedBody?.let {
htmlRenderer.get().render(it.trim())
} ?: messageContent.body
val linkifiedBody = linkifyBody(body, callback)
val linkifiedBody = linkifyBody(bodyToUse, callback)
return MessageTextItem_()
.apply {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited(linkifiedBody, callback, informationData)
message(spannable)
} else {
message(linkifiedBody)
}
}
return MessageTextItem_().apply {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited(linkifiedBody, callback, informationData)
message(spannable)
} else {
message(linkifiedBody)
}
}
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
.searchForPills(isFormatted)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.urlClickCallback(callback)
// click on the text
}
private fun buildCodeBlockItem(formattedBody: CharSequence,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageBlockCodeItem? {
return MessageBlockCodeItem_()
.apply {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited("", callback, informationData)
editedSpan(spannable)
}
}
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.message(formattedBody)
}
private fun annotateWithEdited(linkifiedBody: CharSequence,

View File

@ -19,13 +19,17 @@ package im.vector.riotx.features.home.room.detail.timeline.format
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.*
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import timber.log.Timber
import javax.inject.Inject
@ -36,7 +40,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
return when (val type = timelineEvent.root.getClearType()) {
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName())
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName())
EventType.CALL_INVITE,
@ -96,7 +100,8 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
}
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
?: return null
val formattedVisibility = when (historyVisibility) {
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
@ -135,7 +140,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
}
}
private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String {
val displayText = StringBuilder()
// Check display name has been changed
if (eventContent?.displayName != prevEventContent?.displayName) {
@ -146,7 +151,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName)
else ->
stringProvider.getString(R.string.notice_display_name_changed_from,
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
}
displayText.append(displayNameText)
}
@ -160,6 +165,11 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
}
displayText.append(displayAvatarText)
}
if (displayText.isEmpty()) {
displayText.append(
stringProvider.getString(R.string.notice_member_no_changes, senderName)
)
}
return displayText.toString()
}
@ -171,9 +181,10 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId
when {
eventContent.thirdPartyInvite != null -> {
val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey
val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid
?: event.stateKey
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
userWhoHasAccepted, eventContent.thirdPartyInvite?.displayName)
userWhoHasAccepted, eventContent.thirdPartyInvite?.displayName)
}
event.stateKey == selfUserId ->
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)

View File

@ -17,8 +17,6 @@
package im.vector.riotx.features.home.room.detail.timeline.helper
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.extensions.localDateTime
@ -47,25 +45,6 @@ object TimelineDisplayableEvents {
)
}
fun TimelineEvent.senderAvatar(): String? {
// We might have no avatar when user leave, so we try to get it from prevContent
return senderAvatar
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
root.prevContent.toModel<RoomMember>()?.avatarUrl
} else {
null
}
}
fun TimelineEvent.senderName(): String? {
// We might have no senderName when user leave, so we try to get it from prevContent
return when {
senderName != null -> getDisambiguatedDisplayName()
root.type == EventType.STATE_ROOM_MEMBER -> root.prevContent.toModel<RoomMember>()?.displayName
else -> null
}
}
fun TimelineEvent.canBeMerged(): Boolean {
return root.getClearType() == EventType.STATE_ROOM_MEMBER
}

View File

@ -0,0 +1,54 @@
/*
* 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.features.home.room.detail.timeline.item
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.extensions.setTextOrHide
import me.saket.bettermovementmethod.BetterLinkMovementMethod
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageBlockCodeItem : AbsMessageItem<MessageBlockCodeItem.Holder>() {
@EpoxyAttribute
var message: CharSequence? = null
@EpoxyAttribute
var editedSpan: CharSequence? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.messageView.text = message
renderSendState(holder.messageView, holder.messageView)
holder.messageView.setOnClickListener(attributes.itemClickListener)
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
holder.editedView.movementMethod = BetterLinkMovementMethod.getInstance()
holder.editedView.setTextOrHide(editedSpan)
}
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val messageView by bind<TextView>(R.id.codeBlockTextView)
val editedView by bind<TextView>(R.id.codeBlockEditedView)
}
companion object {
private const val STUB_ID = R.id.messageContentCodeBlockStub
}
}

View File

@ -30,7 +30,6 @@ import im.vector.riotx.core.resources.DateProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import me.gujun.android.span.span
import javax.inject.Inject
@ -97,10 +96,10 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
&& latestEvent.root.mxDecryptionResult == null) {
stringProvider.getString(R.string.encrypted_message)
} else if (latestEvent.root.getClearType() == EventType.MESSAGE) {
val senderName = latestEvent.senderName() ?: latestEvent.root.senderId
val senderName = latestEvent.getDisambiguatedDisplayName()
val content = latestEvent.root.getClearContent()?.toModel<MessageContent>()
val message = content?.body ?: ""
if (roomSummary.isDirect.not() && senderName != null) {
if (roomSummary.isDirect.not()) {
span {
text = senderName
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)

View File

@ -0,0 +1,55 @@
/*
* 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.features.html
import org.commonmark.node.AbstractVisitor
import org.commonmark.node.Code
import org.commonmark.node.FencedCodeBlock
import org.commonmark.node.IndentedCodeBlock
/**
* This class is in charge of visiting nodes and tells if we have some code nodes (inline or block).
*/
class CodeVisitor : AbstractVisitor() {
var codeKind: Kind = Kind.NONE
private set
override fun visit(fencedCodeBlock: FencedCodeBlock?) {
if (codeKind == Kind.NONE) {
codeKind = Kind.BLOCK
}
}
override fun visit(indentedCodeBlock: IndentedCodeBlock?) {
if (codeKind == Kind.NONE) {
codeKind = Kind.BLOCK
}
}
override fun visit(code: Code?) {
if (codeKind == Kind.NONE) {
codeKind = Kind.INLINE
}
}
enum class Kind {
NONE,
INLINE,
BLOCK
}
}

View File

@ -17,171 +17,46 @@
package im.vector.riotx.features.html
import android.content.Context
import android.text.style.URLSpan
import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequests
import im.vector.riotx.features.home.AvatarRenderer
import org.commonmark.node.BlockQuote
import org.commonmark.node.HtmlBlock
import org.commonmark.node.HtmlInline
import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.html.TagHandlerNoOp
import org.commonmark.node.Node
import ru.noties.markwon.*
import ru.noties.markwon.html.HtmlTag
import ru.noties.markwon.html.MarkwonHtmlParserImpl
import ru.noties.markwon.html.MarkwonHtmlRenderer
import ru.noties.markwon.html.TagHandler
import ru.noties.markwon.html.tag.*
import java.util.Arrays.asList
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class EventHtmlRenderer @Inject constructor(context: Context,
avatarRenderer: AvatarRenderer,
sessionHolder: ActiveSessionHolder) {
htmlConfigure: MatrixHtmlPluginConfigure) {
private val markwon = Markwon.builder(context)
.usePlugin(MatrixPlugin.create(GlideApp.with(context), context, avatarRenderer, sessionHolder))
.usePlugin(HtmlPlugin.create(htmlConfigure))
.build()
fun parse(text: String): Node {
return markwon.parse(text)
}
fun render(text: String): CharSequence {
return markwon.toMarkdown(text)
}
fun render(node: Node) : CharSequence {
fun render(node: Node): CharSequence {
return markwon.render(node)
}
}
private class MatrixPlugin private constructor(private val glideRequests: GlideRequests,
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val session: ActiveSessionHolder) : AbstractMarkwonPlugin() {
class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.htmlParser(MarkwonHtmlParserImpl.create())
}
override fun configureHtmlRenderer(builder: MarkwonHtmlRenderer.Builder) {
builder
.setHandler(
"img",
ImageHandler.create())
.setHandler(
"a",
MxLinkHandler(glideRequests, context, avatarRenderer, session))
.setHandler(
"blockquote",
BlockquoteHandler())
.setHandler(
"font",
FontTagHandler())
.setHandler(
"sub",
SubScriptHandler())
.setHandler(
"sup",
SuperScriptHandler())
.setHandler(
asList<String>("b", "strong"),
StrongEmphasisHandler())
.setHandler(
asList<String>("s", "del"),
StrikeHandler())
.setHandler(
asList<String>("u", "ins"),
UnderlineHandler())
.setHandler(
asList<String>("ul", "ol"),
ListHandler())
.setHandler(
asList<String>("i", "em", "cite", "dfn"),
EmphasisHandler())
.setHandler(
asList<String>("h1", "h2", "h3", "h4", "h5", "h6"),
HeadingHandler())
.setHandler("mx-reply",
MxReplyTagHandler())
}
override fun afterRender(node: Node, visitor: MarkwonVisitor) {
val configuration = visitor.configuration()
configuration.htmlRenderer().render(visitor, configuration.htmlParser())
}
override fun configureVisitor(builder: MarkwonVisitor.Builder) {
builder
.on(HtmlBlock::class.java) { visitor, htmlBlock -> visitHtml(visitor, htmlBlock.literal) }
.on(HtmlInline::class.java) { visitor, htmlInline -> visitHtml(visitor, htmlInline.literal) }
}
private fun visitHtml(visitor: MarkwonVisitor, html: String?) {
if (html != null) {
visitor.configuration().htmlParser().processFragment(visitor.builder(), html)
}
}
companion object {
fun create(glideRequests: GlideRequests, context: Context, avatarRenderer: AvatarRenderer, session: ActiveSessionHolder): MatrixPlugin {
return MatrixPlugin(glideRequests, context, avatarRenderer, session)
}
}
}
private class MxLinkHandler(private val glideRequests: GlideRequests,
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val sessionHolder: ActiveSessionHolder) : TagHandler() {
private val linkHandler = LinkHandler()
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val link = tag.attributes()["href"]
if (link != null) {
val permalinkData = PermalinkParser.parse(link)
when (permalinkData) {
is PermalinkData.UserLink -> {
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user)
SpannableBuilder.setSpans(
visitor.builder(),
span,
tag.start(),
tag.end()
)
// also add clickable span
SpannableBuilder.setSpans(
visitor.builder(),
URLSpan(link),
tag.start(),
tag.end()
)
}
else -> linkHandler.handle(visitor, renderer, tag)
}
} else {
linkHandler.handle(visitor, renderer, tag)
}
}
}
private class MxReplyTagHandler : TagHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val configuration = visitor.configuration()
val factory = configuration.spansFactory().get(BlockQuote::class.java)
if (factory != null) {
SpannableBuilder.setSpans(
visitor.builder(),
factory.getSpans(configuration, visitor.renderProps()),
tag.start(),
tag.end()
)
val replyText = visitor.builder().removeFromEnd(tag.end())
visitor.builder().append("\n\n").append(replyText)
}
override fun configureHtml(plugin: HtmlPlugin) {
plugin
.addHandler(TagHandlerNoOp.create("a"))
.addHandler(FontTagHandler())
.addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session))
.addHandler(MxReplyTagHandler())
}
}

View File

@ -17,15 +17,18 @@ package im.vector.riotx.features.html
import android.graphics.Color
import android.text.style.ForegroundColorSpan
import ru.noties.markwon.MarkwonConfiguration
import ru.noties.markwon.RenderProps
import ru.noties.markwon.html.HtmlTag
import ru.noties.markwon.html.tag.SimpleTagHandler
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.RenderProps
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.tag.SimpleTagHandler
/**
* custom to matrix for IRC-style font coloring
*/
class FontTagHandler : SimpleTagHandler() {
override fun supportedTags() = listOf("font")
override fun getSpans(configuration: MarkwonConfiguration, renderProps: RenderProps, tag: HtmlTag): Any? {
val colorString = tag.attributes()["color"]?.let { parseColor(it) } ?: Color.BLACK
return ForegroundColorSpan(colorString)
@ -37,23 +40,23 @@ class FontTagHandler : SimpleTagHandler() {
} catch (e: Exception) {
// try other w3c colors?
return when (color_name) {
"white" -> Color.WHITE
"yellow" -> Color.YELLOW
"white" -> Color.WHITE
"yellow" -> Color.YELLOW
"fuchsia" -> Color.parseColor("#FF00FF")
"red" -> Color.RED
"silver" -> Color.parseColor("#C0C0C0")
"gray" -> Color.GRAY
"olive" -> Color.parseColor("#808000")
"purple" -> Color.parseColor("#800080")
"maroon" -> Color.parseColor("#800000")
"aqua" -> Color.parseColor("#00FFFF")
"lime" -> Color.parseColor("#00FF00")
"teal" -> Color.parseColor("#008080")
"green" -> Color.GREEN
"blue" -> Color.BLUE
"orange" -> Color.parseColor("#FFA500")
"navy" -> Color.parseColor("#000080")
else -> Color.BLACK
"red" -> Color.RED
"silver" -> Color.parseColor("#C0C0C0")
"gray" -> Color.GRAY
"olive" -> Color.parseColor("#808000")
"purple" -> Color.parseColor("#800080")
"maroon" -> Color.parseColor("#800000")
"aqua" -> Color.parseColor("#00FFFF")
"lime" -> Color.parseColor("#00FF00")
"teal" -> Color.parseColor("#008080")
"green" -> Color.GREEN
"blue" -> Color.BLUE
"orange" -> Color.parseColor("#FFA500")
"navy" -> Color.parseColor("#000080")
else -> Color.BLACK
}
}
}

View File

@ -0,0 +1,65 @@
/*
* 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.features.html
import android.content.Context
import android.text.style.URLSpan
import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideRequests
import im.vector.riotx.features.home.AvatarRenderer
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.SpannableBuilder
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.MarkwonHtmlRenderer
import io.noties.markwon.html.tag.LinkHandler
class MxLinkTagHandler(private val glideRequests: GlideRequests,
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val sessionHolder: ActiveSessionHolder) : LinkHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val link = tag.attributes()["href"]
if (link != null) {
val permalinkData = PermalinkParser.parse(link)
when (permalinkData) {
is PermalinkData.UserLink -> {
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user)
SpannableBuilder.setSpans(
visitor.builder(),
span,
tag.start(),
tag.end()
)
// also add clickable span
SpannableBuilder.setSpans(
visitor.builder(),
URLSpan(link),
tag.start(),
tag.end()
)
}
else -> super.handle(visitor, renderer, tag)
}
} else {
super.handle(visitor, renderer, tag)
}
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.features.html
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.SpannableBuilder
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.MarkwonHtmlRenderer
import io.noties.markwon.html.TagHandler
import org.commonmark.node.BlockQuote
class MxReplyTagHandler : TagHandler() {
override fun supportedTags() = listOf("mx-reply")
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val configuration = visitor.configuration()
val factory = configuration.spansFactory().get(BlockQuote::class.java)
if (factory != null) {
SpannableBuilder.setSpans(
visitor.builder(),
factory.getSpans(configuration, visitor.renderProps()),
tag.start(),
tag.end()
)
val replyText = visitor.builder().removeFromEnd(tag.end())
visitor.builder().append("\n\n").append(replyText)
}
}
}

View File

@ -33,7 +33,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import timber.log.Timber
import java.util.*
import java.util.UUID
import javax.inject.Inject
/**
@ -94,7 +94,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
event.getLastMessageBody()
?: stringProvider.getString(R.string.notification_unknown_new_event)
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.senderName ?: event.root.senderId
val senderDisplayName = event.getDisambiguatedDisplayName()
val notifiableEvent = NotifiableMessageEvent(
eventId = event.root.eventId!!,
@ -128,7 +128,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
val body = event.getLastMessageBody()
?: stringProvider.getString(R.string.notification_unknown_new_event)
val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderName ?: event.root.senderId
val senderDisplayName = event.getDisambiguatedDisplayName()
val notifiableEvent = NotifiableMessageEvent(
eventId = event.root.eventId!!,

View File

@ -27,7 +27,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.vectorComponent
import timber.log.Timber
import java.util.*
import java.util.UUID
import javax.inject.Inject
/**

View File

@ -45,9 +45,9 @@ import im.vector.riotx.features.home.room.detail.RoomDetailActivity
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.settings.VectorPreferences
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.random.Random
/**
* Util class for creating notifications.
@ -299,7 +299,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
// use a generator for the private requestCode.
// When using 0, the intent is not created/launched when the user taps on the notification.
//
val pendingIntent = stackBuilder.getPendingIntent(Random().nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT)
val pendingIntent = stackBuilder.getPendingIntent(Random.nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT)
builder.setContentIntent(pendingIntent)
@ -599,7 +599,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
val intent = HomeActivity.newIntent(context, clearNotification = true)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
intent.data = Uri.parse("foobar://tapSummary")
return PendingIntent.getActivity(context, Random().nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT)
return PendingIntent.getActivity(context, Random.nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/*

View File

@ -46,7 +46,7 @@ import org.json.JSONObject
import timber.log.Timber
import java.io.*
import java.net.HttpURLConnection
import java.util.*
import java.util.Locale
import java.util.zip.GZIPOutputStream
import javax.inject.Inject
import javax.inject.Singleton

View File

@ -24,7 +24,9 @@ import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.logging.*
import java.util.logging.Formatter
import javax.inject.Inject

View File

@ -51,7 +51,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.*
class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {

View File

@ -56,7 +56,8 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActiv
import timber.log.Timber
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() {

View File

@ -28,7 +28,6 @@ import androidx.core.graphics.drawable.DrawableCompat
import androidx.preference.PreferenceManager
import im.vector.riotx.R
import timber.log.Timber
import java.util.*
/**
* Util class for managing themes.
@ -131,24 +130,16 @@ object ThemeUtils {
*/
@ColorInt
fun getColor(c: Context, @AttrRes colorAttribute: Int): Int {
if (mColorByAttr.containsKey(colorAttribute)) {
return mColorByAttr[colorAttribute] as Int
return mColorByAttr.getOrPut(colorAttribute) {
try {
val color = TypedValue()
c.theme.resolveAttribute(colorAttribute, color, true)
color.data
} catch (e: Exception) {
Timber.e(e, "Unable to get color")
ContextCompat.getColor(c, android.R.color.holo_red_dark)
}
}
var matchedColor: Int
try {
val color = TypedValue()
c.theme.resolveAttribute(colorAttribute, color, true)
matchedColor = color.data
} catch (e: Exception) {
Timber.e(e, "Unable to get color")
matchedColor = ContextCompat.getColor(c, android.R.color.holo_red_dark)
}
mColorByAttr[colorAttribute] = matchedColor
return matchedColor
}
fun getAttribute(c: Context, @AttrRes attribute: Int): TypedValue? {

View File

@ -86,7 +86,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomToolbar" />
<com.airbnb.epoxy.EpoxyRecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"

View File

@ -78,6 +78,13 @@
android:layout="@layout/item_timeline_event_text_message_stub"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentCodeBlockStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_code_block_stub"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentMediaStub"
style="@style/TimelineContentStubBaseParams"

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/codeBlockTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textSize="14sp" />
</HorizontalScrollView>
<TextView
android:id="@+id/codeBlockEditedView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
</LinearLayout>

View File

@ -30,10 +30,12 @@
android:id="@+id/attachmentCameraButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_camera_white_24dp"
android:contentDescription="@string/attachment_type_camera"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_camera" />
</LinearLayout>
@ -50,10 +52,12 @@
android:id="@+id/attachmentGalleryButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_gallery_white_24dp"
android:contentDescription="@string/attachment_type_gallery"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_gallery" />
</LinearLayout>
@ -70,10 +74,12 @@
android:id="@+id/attachmentFileButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_file_white_24dp"
android:contentDescription="@string/attachment_type_file"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_file" />
</LinearLayout>
@ -99,10 +105,12 @@
android:id="@+id/attachmentAudioButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_audio_white_24dp"
android:contentDescription="@string/attachment_type_audio"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_audio" />
</LinearLayout>
@ -119,10 +127,12 @@
android:id="@+id/attachmentContactButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_contact_white_24dp"
android:contentDescription="@string/attachment_type_contact"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_contact" />
</LinearLayout>
@ -139,14 +149,16 @@
android:id="@+id/attachmentStickersButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_stickers_white_24dp"
android:contentDescription="@string/attachment_type_sticker"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_sticker" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -2,6 +2,6 @@
<resources>
<!-- Strings not defined in Riot -->
<string name="notice_member_no_changes">"%1$s made no changes"</string>
</resources>