mirror of
https://github.com/vector-im/element-android.git
synced 2024-12-02 16:06:40 +08:00
Merge branch 'release/0.91.4'
This commit is contained in:
commit
51abdb6066
21
CHANGES.md
21
CHANGES.md
@ -1,3 +1,24 @@
|
||||
Changes in Riot.imX 0.91.4 (2020-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Re-activate Wellknown support with updated UI (#1614)
|
||||
|
||||
Improvements 🙌:
|
||||
- Upload device keys only once to the homeserver and fix crash when no network (#1629)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix crash when coming from a notification (#1601)
|
||||
- Fix Exception when importing keys (#1576)
|
||||
- File isn't downloaded when another file with the same name already exists (#1578)
|
||||
- saved images don't show up in gallery (#1324)
|
||||
- Fix reply fallback leaking sender locale (#429)
|
||||
|
||||
Build 🧱:
|
||||
- Fix lint false-positive about WorkManager (#1012)
|
||||
- Upgrade build-tools from 3.5.3 to 3.6.6
|
||||
- Upgrade gradle from 5.4.1 to 5.6.4
|
||||
|
||||
Changes in Riot.imX 0.91.3 (2020-07-01)
|
||||
===================================================
|
||||
|
||||
|
@ -10,7 +10,7 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
classpath 'com.google.gms:google-services:4.3.2'
|
||||
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Fri Sep 27 10:10:35 CEST 2019
|
||||
#Thu Jul 02 12:33:07 CEST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||
|
16
matrix-sdk-android/proguard-rules.pro
vendored
16
matrix-sdk-android/proguard-rules.pro
vendored
@ -64,3 +64,19 @@
|
||||
|
||||
### Webrtc
|
||||
-keep class org.webrtc.** { *; }
|
||||
|
||||
### Serializable persisted classes
|
||||
# https://www.guardsquare.com/en/products/proguard/manual/examples#serializable
|
||||
-keepnames class * implements java.io.Serializable
|
||||
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||
!static !transient <fields>;
|
||||
!private <fields>;
|
||||
!private <methods>;
|
||||
private void writeObject(java.io.ObjectOutputStream);
|
||||
private void readObject(java.io.ObjectInputStream);
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="im.vector.matrix.android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@ -8,11 +7,6 @@
|
||||
|
||||
<application android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
android:exported="false"
|
||||
tools:node="remove" />
|
||||
<!--
|
||||
The SDK offers a secured File provider to access downloaded files.
|
||||
Access to these file will be given via the FileService, with a temporary
|
||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.auth.login
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.auth.AuthAPI
|
||||
import im.vector.matrix.android.internal.auth.SessionCreator
|
||||
@ -27,6 +28,7 @@ import im.vector.matrix.android.internal.di.Unauthenticated
|
||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
|
||||
import im.vector.matrix.android.internal.network.ssl.UnrecognizedCertificateException
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
@ -49,13 +51,28 @@ internal class DefaultDirectLoginTask @Inject constructor(
|
||||
|
||||
override suspend fun execute(params: DirectLoginTask.Params): Session {
|
||||
val client = buildClient(params.homeServerConnectionConfig)
|
||||
val authAPI = retrofitFactory.create(client, params.homeServerConnectionConfig.homeServerUri.toString())
|
||||
val homeServerUrl = params.homeServerConnectionConfig.homeServerUri.toString()
|
||||
|
||||
val authAPI = retrofitFactory.create(client, homeServerUrl)
|
||||
.create(AuthAPI::class.java)
|
||||
|
||||
val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName)
|
||||
|
||||
val credentials = executeRequest<Credentials>(null) {
|
||||
apiCall = authAPI.login(loginParams)
|
||||
val credentials = try {
|
||||
executeRequest<Credentials>(null) {
|
||||
apiCall = authAPI.login(loginParams)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
when (throwable) {
|
||||
is UnrecognizedCertificateException -> {
|
||||
throw Failure.UnrecognizedCertificateFailure(
|
||||
homeServerUrl,
|
||||
throwable.fingerprint
|
||||
)
|
||||
}
|
||||
else ->
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
return sessionCreator.createSession(credentials, params.homeServerConnectionConfig)
|
||||
|
@ -70,7 +70,6 @@ import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldConte
|
||||
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.model.toRest
|
||||
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||
@ -98,6 +97,7 @@ import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.TaskThread
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.task.launchToCallback
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.fetchCopied
|
||||
@ -340,11 +340,14 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
}
|
||||
|
||||
fun ensureDevice() {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) {
|
||||
// Open the store
|
||||
cryptoStore.open()
|
||||
// TODO why do that everytime? we should mark that it was done
|
||||
uploadDeviceKeys()
|
||||
// this can throw if no network
|
||||
tryThis {
|
||||
uploadDeviceKeys()
|
||||
}
|
||||
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
// this can throw if no backup
|
||||
tryThis {
|
||||
@ -389,7 +392,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
// } else {
|
||||
|
||||
// Why would we do that? it will be called at end of syn
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
// }
|
||||
}.fold(
|
||||
{
|
||||
@ -888,7 +891,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
*/
|
||||
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
|
||||
return when (secretName) {
|
||||
MASTER_KEY_SSSS_NAME -> {
|
||||
MASTER_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretMSKGossip(secretValue)
|
||||
true
|
||||
}
|
||||
@ -980,7 +983,11 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
/**
|
||||
* Upload my user's device keys.
|
||||
*/
|
||||
private suspend fun uploadDeviceKeys(): KeysUploadResponse {
|
||||
private suspend fun uploadDeviceKeys() {
|
||||
if (cryptoStore.getDeviceKeysUploaded()) {
|
||||
Timber.d("Keys already uploaded, nothing to do")
|
||||
return
|
||||
}
|
||||
// Prepare the device keys data to send
|
||||
// Sign it
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
|
||||
@ -991,7 +998,9 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
)
|
||||
|
||||
val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null)
|
||||
return uploadKeysTask.execute(uploadDeviceKeysParams)
|
||||
uploadKeysTask.execute(uploadDeviceKeysParams)
|
||||
|
||||
cryptoStore.setDeviceKeysUploaded(true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -433,4 +433,7 @@ internal interface IMXCryptoStore {
|
||||
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
fun getGossipingEventsTrail(): List<Event>
|
||||
|
||||
fun setDeviceKeysUploaded(uploaded: Boolean)
|
||||
fun getDeviceKeysUploaded(): Boolean
|
||||
}
|
||||
|
@ -842,6 +842,18 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
} ?: false
|
||||
}
|
||||
|
||||
override fun setDeviceKeysUploaded(uploaded: Boolean) {
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDeviceKeysUploaded(): Boolean {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer
|
||||
} ?: false
|
||||
}
|
||||
|
||||
override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) {
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
// Reset all
|
||||
|
@ -54,7 +54,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||
// 0, 1, 2: legacy Riot-Android
|
||||
// 3: migrate to RiotX schema
|
||||
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 10L
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 11L
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
@ -70,6 +70,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||
if (oldVersion <= 7) migrateTo8(realm)
|
||||
if (oldVersion <= 8) migrateTo9(realm)
|
||||
if (oldVersion <= 9) migrateTo10(realm)
|
||||
if (oldVersion <= 10) migrateTo11(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1Legacy(realm: DynamicRealm) {
|
||||
@ -176,13 +177,14 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||
}
|
||||
}
|
||||
|
||||
// Convert MXOlmInboundGroupSession2 to OlmInboundGroupSessionWrapper2
|
||||
// Convert MXOlmInboundGroupSession2 to OlmInboundGroupSessionWrapper
|
||||
realm.schema.get("OlmInboundGroupSessionEntity")
|
||||
?.transform { obj ->
|
||||
try {
|
||||
val oldSerializedData = obj.getString("olmInboundGroupSessionData")
|
||||
deserializeFromRealm<MXOlmInboundGroupSession2>(oldSerializedData)?.let { mxOlmInboundGroupSession2 ->
|
||||
val newOlmInboundGroupSessionWrapper2 = OlmInboundGroupSessionWrapper2()
|
||||
val sessionKey = mxOlmInboundGroupSession2.mSession.sessionIdentifier()
|
||||
val newOlmInboundGroupSessionWrapper = OlmInboundGroupSessionWrapper(sessionKey, false)
|
||||
.apply {
|
||||
olmInboundGroupSession = mxOlmInboundGroupSession2.mSession
|
||||
roomId = mxOlmInboundGroupSession2.mRoomId
|
||||
@ -191,7 +193,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||
forwardingCurve25519KeyChain = mxOlmInboundGroupSession2.mForwardingCurve25519KeyChain
|
||||
}
|
||||
|
||||
obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper2))
|
||||
obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error")
|
||||
@ -445,4 +447,11 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||
.addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java)
|
||||
.setNullable(SharedSessionEntityFields.CHAIN_INDEX, true)
|
||||
}
|
||||
|
||||
// Version 11L added deviceKeysSentToServer boolean to CryptoMetadataEntity
|
||||
private fun migrateTo11(realm: DynamicRealm) {
|
||||
Timber.d("Step 10 -> 11")
|
||||
realm.schema.get("CryptoMetadataEntity")
|
||||
?.addField(CryptoMetadataEntityFields.DEVICE_KEYS_SENT_TO_SERVER, Boolean::class.java)
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,9 @@ internal open class CryptoMetadataEntity(
|
||||
// The keys backup version currently used. Null means no backup.
|
||||
var backupVersion: String? = null,
|
||||
|
||||
// The device keys has been sent to the homeserver
|
||||
var deviceKeysSentToServer: Boolean = false,
|
||||
|
||||
var xSignMasterPrivateKey: String? = null,
|
||||
var xSignUserPrivateKey: String? = null,
|
||||
var xSignSelfSignedPrivateKey: String? = null,
|
||||
|
@ -177,7 +177,6 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||
val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.root.getClearContent().toModel())
|
||||
val replyFormatted = REPLY_PATTERN.format(
|
||||
permalink,
|
||||
stringProvider.getString(R.string.message_reply_to_prefix),
|
||||
userLink,
|
||||
originalEvent.senderInfo.disambiguatedDisplayName,
|
||||
body.takeFormatted(),
|
||||
@ -372,7 +371,6 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.root.getClearContent().toModel())
|
||||
val replyFormatted = REPLY_PATTERN.format(
|
||||
permalink,
|
||||
stringProvider.getString(R.string.message_reply_to_prefix),
|
||||
userLink,
|
||||
userId,
|
||||
body.takeFormatted(),
|
||||
@ -434,10 +432,10 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||
TextContent(content.body, formattedText)
|
||||
}
|
||||
}
|
||||
MessageType.MSGTYPE_FILE -> return TextContent(stringProvider.getString(R.string.reply_to_a_file))
|
||||
MessageType.MSGTYPE_AUDIO -> return TextContent(stringProvider.getString(R.string.reply_to_an_audio_file))
|
||||
MessageType.MSGTYPE_IMAGE -> return TextContent(stringProvider.getString(R.string.reply_to_an_image))
|
||||
MessageType.MSGTYPE_VIDEO -> return TextContent(stringProvider.getString(R.string.reply_to_a_video))
|
||||
MessageType.MSGTYPE_FILE -> return TextContent("sent a file.")
|
||||
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
|
||||
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
|
||||
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
|
||||
else -> return TextContent(content?.body ?: "")
|
||||
}
|
||||
}
|
||||
@ -489,6 +487,6 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||
// </blockquote>
|
||||
// </mx-reply>
|
||||
// No whitespace because currently breaks temporary formatted text to Span
|
||||
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">%s</a><a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
|
||||
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">In reply to</a> <a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.di.Unauthenticated
|
||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
|
||||
import im.vector.matrix.android.internal.network.ssl.UnrecognizedCertificateException
|
||||
import im.vector.matrix.android.internal.session.homeserver.CapabilitiesAPI
|
||||
import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
@ -106,6 +107,12 @@ internal class DefaultGetWellknownTask @Inject constructor(
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
when (throwable) {
|
||||
is UnrecognizedCertificateException -> {
|
||||
throw Failure.UnrecognizedCertificateFailure(
|
||||
"https://$domain",
|
||||
throwable.fingerprint
|
||||
)
|
||||
}
|
||||
is Failure.NetworkConnection -> {
|
||||
WellknownResult.Ignore
|
||||
}
|
||||
|
@ -63,12 +63,6 @@
|
||||
<string name="summary_user_sent_sticker">أرسل %1$s ملصقا.</string>
|
||||
|
||||
<string name="notice_avatar_changed_too">(تغيّرت الصورة أيضا)</string>
|
||||
<string name="message_reply_to_prefix">ردا على</string>
|
||||
|
||||
<string name="reply_to_an_image">أرسل صورة.</string>
|
||||
<string name="reply_to_a_video">أرسل فديوهًا.</string>
|
||||
<string name="reply_to_an_audio_file">أرسل ملف صوت.</string>
|
||||
<string name="reply_to_a_file">أرسل ملفًا.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">دعوة من %s</string>
|
||||
<string name="room_displayname_empty_room">غرفة فارغة</string>
|
||||
|
@ -52,8 +52,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** Şifrəni aça bilmir: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Göndərənin cihazı bu mesaj üçün açarları bizə göndərməyib.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Cavab olaraq</string>
|
||||
|
||||
<string name="could_not_redact">Redaktə etmək olmur</string>
|
||||
<string name="unable_to_send_message">Mesaj göndərmək olmur</string>
|
||||
|
||||
@ -69,11 +67,6 @@
|
||||
<string name="medium_email">Elektron poçt ünvanı</string>
|
||||
<string name="medium_phone_number">Telefon nömrəsi</string>
|
||||
|
||||
<string name="reply_to_an_image">şəkil göndərdi.</string>
|
||||
<string name="reply_to_a_video">video göndərdi.</string>
|
||||
<string name="reply_to_an_audio_file">səs faylı göndərdi.</string>
|
||||
<string name="reply_to_a_file">fayl göndərdi.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">%s-dən dəvət</string>
|
||||
<string name="room_displayname_room_invite">Otağa dəvət</string>
|
||||
|
||||
|
@ -63,13 +63,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s изпрати стикер.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">В отговор на</string>
|
||||
|
||||
<string name="reply_to_an_image">изпрати снимка.</string>
|
||||
<string name="reply_to_a_video">изпрати видео.</string>
|
||||
<string name="reply_to_an_audio_file">изпрати аудио файл.</string>
|
||||
<string name="reply_to_a_file">изпрати файл.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Покана от %s</string>
|
||||
<string name="room_displayname_room_invite">Покана за стая</string>
|
||||
<string name="room_displayname_two_members">%1$s и %2$s</string>
|
||||
|
@ -76,11 +76,4 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s ha enviat un adhesiu.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">En resposta a</string>
|
||||
|
||||
<string name="reply_to_an_image">ha enviat una imatge.</string>
|
||||
<string name="reply_to_a_video">ha enviat un vídeo.</string>
|
||||
<string name="reply_to_an_audio_file">ha enviat un fitxer d\'àudio.</string>
|
||||
<string name="reply_to_a_file">ha enviat un fitxer.</string>
|
||||
|
||||
</resources>
|
||||
|
@ -46,8 +46,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** Nelze dešifrovat: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Odesílatelovo zařízení neposlalo klíče pro tuto zprávu.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">V odpovědi na</string>
|
||||
|
||||
<string name="could_not_redact">Nelze vymazat</string>
|
||||
<string name="unable_to_send_message">Zprávu nelze odeslat</string>
|
||||
|
||||
@ -63,11 +61,6 @@
|
||||
<string name="medium_email">E-mailová adresa</string>
|
||||
<string name="medium_phone_number">Telefonní číslo</string>
|
||||
|
||||
<string name="reply_to_an_image">odeslal obrázek.</string>
|
||||
<string name="reply_to_a_video">odeslal video.</string>
|
||||
<string name="reply_to_an_audio_file">odeslal zvukový soubor.</string>
|
||||
<string name="reply_to_a_file">odeslal soubor.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Pozvání od %s</string>
|
||||
<string name="room_displayname_room_invite">Pozvání do místnosti</string>
|
||||
|
||||
|
@ -73,13 +73,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s sandte einen Sticker.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Als Antwort auf</string>
|
||||
|
||||
<string name="reply_to_an_image">hat ein Bild gesendet.</string>
|
||||
<string name="reply_to_a_video">hat ein Video gesendet.</string>
|
||||
<string name="reply_to_an_audio_file">hat eine Audio-Datei gesendet.</string>
|
||||
<string name="reply_to_a_file">sandte eine Datei.</string>
|
||||
|
||||
<!-- Room display name -->
|
||||
<string name="room_displayname_invite_from">Einladung von %s</string>
|
||||
<string name="room_displayname_room_invite">Raumeinladung</string>
|
||||
|
@ -40,8 +40,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** Αδυναμία αποκρυπτογράφησης: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Η συσκευή του/της αποστολέα δεν μας έχει στείλει τα κλειδιά για αυτό το μήνυμα.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Προς απάντηση στο</string>
|
||||
|
||||
<string name="unable_to_send_message">Αποτυχία αποστολής μηνύματος</string>
|
||||
|
||||
<string name="message_failed_to_upload">Αποτυχία αναφόρτωσης εικόνας</string>
|
||||
@ -56,10 +54,6 @@
|
||||
<string name="notice_voip_finished">Η VoIP διάσκεψη έληξε</string>
|
||||
|
||||
<string name="notice_room_join">Ο/Η %1$s εισήλθε στο δωμάτιο</string>
|
||||
<string name="reply_to_an_image">έστειλε μία εικόνα.</string>
|
||||
<string name="reply_to_a_video">έστειλε ένα βίντεο.</string>
|
||||
<string name="reply_to_an_audio_file">έστειλε ένα αρχείο ήχου.</string>
|
||||
<string name="reply_to_a_file">έστειλε ένα αρχείο.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Πρόσκληση από %s</string>
|
||||
<string name="room_displayname_room_invite">Πρόσκληση στο δωμάτιο</string>
|
||||
|
@ -17,8 +17,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** Ne eblas malĉifri: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Responde al</string>
|
||||
|
||||
<string name="summary_message">%1$s: %2$s</string>
|
||||
<string name="notice_display_name_set">%1$s ŝanĝis sian vidigan nomon al %2$s</string>
|
||||
<string name="notice_display_name_changed_from">%1$s ŝanĝis sian vidigan nomon de %2$s al %3$s</string>
|
||||
@ -62,11 +60,6 @@
|
||||
<string name="medium_email">Retpoŝtadreso</string>
|
||||
<string name="medium_phone_number">Telefonnumero</string>
|
||||
|
||||
<string name="reply_to_an_image">sendis bildon.</string>
|
||||
<string name="reply_to_a_video">sendis filmon.</string>
|
||||
<string name="reply_to_an_audio_file">sendis sondosieron.</string>
|
||||
<string name="reply_to_a_file">sendis dosieron.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Invito de %s</string>
|
||||
<string name="room_displayname_room_invite">Ĉambra invito</string>
|
||||
|
||||
|
@ -73,13 +73,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s envió una calcomanía.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">En respuesta a</string>
|
||||
|
||||
<string name="reply_to_an_image">envió una imagen.</string>
|
||||
<string name="reply_to_a_video">envió un video.</string>
|
||||
<string name="reply_to_an_audio_file">envió un archivo de audio.</string>
|
||||
<string name="reply_to_a_file">envió un archivo.</string>
|
||||
|
||||
<!-- Room display name -->
|
||||
<string name="room_displayname_invite_from">Invitación de %s</string>
|
||||
<string name="room_displayname_room_invite">Invitación de Sala</string>
|
||||
|
@ -73,13 +73,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s envió una pegatina.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">En respuesta a</string>
|
||||
|
||||
<string name="reply_to_an_image">envió una imagen.</string>
|
||||
<string name="reply_to_a_video">envió un vídeo.</string>
|
||||
<string name="reply_to_an_audio_file">envió un archivo de audio.</string>
|
||||
<string name="reply_to_a_file">envió un archivo.</string>
|
||||
|
||||
<!-- Room display name -->
|
||||
<string name="room_displayname_invite_from">Invitación de %s</string>
|
||||
<string name="room_displayname_room_invite">Invitación a Sala</string>
|
||||
|
@ -50,8 +50,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** Ei õnnestu dekrüptida: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Vastuseks kasutajale</string>
|
||||
|
||||
<string name="could_not_redact">Ei saanud muuta sõnumit</string>
|
||||
<string name="unable_to_send_message">Sõnumi saatmine ei õnnestunud</string>
|
||||
|
||||
@ -67,11 +65,6 @@
|
||||
<string name="medium_email">E-posti aadress</string>
|
||||
<string name="medium_phone_number">Telefoninumber</string>
|
||||
|
||||
<string name="reply_to_an_image">saatis pildi.</string>
|
||||
<string name="reply_to_a_video">saatis video.</string>
|
||||
<string name="reply_to_an_audio_file">saatis helifaili.</string>
|
||||
<string name="reply_to_a_file">saatis faili.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Kutse kasutajalt %s</string>
|
||||
<string name="room_displayname_room_invite">Kutse jututuppa</string>
|
||||
|
||||
|
@ -63,13 +63,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s erabiltzaileak eranskailu bat bidali du.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Honi erantzunez</string>
|
||||
|
||||
<string name="reply_to_an_image">irudi bat bidali du.</string>
|
||||
<string name="reply_to_a_video">bideo bat bidali du.</string>
|
||||
<string name="reply_to_an_audio_file">audio fitxategi bat bidali du.</string>
|
||||
<string name="reply_to_a_file">fitxategi bat bidali du.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">%s gelarako gonbidapena</string>
|
||||
<string name="room_displayname_room_invite">Gela gonbidapena</string>
|
||||
<string name="room_displayname_two_members">%1$s eta %2$s</string>
|
||||
|
@ -51,8 +51,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** ناتوان در رمزگشایی: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">دستگاه فرستنده، کلیدهای این پیام را برایمان نفرستاده است.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">در پاسخ به</string>
|
||||
|
||||
<string name="unable_to_send_message">ناتوان در فرستادن پیام</string>
|
||||
|
||||
<string name="message_failed_to_upload">شکست در بارگذاری تصویر</string>
|
||||
@ -67,11 +65,6 @@
|
||||
<string name="medium_email">نشانی رایانامه</string>
|
||||
<string name="medium_phone_number">شماره تلفن</string>
|
||||
|
||||
<string name="reply_to_an_image">تصویری فرستاد.</string>
|
||||
<string name="reply_to_a_video">ویدیویی فرستاد.</string>
|
||||
<string name="reply_to_an_audio_file">پروندهای صوتی فرستاد.</string>
|
||||
<string name="reply_to_a_file">پروندهای فرستاد.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">دعوت از %s</string>
|
||||
<string name="room_displayname_room_invite">دعوت اتاق</string>
|
||||
|
||||
|
@ -70,13 +70,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s lähetti tarran.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Vastauksena käyttäjälle</string>
|
||||
|
||||
<string name="reply_to_an_image">oli lähettänyt kuvan.</string>
|
||||
<string name="reply_to_a_video">lähetti videon.</string>
|
||||
<string name="reply_to_an_audio_file">lähetti äänitiedoston.</string>
|
||||
<string name="reply_to_a_file">lähetti tiedoston.</string>
|
||||
|
||||
<plurals name="room_displayname_three_and_more_members">
|
||||
<item quantity="one">%1$s ja yksi muu</item>
|
||||
<item quantity="other">%1$s ja %2$d muuta</item>
|
||||
|
@ -63,13 +63,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s a envoyé un sticker.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">En réponse à</string>
|
||||
|
||||
<string name="reply_to_an_image">a envoyé une image.</string>
|
||||
<string name="reply_to_a_video">a envoyé une vidéo.</string>
|
||||
<string name="reply_to_an_audio_file">a envoyé un fichier audio.</string>
|
||||
<string name="reply_to_a_file">a envoyé un fichier.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Invitation de %s</string>
|
||||
<string name="room_displayname_room_invite">Invitation au salon</string>
|
||||
<string name="room_displayname_empty_room">Salon vide</string>
|
||||
|
@ -50,8 +50,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** Imposíbel descifrar: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">O dispositivo do que envía non enviou as chaves desta mensaxe.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Respondéndolle a</string>
|
||||
|
||||
<string name="could_not_redact">Non se puido redactar</string>
|
||||
<string name="unable_to_send_message">Non foi posíbel enviar a mensaxe</string>
|
||||
|
||||
@ -64,11 +62,6 @@
|
||||
|
||||
<string name="medium_phone_number">Número de teléfono</string>
|
||||
|
||||
<string name="reply_to_an_image">Responder a</string>
|
||||
<string name="reply_to_a_video">enviar un vídeo.</string>
|
||||
<string name="reply_to_an_audio_file">enviar un ficheiro de son.</string>
|
||||
<string name="reply_to_a_file">enviar un ficheiro.</string>
|
||||
|
||||
<string name="room_displayname_two_members">%1$s e %2$s</string>
|
||||
|
||||
|
||||
|
@ -62,13 +62,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s küldött egy matricát.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Válasz erre:</string>
|
||||
|
||||
<string name="reply_to_an_image">képet küldött.</string>
|
||||
<string name="reply_to_a_video">videót küldött.</string>
|
||||
<string name="reply_to_an_audio_file">hangfájlt küldött.</string>
|
||||
<string name="reply_to_a_file">fájlt küldött.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Meghívó tőle: %s</string>
|
||||
<string name="room_displayname_room_invite">Meghívó egy szobába</string>
|
||||
<string name="room_displayname_two_members">%1$s és %2$s</string>
|
||||
|
@ -24,7 +24,6 @@
|
||||
|
||||
<string name="notice_avatar_changed_too">(einnig var skipt um auðkennismynd)</string>
|
||||
<string name="notice_crypto_unable_to_decrypt">** Mistókst að afkóða: %s **</string>
|
||||
<string name="message_reply_to_prefix">Sem svar til</string>
|
||||
|
||||
<string name="unable_to_send_message">Gat ekki sent skilaboð</string>
|
||||
|
||||
|
@ -62,13 +62,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s ha inviato un adesivo.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">In risposta a</string>
|
||||
|
||||
<string name="reply_to_an_image">inviata un\'immagine.</string>
|
||||
<string name="reply_to_a_video">inviato un video.</string>
|
||||
<string name="reply_to_an_audio_file">inviato un file audio.</string>
|
||||
<string name="reply_to_a_file">inviato un file.</string>
|
||||
|
||||
<!-- Room display name -->
|
||||
<string name="room_displayname_invite_from">Invito da %s</string>
|
||||
<string name="room_displayname_room_invite">Invito nella stanza</string>
|
||||
|
@ -56,8 +56,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** 解読できません: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">送信者の端末からこのメッセージのキーが送信されていません。</string>
|
||||
|
||||
<string name="message_reply_to_prefix">に返信</string>
|
||||
|
||||
<string name="could_not_redact">修正できませんでした</string>
|
||||
<string name="unable_to_send_message">メッセージを送信できません</string>
|
||||
|
||||
@ -73,9 +71,4 @@
|
||||
<string name="medium_email">メールアドレス</string>
|
||||
<string name="medium_phone_number">電話番号</string>
|
||||
|
||||
<string name="reply_to_an_image">画像を送信しました。</string>
|
||||
<string name="reply_to_a_video">動画を送りました。</string>
|
||||
<string name="reply_to_an_audio_file">音声ファイルを送信しました。</string>
|
||||
<string name="reply_to_a_file">ファイルを送信しました。</string>
|
||||
|
||||
</resources>
|
||||
|
@ -52,8 +52,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** 암호를 복호화할 수 없음: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">관련 대화</string>
|
||||
|
||||
<string name="could_not_redact">검열할 수 없습니다</string>
|
||||
<string name="unable_to_send_message">메시지를 보낼 수 없습니다</string>
|
||||
|
||||
@ -69,11 +67,6 @@
|
||||
<string name="medium_email">이메일 주소</string>
|
||||
<string name="medium_phone_number">전화번호</string>
|
||||
|
||||
<string name="reply_to_an_image">사진을 보냈습니다.</string>
|
||||
<string name="reply_to_a_video">동영상을 보냈습니다.</string>
|
||||
<string name="reply_to_an_audio_file">오디오 파일을 보냈습니다.</string>
|
||||
<string name="reply_to_a_file">파일을 보냈습니다.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">%s에서 초대함</string>
|
||||
<string name="room_displayname_room_invite">방 초대</string>
|
||||
|
||||
|
@ -71,13 +71,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s heeft een sticker gestuurd.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Als antwoord op</string>
|
||||
|
||||
<string name="reply_to_an_image">heeft een afbeelding gestuurd.</string>
|
||||
<string name="reply_to_a_video">heeft een video gestuurd.</string>
|
||||
<string name="reply_to_an_audio_file">heeft een audiobestand gestuurd.</string>
|
||||
<string name="reply_to_a_file">heeft een bestand gestuurd.</string>
|
||||
|
||||
<!-- Room display name -->
|
||||
<string name="room_displayname_invite_from">Uitnodiging van %s</string>
|
||||
<string name="room_displayname_room_invite">Gespreksuitnodiging</string>
|
||||
|
@ -49,8 +49,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** Fekk ikkje til å dekryptera: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Avsendareiningi hev ikkje sendt oss nyklane fyr denna meldingi.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Som svar til</string>
|
||||
|
||||
<string name="could_not_redact">Kunde ikkje gjera um</string>
|
||||
<string name="unable_to_send_message">Fekk ikkje å senda meldingi</string>
|
||||
|
||||
@ -64,11 +62,6 @@
|
||||
<string name="medium_email">Epostadresse</string>
|
||||
<string name="medium_phone_number">Telefonnummer</string>
|
||||
|
||||
<string name="reply_to_an_image">sende eit bilæte.</string>
|
||||
<string name="reply_to_a_video">sende ein video.</string>
|
||||
<string name="reply_to_an_audio_file">sende ei ljodfil.</string>
|
||||
<string name="reply_to_a_file">sende ei fil.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Innbjoding frå %s</string>
|
||||
<string name="room_displayname_room_invite">Rominnbjoding</string>
|
||||
<string name="room_displayname_two_members">%1$s og %2$s</string>
|
||||
|
@ -42,7 +42,6 @@
|
||||
<string name="notice_room_withdraw">%1$s wycofał(a) zaproszenie %2$s</string>
|
||||
<string name="notice_answered_call">%s odebrał(a) połączenie.</string>
|
||||
<string name="notice_avatar_changed_too">(awatar też został zmieniony)</string>
|
||||
<string name="message_reply_to_prefix">W odpowiedzi do</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Zaproszenie od %s</string>
|
||||
<string name="room_displayname_room_invite">Zaproszenie do pokoju</string>
|
||||
@ -76,11 +75,6 @@
|
||||
<string name="could_not_redact">Nie można zredagować</string>
|
||||
<string name="room_error_join_failed_empty_room">Obecnie nie jest możliwe ponowne dołączenie do pustego pokoju.</string>
|
||||
|
||||
<string name="reply_to_an_image">wyślij zdjęcie.</string>
|
||||
<string name="reply_to_a_video">wyślij wideo.</string>
|
||||
<string name="reply_to_an_audio_file">wyślij plik audio.</string>
|
||||
<string name="reply_to_a_file">wyślij plik.</string>
|
||||
|
||||
<string name="notice_event_redacted">Wiadomość usunięta</string>
|
||||
<string name="notice_event_redacted_by">Wiadomość usunięta przez %1$s</string>
|
||||
<string name="notice_event_redacted_with_reason">Wiadomość usunięta [powód: %1$s]</string>
|
||||
|
@ -74,13 +74,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s enviou um sticker.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Em resposta a</string>
|
||||
|
||||
<string name="reply_to_an_image">enviou uma imagem.</string>
|
||||
<string name="reply_to_a_video">enviou um vídeo.</string>
|
||||
<string name="reply_to_an_audio_file">enviou um arquivo de áudio.</string>
|
||||
<string name="reply_to_a_file">enviou um arquivo.</string>
|
||||
|
||||
<!-- Room display name -->
|
||||
<string name="room_displayname_invite_from">Convite de %s</string>
|
||||
<string name="room_displayname_room_invite">Convite para sala</string>
|
||||
|
@ -73,13 +73,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s отправил стикер.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">В ответ на</string>
|
||||
|
||||
<string name="reply_to_an_image">отправил изображение.</string>
|
||||
<string name="reply_to_a_video">отправил видео.</string>
|
||||
<string name="reply_to_an_audio_file">отправил аудиофайл.</string>
|
||||
<string name="reply_to_a_file">отправил файл.</string>
|
||||
|
||||
<!-- Room display name -->
|
||||
<string name="room_displayname_invite_from">Приглашение от %s</string>
|
||||
<string name="room_displayname_room_invite">Приглашение в комнату</string>
|
||||
|
@ -62,13 +62,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s poslal nálepku.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Odpoveď na</string>
|
||||
|
||||
<string name="reply_to_an_image">odoslal obrázok.</string>
|
||||
<string name="reply_to_a_video">odoslal video.</string>
|
||||
<string name="reply_to_an_audio_file">odoslal zvukový súbor.</string>
|
||||
<string name="reply_to_a_file">Odoslal súbor.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Pozvanie od %s</string>
|
||||
<string name="room_displayname_room_invite">Pozvanie do miestnosti</string>
|
||||
<string name="room_displayname_two_members">%1$s a %2$s</string>
|
||||
|
@ -34,8 +34,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** S’arrihet të shfshehtëzohet: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Pajisja e dërguesit nuk na ka dërguar kyçet për këtë mesazh.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Në përgjigje të</string>
|
||||
|
||||
<string name="could_not_redact">S’u redaktua dot</string>
|
||||
<string name="unable_to_send_message">S’arrihet të dërgohet mesazh</string>
|
||||
|
||||
@ -51,11 +49,6 @@
|
||||
<string name="medium_email">Adresë email</string>
|
||||
<string name="medium_phone_number">Numër telefoni</string>
|
||||
|
||||
<string name="reply_to_an_image">dërgoi një figurë.</string>
|
||||
<string name="reply_to_a_video">dërgoi një video.</string>
|
||||
<string name="reply_to_an_audio_file">dërgoi një kartelë audio.</string>
|
||||
<string name="reply_to_a_file">dërgoi një kartelë.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Ftesë nga %s</string>
|
||||
<string name="room_displayname_room_invite">Ftesë Dhome</string>
|
||||
|
||||
|
@ -56,8 +56,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** Неможливо розшифрувати: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Пристрій відправника не надіслав нам ключ для цього повідомлення.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">У відповідь на</string>
|
||||
|
||||
<string name="could_not_redact">Неможливо відредагувати</string>
|
||||
<string name="unable_to_send_message">Не вдалося надіслати повідомлення</string>
|
||||
|
||||
@ -71,11 +69,6 @@
|
||||
<string name="medium_email">Адреса електронної пошти</string>
|
||||
<string name="medium_phone_number">Номер телефону</string>
|
||||
|
||||
<string name="reply_to_an_image">надіслав зображення.</string>
|
||||
<string name="reply_to_a_video">надіслав відео.</string>
|
||||
<string name="reply_to_an_audio_file">надіслав аудіо файл.</string>
|
||||
<string name="reply_to_a_file">надіслав файл.</string>
|
||||
|
||||
<plurals name="room_displayname_three_and_more_members">
|
||||
<item quantity="one">%1$s та 1 інший</item>
|
||||
<item quantity="few">%1$s та %2$d інші</item>
|
||||
|
@ -50,8 +50,6 @@
|
||||
<string name="notice_crypto_unable_to_decrypt">** Kun nie ountsleuteln: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">’t Toestel van den afzender èt geen sleutels vo da bericht hier gesteurd.</string>
|
||||
|
||||
<string name="message_reply_to_prefix">Als antwoord ip</string>
|
||||
|
||||
<string name="could_not_redact">Kosteg nie verwyderd wordn</string>
|
||||
<string name="unable_to_send_message">Kosteg ’t bericht nie verzendn</string>
|
||||
|
||||
@ -67,11 +65,6 @@
|
||||
<string name="medium_email">E-mailadresse</string>
|
||||
<string name="medium_phone_number">Telefongnumero</string>
|
||||
|
||||
<string name="reply_to_an_image">èt e fotootje gesteurd.</string>
|
||||
<string name="reply_to_a_video">èt e filmtje gesteurd.</string>
|
||||
<string name="reply_to_an_audio_file">èt e geluudsfragment gesteurd.</string>
|
||||
<string name="reply_to_a_file">èt e bestand gesteurd.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Uutnodigienge van %s</string>
|
||||
<string name="room_displayname_room_invite">Gespreksuutnodigienge</string>
|
||||
|
||||
|
@ -63,13 +63,6 @@
|
||||
<string name="summary_message">%1$s:%2$s</string>
|
||||
<string name="summary_user_sent_sticker">%1$s 发送了一张贴纸。</string>
|
||||
|
||||
<string name="reply_to_an_image">发送了一张图片。</string>
|
||||
<string name="reply_to_a_video">发送了一个视频。</string>
|
||||
<string name="reply_to_an_audio_file">发送了一段音频。</string>
|
||||
<string name="reply_to_a_file">发送了一个文件。</string>
|
||||
|
||||
<string name="message_reply_to_prefix">回复</string>
|
||||
|
||||
<string name="room_displayname_empty_room">空聊天室</string>
|
||||
<string name="room_displayname_invite_from">来自 %s 的邀请</string>
|
||||
<string name="room_displayname_room_invite">聊天室邀请</string>
|
||||
|
@ -62,13 +62,6 @@
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s 傳送了一張貼圖。</string>
|
||||
|
||||
<string name="message_reply_to_prefix">回覆</string>
|
||||
|
||||
<string name="reply_to_an_image">傳送了圖片。</string>
|
||||
<string name="reply_to_a_video">傳送了影片。</string>
|
||||
<string name="reply_to_an_audio_file">傳送了音訊檔案。</string>
|
||||
<string name="reply_to_a_file">傳送了檔案。</string>
|
||||
|
||||
<string name="room_displayname_invite_from">來自%s 的邀請</string>
|
||||
<string name="room_displayname_room_invite">聊天室邀請</string>
|
||||
<string name="room_displayname_two_members">%1$s 和 %2$s</string>
|
||||
|
@ -112,7 +112,6 @@
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">The sender\'s device has not sent us the keys for this message.</string>
|
||||
|
||||
<!-- Messages -->
|
||||
<string name="message_reply_to_prefix">In reply to</string>
|
||||
|
||||
<!-- Room Screen -->
|
||||
<string name="could_not_redact">Could not redact</string>
|
||||
@ -139,12 +138,6 @@
|
||||
<string name="medium_email">Email address</string>
|
||||
<string name="medium_phone_number">Phone number</string>
|
||||
|
||||
<!-- Reply to -->
|
||||
<string name="reply_to_an_image">sent an image.</string>
|
||||
<string name="reply_to_a_video">sent a video.</string>
|
||||
<string name="reply_to_an_audio_file">sent an audio file.</string>
|
||||
<string name="reply_to_a_file">sent a file.</string>
|
||||
|
||||
<!-- Room display name -->
|
||||
<string name="room_displayname_invite_from">Invite from %s</string>
|
||||
<string name="room_displayname_room_invite">Room Invite</string>
|
||||
|
@ -17,7 +17,7 @@ androidExtensions {
|
||||
// Note: 2 digits max for each value
|
||||
ext.versionMajor = 0
|
||||
ext.versionMinor = 91
|
||||
ext.versionPatch = 3
|
||||
ext.versionPatch = 4
|
||||
|
||||
static def getGitTimestamp() {
|
||||
def cmd = 'git show -s --format=%ct'
|
||||
@ -106,6 +106,11 @@ def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
|
||||
// Due to a bug introduced in Android gradle plugin 3.6.0, we have to specify the ndk version to use
|
||||
// Ref: https://issuetracker.google.com/issues/144111441
|
||||
ndkVersion "21.3.6528147"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "im.vector.app"
|
||||
// Set to API 21: see #405
|
||||
@ -232,8 +237,7 @@ android {
|
||||
lintOptions {
|
||||
lintConfig file("lint.xml")
|
||||
|
||||
// TODO Restore true once pb with WorkManager is fixed
|
||||
abortOnError false
|
||||
abortOnError true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
|
5
vector/proguard-rules.pro
vendored
5
vector/proguard-rules.pro
vendored
@ -20,4 +20,7 @@
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep class im.vector.riotx.features.** { *; }
|
||||
-keep class im.vector.riotx.features.** { *; }
|
||||
|
||||
## print all the rules in a file
|
||||
# -printconfiguration ../proguard_files/full-r8-config.txt
|
||||
|
@ -247,6 +247,12 @@
|
||||
|
||||
<!-- Providers -->
|
||||
|
||||
<!-- Remove WorkManagerInitializer Provider because we are using on-demand initialization of WorkManager-->
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
tools:node="remove" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileProvider"
|
||||
|
@ -63,7 +63,13 @@ import java.util.Locale
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
|
||||
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {
|
||||
import androidx.work.Configuration as WorkConfiguration
|
||||
|
||||
class VectorApplication :
|
||||
Application(),
|
||||
HasVectorInjector,
|
||||
MatrixConfiguration.Provider,
|
||||
WorkConfiguration.Provider {
|
||||
|
||||
lateinit var appContext: Context
|
||||
@Inject lateinit var legacySessionImporter: LegacySessionImporter
|
||||
@ -85,6 +91,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||
|
||||
lateinit var vectorComponent: VectorComponent
|
||||
|
||||
// font thread handler
|
||||
private var fontThreadHandler: Handler? = null
|
||||
|
||||
@ -157,7 +164,11 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||
|
||||
override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION)
|
||||
|
||||
override fun getWorkManagerConfiguration() = androidx.work.Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build()
|
||||
override fun getWorkManagerConfiguration(): WorkConfiguration {
|
||||
return WorkConfiguration.Builder()
|
||||
.setExecutor(Executors.newCachedThreadPool())
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun injector(): VectorComponent {
|
||||
return vectorComponent
|
||||
|
@ -17,29 +17,39 @@
|
||||
package im.vector.riotx.core.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.DownloadManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Browser
|
||||
import android.provider.MediaStore
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.browser.customtabs.CustomTabsSession
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.riotx.BuildConfig
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.features.notifications.NotificationUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
@ -301,42 +311,20 @@ fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
|
||||
|
||||
fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?, notificationUtils: NotificationUtils) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val externalContentUri: Uri
|
||||
val values = ContentValues()
|
||||
when {
|
||||
mediaMimeType?.startsWith("image/") == true -> {
|
||||
externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
values.put(MediaStore.Images.Media.TITLE, title)
|
||||
values.put(MediaStore.Images.Media.DISPLAY_NAME, title)
|
||||
values.put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType)
|
||||
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
|
||||
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||
}
|
||||
mediaMimeType?.startsWith("video/") == true -> {
|
||||
externalContentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
values.put(MediaStore.Video.Media.TITLE, title)
|
||||
values.put(MediaStore.Video.Media.DISPLAY_NAME, title)
|
||||
values.put(MediaStore.Video.Media.MIME_TYPE, mediaMimeType)
|
||||
values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis())
|
||||
values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||
}
|
||||
mediaMimeType?.startsWith("audio/") == true -> {
|
||||
externalContentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
values.put(MediaStore.Audio.Media.TITLE, title)
|
||||
values.put(MediaStore.Audio.Media.DISPLAY_NAME, title)
|
||||
values.put(MediaStore.Audio.Media.MIME_TYPE, mediaMimeType)
|
||||
values.put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis())
|
||||
values.put(MediaStore.Audio.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||
}
|
||||
else -> {
|
||||
externalContentUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||
values.put(MediaStore.Downloads.TITLE, title)
|
||||
values.put(MediaStore.Downloads.DISPLAY_NAME, title)
|
||||
values.put(MediaStore.Downloads.MIME_TYPE, mediaMimeType)
|
||||
values.put(MediaStore.Downloads.DATE_ADDED, System.currentTimeMillis())
|
||||
values.put(MediaStore.Downloads.DATE_TAKEN, System.currentTimeMillis())
|
||||
}
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.Images.Media.TITLE, title)
|
||||
put(MediaStore.Images.Media.DISPLAY_NAME, title)
|
||||
put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType)
|
||||
put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
|
||||
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||
}
|
||||
val externalContentUri = when {
|
||||
mediaMimeType?.startsWith("image/") == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
mediaMimeType?.startsWith("video/") == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
mediaMimeType?.startsWith("audio/") == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||
}
|
||||
|
||||
val uri = context.contentResolver.insert(externalContentUri, values)
|
||||
if (uri == null) {
|
||||
Toast.makeText(context, R.string.error_saving_media_file, Toast.LENGTH_LONG).show()
|
||||
@ -357,16 +345,70 @@ fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String
|
||||
notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification)
|
||||
}
|
||||
}
|
||||
// TODO add notification?
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).also { mediaScanIntent ->
|
||||
mediaScanIntent.data = Uri.fromFile(file)
|
||||
context.sendBroadcast(mediaScanIntent)
|
||||
saveMediaLegacy(context, mediaMimeType, title, file)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: String, file: File) {
|
||||
val state = Environment.getExternalStorageState()
|
||||
if (Environment.MEDIA_MOUNTED != state) {
|
||||
context.toast(context.getString(R.string.error_saving_media_file))
|
||||
return
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val dest = when {
|
||||
mediaMimeType?.startsWith("image/") == true -> Environment.DIRECTORY_PICTURES
|
||||
mediaMimeType?.startsWith("video/") == true -> Environment.DIRECTORY_MOVIES
|
||||
mediaMimeType?.startsWith("audio/") == true -> Environment.DIRECTORY_MUSIC
|
||||
else -> Environment.DIRECTORY_DOWNLOADS
|
||||
}
|
||||
val downloadDir = Environment.getExternalStoragePublicDirectory(dest)
|
||||
try {
|
||||
val outputFilename = if (title.substringAfterLast('.', "").isEmpty()) {
|
||||
val extension = mediaMimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) }
|
||||
"$title.$extension"
|
||||
} else {
|
||||
title
|
||||
}
|
||||
val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename)
|
||||
if (savedFile != null) {
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as? DownloadManager
|
||||
downloadManager?.addCompletedDownload(
|
||||
savedFile.name,
|
||||
title,
|
||||
true,
|
||||
mediaMimeType ?: "application/octet-stream",
|
||||
savedFile.absolutePath,
|
||||
savedFile.length(),
|
||||
true)
|
||||
addToGallery(savedFile, mediaMimeType, context)
|
||||
}
|
||||
} catch (error: Throwable) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
context.toast(context.getString(R.string.error_saving_media_file))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToGallery(savedFile: File, mediaMimeType: String?, context: Context) {
|
||||
// MediaScannerConnection provides a way for applications to pass a newly created or downloaded media file to the media scanner service.
|
||||
var mediaConnection: MediaScannerConnection? = null
|
||||
val mediaScannerConnectionClient: MediaScannerConnection.MediaScannerConnectionClient = object : MediaScannerConnection.MediaScannerConnectionClient {
|
||||
override fun onMediaScannerConnected() {
|
||||
mediaConnection?.scanFile(savedFile.path, mediaMimeType)
|
||||
}
|
||||
|
||||
override fun onScanCompleted(path: String, uri: Uri?) {
|
||||
if (path == savedFile.path) mediaConnection?.disconnect()
|
||||
}
|
||||
}
|
||||
mediaConnection = MediaScannerConnection(context, mediaScannerConnectionClient).apply { connect() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the play store to the provided application Id, default to this app
|
||||
*/
|
||||
@ -381,3 +423,76 @@ fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================================================================
|
||||
// Media utils
|
||||
// ==============================================================================================================
|
||||
/**
|
||||
* Copy a file into a dstPath directory.
|
||||
* The output filename can be provided.
|
||||
* The output file is not overridden if it is already exist.
|
||||
*
|
||||
* ~~ This is copied from the old matrix sdk ~~
|
||||
*
|
||||
* @param sourceFile the file source path
|
||||
* @param dstDirPath the dst path
|
||||
* @param outputFilename optional the output filename
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: String?): File? {
|
||||
// defines another name for the external media
|
||||
val dstFileName: String
|
||||
|
||||
// build a filename is not provided
|
||||
if (null == outputFilename) {
|
||||
// extract the file extension from the uri
|
||||
val dotPos = sourceFile.name.lastIndexOf(".")
|
||||
var fileExt = ""
|
||||
if (dotPos > 0) {
|
||||
fileExt = sourceFile.name.substring(dotPos)
|
||||
}
|
||||
dstFileName = "vector_" + System.currentTimeMillis() + fileExt
|
||||
} else {
|
||||
dstFileName = outputFilename
|
||||
}
|
||||
|
||||
var dstFile = File(dstDirPath, dstFileName)
|
||||
|
||||
// if the file already exists, append a marker
|
||||
if (dstFile.exists()) {
|
||||
var baseFileName = dstFileName
|
||||
var fileExt = ""
|
||||
val lastDotPos = dstFileName.lastIndexOf(".")
|
||||
if (lastDotPos > 0) {
|
||||
baseFileName = dstFileName.substring(0, lastDotPos)
|
||||
fileExt = dstFileName.substring(lastDotPos)
|
||||
}
|
||||
var counter = 1
|
||||
while (dstFile.exists()) {
|
||||
dstFile = File(dstDirPath, "$baseFileName($counter)$fileExt")
|
||||
counter++
|
||||
}
|
||||
}
|
||||
|
||||
// Copy source file to destination
|
||||
var inputStream: FileInputStream? = null
|
||||
var outputStream: FileOutputStream? = null
|
||||
try {
|
||||
dstFile.createNewFile()
|
||||
inputStream = FileInputStream(sourceFile)
|
||||
outputStream = FileOutputStream(dstFile)
|
||||
val buffer = ByteArray(1024 * 10)
|
||||
var len: Int
|
||||
while (inputStream.read(buffer).also { len = it } != -1) {
|
||||
outputStream.write(buffer, 0, len)
|
||||
}
|
||||
return dstFile
|
||||
} catch (failure: Throwable) {
|
||||
return null
|
||||
} finally {
|
||||
// Close resources
|
||||
tryThis { inputStream?.close() }
|
||||
tryThis { outputStream?.close() }
|
||||
}
|
||||
}
|
||||
|
@ -37,3 +37,11 @@ internal fun String.ensureProtocol(): String {
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
internal fun String.ensureTrailingSlash(): String {
|
||||
return when {
|
||||
isEmpty() -> this
|
||||
!endsWith("/") -> "$this/"
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ abstract class FormEditTextWithButtonItem : VectorEpoxyModel<FormEditTextWithBut
|
||||
}
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.textInputLayout.isEnabled = enabled
|
||||
holder.textInputLayout.hint = hint
|
||||
|
||||
|
@ -22,6 +22,7 @@ import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.text.Spannable
|
||||
@ -222,6 +223,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
|
||||
private const val AUDIO_CALL_PERMISSION_REQUEST_CODE = 1
|
||||
private const val VIDEO_CALL_PERMISSION_REQUEST_CODE = 2
|
||||
private const val SAVE_ATTACHEMENT_REQUEST_CODE = 3
|
||||
|
||||
/**
|
||||
* Sanitize the display name.
|
||||
@ -1194,17 +1196,12 @@ class RoomDetailFragment @Inject constructor(
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
if (allGranted(grantResults)) {
|
||||
when (requestCode) {
|
||||
// PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> {
|
||||
// val action = roomDetailViewModel.pendingAction
|
||||
// if (action != null) {
|
||||
// (action as? RoomDetailAction.DownloadFile)
|
||||
// ?.messageFileContent
|
||||
// ?.getFileName()
|
||||
// ?.let { showSnackWithMessage(getString(R.string.downloading_file, it)) }
|
||||
// roomDetailViewModel.pendingAction = null
|
||||
// roomDetailViewModel.handle(action)
|
||||
// }
|
||||
// }
|
||||
SAVE_ATTACHEMENT_REQUEST_CODE -> {
|
||||
sharedActionViewModel.pendingAction?.let {
|
||||
handleActions(it)
|
||||
sharedActionViewModel.pendingAction = null
|
||||
}
|
||||
}
|
||||
PERMISSION_REQUEST_CODE_INCOMING_URI -> {
|
||||
val pendingUri = roomDetailViewModel.pendingUri
|
||||
if (pendingUri != null) {
|
||||
@ -1357,6 +1354,11 @@ class RoomDetailFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun onSaveActionClicked(action: EventSharedAction.Save) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
|
||||
&& !checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, SAVE_ATTACHEMENT_REQUEST_CODE)) {
|
||||
sharedActionViewModel.pendingAction = action
|
||||
return
|
||||
}
|
||||
session.fileService().downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
||||
id = action.eventId,
|
||||
|
@ -21,4 +21,6 @@ import javax.inject.Inject
|
||||
/**
|
||||
* Activity shared view model to handle message actions
|
||||
*/
|
||||
class MessageSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<EventSharedAction>()
|
||||
class MessageSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<EventSharedAction>() {
|
||||
var pendingAction : EventSharedAction? = null
|
||||
}
|
||||
|
@ -73,6 +73,9 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
|
||||
|
||||
override fun showFailure(throwable: Throwable) {
|
||||
when (throwable) {
|
||||
is Failure.Cancelled ->
|
||||
/* Ignore this error, user has cancelled the action */
|
||||
Unit
|
||||
is Failure.ServerError ->
|
||||
if (throwable.error.code == MatrixError.M_FORBIDDEN
|
||||
&& throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
|
||||
|
@ -151,8 +151,8 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
// TODO Disabled because it provokes a flickering
|
||||
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
||||
})
|
||||
is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone()
|
||||
is LoginViewEvents.OnSignModeSelected -> onSignModeSelected()
|
||||
is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone(loginViewEvents)
|
||||
is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents)
|
||||
is LoginViewEvents.OnLoginFlowRetrieved ->
|
||||
addFragmentToBackstack(R.id.loginFragmentContainer,
|
||||
if (loginViewEvents.isSso) {
|
||||
@ -228,18 +228,20 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onServerSelectionDone() = withState(loginViewModel) { state ->
|
||||
when (state.serverType) {
|
||||
private fun onServerSelectionDone(loginViewEvents: LoginViewEvents.OnServerSelectionDone) {
|
||||
when (loginViewEvents.serverType) {
|
||||
ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow
|
||||
ServerType.Modular,
|
||||
ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer,
|
||||
LoginServerUrlFormFragment::class.java,
|
||||
option = commonOption)
|
||||
ServerType.Unknown -> Unit /* Should not happen */
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSignModeSelected() = withState(loginViewModel) { state ->
|
||||
when (state.signMode) {
|
||||
private fun onSignModeSelected(loginViewEvents: LoginViewEvents.OnSignModeSelected) = withState(loginViewModel) { state ->
|
||||
// state.signMode could not be ready yet. So use value from the ViewEvent
|
||||
when (loginViewEvents.signMode) {
|
||||
SignMode.Unknown -> error("Sign mode has to be set before calling this method")
|
||||
SignMode.SignUp -> {
|
||||
// This is managed by the LoginViewEvents
|
||||
|
@ -54,6 +54,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
|
||||
|
||||
private var passwordShown = false
|
||||
private var isSignupMode = false
|
||||
|
||||
// Temporary patch for https://github.com/vector-im/riotX-android/issues/1410,
|
||||
// waiting for https://github.com/matrix-org/synapse/issues/7576
|
||||
private var isNumericOnlyUserIdForbidden = false
|
||||
@ -138,6 +139,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
|
||||
loginServerIcon.isVisible = false
|
||||
loginTitle.text = getString(R.string.login_signin_matrix_id_title)
|
||||
loginNotice.text = getString(R.string.login_signin_matrix_id_notice)
|
||||
loginPasswordNotice.isVisible = true
|
||||
} else {
|
||||
val resId = when (state.signMode) {
|
||||
SignMode.Unknown -> error("developer error")
|
||||
@ -164,7 +166,9 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
|
||||
loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl())
|
||||
loginNotice.text = getString(R.string.login_server_other_text)
|
||||
}
|
||||
ServerType.Unknown -> Unit /* Should not happen */
|
||||
}
|
||||
loginPasswordNotice.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,6 @@ package im.vector.riotx.features.login
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import butterknife.OnClick
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.utils.openUrlInChromeCustomTab
|
||||
import kotlinx.android.synthetic.main.fragment_login_server_selection.*
|
||||
@ -40,11 +39,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
|
||||
}
|
||||
|
||||
private fun updateSelectedChoice(state: LoginViewState) {
|
||||
state.serverType.let {
|
||||
loginServerChoiceMatrixOrg.isChecked = it == ServerType.MatrixOrg
|
||||
loginServerChoiceModular.isChecked = it == ServerType.Modular
|
||||
loginServerChoiceOther.isChecked = it == ServerType.Other
|
||||
}
|
||||
loginServerChoiceMatrixOrg.isChecked = state.serverType == ServerType.MatrixOrg
|
||||
}
|
||||
|
||||
private fun initTextViews() {
|
||||
@ -61,42 +56,17 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
|
||||
|
||||
@OnClick(R.id.loginServerChoiceMatrixOrg)
|
||||
fun selectMatrixOrg() {
|
||||
if (loginServerChoiceMatrixOrg.isChecked) {
|
||||
// Consider this is a submit
|
||||
submit()
|
||||
} else {
|
||||
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg))
|
||||
}
|
||||
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg))
|
||||
}
|
||||
|
||||
@OnClick(R.id.loginServerChoiceModular)
|
||||
fun selectModular() {
|
||||
if (loginServerChoiceModular.isChecked) {
|
||||
// Consider this is a submit
|
||||
submit()
|
||||
} else {
|
||||
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Modular))
|
||||
}
|
||||
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Modular))
|
||||
}
|
||||
|
||||
@OnClick(R.id.loginServerChoiceOther)
|
||||
fun selectOther() {
|
||||
if (loginServerChoiceOther.isChecked) {
|
||||
// Consider this is a submit
|
||||
submit()
|
||||
} else {
|
||||
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Other))
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.loginServerSubmit)
|
||||
fun submit() = withState(loginViewModel) { state ->
|
||||
if (state.serverType == ServerType.MatrixOrg) {
|
||||
// Request login flow here
|
||||
loginViewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url)))
|
||||
} else {
|
||||
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnServerSelectionDone))
|
||||
}
|
||||
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Other))
|
||||
}
|
||||
|
||||
@OnClick(R.id.loginServerIKnowMyIdSubmit)
|
||||
|
@ -70,7 +70,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
|
||||
loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_modular_hint)
|
||||
loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_modular_notice)
|
||||
}
|
||||
ServerType.Other -> {
|
||||
else -> {
|
||||
loginServerUrlFormIcon.isVisible = false
|
||||
loginServerUrlFormTitle.text = getString(R.string.login_server_other_title)
|
||||
loginServerUrlFormText.text = getString(R.string.login_connect_to_a_custom_server)
|
||||
@ -78,7 +78,6 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
|
||||
loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_other_hint)
|
||||
loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_other_notice)
|
||||
}
|
||||
else -> error("This fragment should not be displayed in matrix.org mode")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,6 +49,7 @@ open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLo
|
||||
loginSignupSigninTitle.text = getString(R.string.login_server_other_title)
|
||||
loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl())
|
||||
}
|
||||
ServerType.Unknown -> Unit /* Should not happen */
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,9 +33,9 @@ sealed class LoginViewEvents : VectorViewEvents {
|
||||
// Navigation event
|
||||
|
||||
object OpenServerSelection : LoginViewEvents()
|
||||
object OnServerSelectionDone : LoginViewEvents()
|
||||
data class OnServerSelectionDone(val serverType: ServerType) : LoginViewEvents()
|
||||
data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents()
|
||||
object OnSignModeSelected : LoginViewEvents()
|
||||
data class OnSignModeSelected(val signMode: SignMode) : LoginViewEvents()
|
||||
object OnForgetPasswordClicked : LoginViewEvents()
|
||||
object OnResetPasswordSendThreePidDone : LoginViewEvents()
|
||||
object OnResetPasswordMailConfirmationSuccess : LoginViewEvents()
|
||||
|
@ -39,6 +39,7 @@ import im.vector.matrix.android.api.auth.registration.RegistrationResult
|
||||
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
|
||||
import im.vector.matrix.android.api.auth.registration.Stage
|
||||
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.riotx.R
|
||||
@ -47,6 +48,7 @@ import im.vector.riotx.core.extensions.configureAndStart
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.utils.ensureTrailingSlash
|
||||
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
||||
import im.vector.riotx.features.session.SessionListener
|
||||
@ -87,8 +89,12 @@ class LoginViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// Store the last action, to redo it after user has trusted the untrusted certificate
|
||||
private var lastAction: LoginAction? = null
|
||||
private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null
|
||||
|
||||
private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
|
||||
|
||||
val currentThreePid: String?
|
||||
get() = registrationWizard?.currentThreePid
|
||||
|
||||
@ -111,8 +117,8 @@ class LoginViewModel @AssistedInject constructor(
|
||||
is LoginAction.UpdateServerType -> handleUpdateServerType(action)
|
||||
is LoginAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||
is LoginAction.InitWith -> handleInitWith(action)
|
||||
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action)
|
||||
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action)
|
||||
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action }
|
||||
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action }
|
||||
is LoginAction.LoginWithToken -> handleLoginWithToken(action)
|
||||
is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||
is LoginAction.ResetPassword -> handleResetPassword(action)
|
||||
@ -126,10 +132,23 @@ class LoginViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private fun handleUserAcceptCertificate(action: LoginAction.UserAcceptCertificate) {
|
||||
// It happen when we get the login flow, so alter the homeserver config and retrieve again the login flow
|
||||
currentHomeServerConnectionConfig
|
||||
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
|
||||
?.let { getLoginFlow(it) }
|
||||
// It happen when we get the login flow, or during direct authentication.
|
||||
// So alter the homeserver config and retrieve again the login flow
|
||||
when (val finalLastAction = lastAction) {
|
||||
is LoginAction.UpdateHomeServer ->
|
||||
currentHomeServerConnectionConfig
|
||||
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
|
||||
?.let { getLoginFlow(it) }
|
||||
is LoginAction.LoginOrRegister ->
|
||||
handleDirectLogin(
|
||||
finalLastAction,
|
||||
HomeServerConnectionConfig.Builder()
|
||||
// Will be replaced by the task
|
||||
.withHomeServerUri("https://dummy.org")
|
||||
.withAllowedFingerPrints(listOf(action.fingerprint))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLoginWithToken(action: LoginAction.LoginWithToken) {
|
||||
@ -321,7 +340,7 @@ class LoginViewModel @AssistedInject constructor(
|
||||
LoginAction.ResetHomeServerType -> {
|
||||
setState {
|
||||
copy(
|
||||
serverType = ServerType.MatrixOrg
|
||||
serverType = ServerType.Unknown
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -333,6 +352,7 @@ class LoginViewModel @AssistedInject constructor(
|
||||
asyncHomeServerLoginFlowRequest = Uninitialized,
|
||||
homeServerUrl = null,
|
||||
loginMode = LoginMode.Unknown,
|
||||
serverType = ServerType.Unknown,
|
||||
loginModeSupportedTypes = emptyList()
|
||||
)
|
||||
}
|
||||
@ -379,9 +399,9 @@ class LoginViewModel @AssistedInject constructor(
|
||||
when (action.signMode) {
|
||||
SignMode.SignUp -> startRegistrationFlow()
|
||||
SignMode.SignIn -> startAuthenticationFlow()
|
||||
SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected)
|
||||
SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
|
||||
SignMode.Unknown -> Unit
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateServerType(action: LoginAction.UpdateServerType) {
|
||||
@ -390,6 +410,15 @@ class LoginViewModel @AssistedInject constructor(
|
||||
serverType = action.serverType
|
||||
)
|
||||
}
|
||||
|
||||
when (action.serverType) {
|
||||
ServerType.Unknown -> Unit /* Should not happen */
|
||||
ServerType.MatrixOrg ->
|
||||
// Request login flow here
|
||||
handle(LoginAction.UpdateHomeServer(matrixOrgUrl))
|
||||
ServerType.Modular,
|
||||
ServerType.Other -> _viewEvents.post(LoginViewEvents.OnServerSelectionDone(action.serverType))
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleInitWith(action: LoginAction.InitWith) {
|
||||
@ -427,7 +456,6 @@ class LoginViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
// TODO Handled JobCancellationException
|
||||
setState {
|
||||
copy(
|
||||
asyncResetPassword = Fail(failure)
|
||||
@ -469,7 +497,6 @@ class LoginViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
// TODO Handled JobCancellationException
|
||||
setState {
|
||||
copy(
|
||||
asyncResetMailConfirmed = Fail(failure)
|
||||
@ -485,23 +512,22 @@ class LoginViewModel @AssistedInject constructor(
|
||||
SignMode.Unknown -> error("Developer error, invalid sign mode")
|
||||
SignMode.SignIn -> handleLogin(action)
|
||||
SignMode.SignUp -> handleRegisterWith(action)
|
||||
SignMode.SignInWithMatrixId -> handleDirectLogin(action)
|
||||
SignMode.SignInWithMatrixId -> handleDirectLogin(action, null)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleDirectLogin(action: LoginAction.LoginOrRegister) {
|
||||
private fun handleDirectLogin(action: LoginAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Loading()
|
||||
)
|
||||
}
|
||||
|
||||
// TODO Handle certificate error in this case. Direct login is deactivated now, so we will handle that later
|
||||
authenticationService.getWellKnownData(action.username, null, object : MatrixCallback<WellknownResult> {
|
||||
authenticationService.getWellKnownData(action.username, homeServerConnectionConfig, object : MatrixCallback<WellknownResult> {
|
||||
override fun onSuccess(data: WellknownResult) {
|
||||
when (data) {
|
||||
is WellknownResult.Prompt ->
|
||||
onWellknownSuccess(action, data)
|
||||
onWellknownSuccess(action, data, homeServerConnectionConfig)
|
||||
is WellknownResult.InvalidMatrixId -> {
|
||||
setState {
|
||||
copy(
|
||||
@ -522,23 +548,26 @@ class LoginViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Fail(failure)
|
||||
)
|
||||
}
|
||||
onDirectLoginError(failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun onWellknownSuccess(action: LoginAction.LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) {
|
||||
val homeServerConnectionConfig = HomeServerConnectionConfig(
|
||||
homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl),
|
||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
|
||||
)
|
||||
private fun onWellknownSuccess(action: LoginAction.LoginOrRegister,
|
||||
wellKnownPrompt: WellknownResult.Prompt,
|
||||
homeServerConnectionConfig: HomeServerConnectionConfig?) {
|
||||
val alteredHomeServerConnectionConfig = homeServerConnectionConfig
|
||||
?.copy(
|
||||
homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl),
|
||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
|
||||
)
|
||||
?: HomeServerConnectionConfig(
|
||||
homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl),
|
||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
|
||||
)
|
||||
|
||||
authenticationService.directAuthentication(
|
||||
homeServerConnectionConfig,
|
||||
alteredHomeServerConnectionConfig,
|
||||
action.username,
|
||||
action.password,
|
||||
action.initialDeviceName,
|
||||
@ -548,15 +577,29 @@ class LoginViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Fail(failure)
|
||||
)
|
||||
}
|
||||
onDirectLoginError(failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun onDirectLoginError(failure: Throwable) {
|
||||
if (failure is Failure.UnrecognizedCertificateFailure) {
|
||||
// Display this error in a dialog
|
||||
_viewEvents.post(LoginViewEvents.Failure(failure))
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Uninitialized
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Fail(failure)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLogin(action: LoginAction.LoginOrRegister) {
|
||||
val safeLoginWizard = loginWizard
|
||||
|
||||
@ -584,7 +627,6 @@ class LoginViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
// TODO Handled JobCancellationException
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Fail(failure)
|
||||
@ -609,7 +651,7 @@ class LoginViewModel @AssistedInject constructor(
|
||||
// Ensure Wizard is ready
|
||||
loginWizard
|
||||
|
||||
_viewEvents.post(LoginViewEvents.OnSignModeSelected)
|
||||
_viewEvents.post(LoginViewEvents.OnSignModeSelected(SignMode.SignIn))
|
||||
}
|
||||
|
||||
private fun onFlowResponse(flowResult: FlowResult) {
|
||||
@ -673,7 +715,10 @@ class LoginViewModel @AssistedInject constructor(
|
||||
|
||||
setState {
|
||||
copy(
|
||||
asyncHomeServerLoginFlowRequest = Loading()
|
||||
asyncHomeServerLoginFlowRequest = Loading(),
|
||||
// If user has entered https://matrix.org, ensure that server type is ServerType.MatrixOrg
|
||||
// It is also useful to set the value again in the case of a certificate error on matrix.org
|
||||
serverType = if (homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl) ServerType.MatrixOrg else serverType
|
||||
)
|
||||
}
|
||||
|
||||
@ -682,7 +727,9 @@ class LoginViewModel @AssistedInject constructor(
|
||||
_viewEvents.post(LoginViewEvents.Failure(failure))
|
||||
setState {
|
||||
copy(
|
||||
asyncHomeServerLoginFlowRequest = Uninitialized
|
||||
asyncHomeServerLoginFlowRequest = Uninitialized,
|
||||
// If we were trying to retrieve matrix.org login flow, also reset the serverType
|
||||
serverType = if (serverType == ServerType.MatrixOrg) ServerType.Unknown else serverType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ data class LoginViewState(
|
||||
|
||||
// User choices
|
||||
@PersistState
|
||||
val serverType: ServerType = ServerType.MatrixOrg,
|
||||
val serverType: ServerType = ServerType.Unknown,
|
||||
@PersistState
|
||||
val signMode: SignMode = SignMode.Unknown,
|
||||
@PersistState
|
||||
|
@ -17,6 +17,7 @@
|
||||
package im.vector.riotx.features.login
|
||||
|
||||
enum class ServerType {
|
||||
Unknown,
|
||||
MatrixOrg,
|
||||
Modular,
|
||||
Other
|
||||
|
@ -205,7 +205,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||
@SuppressLint("NewApi")
|
||||
fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification {
|
||||
// build the pending intent go to the home screen if this is clicked.
|
||||
val i = Intent(context, HomeActivity::class.java)
|
||||
val i = HomeActivity.newIntent(context)
|
||||
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
val pi = PendingIntent.getActivity(context, 0, i, 0)
|
||||
|
||||
@ -307,7 +307,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||
val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0)
|
||||
|
||||
val answerCallPendingIntent = TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
|
||||
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||
.addNextIntent(VectorCallActivity.newIntent(
|
||||
context = context,
|
||||
callId = callId,
|
||||
@ -459,7 +459,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||
)
|
||||
|
||||
val contentPendingIntent = TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
|
||||
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||
// TODO other userId
|
||||
.addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, "otherUserId", true, isVideo, null))
|
||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
@ -651,7 +651,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||
stringProvider.getString(R.string.join),
|
||||
joinIntentPendingIntent)
|
||||
|
||||
val contentIntent = Intent(context, HomeActivity::class.java)
|
||||
val contentIntent = HomeActivity.newIntent(context)
|
||||
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
|
||||
contentIntent.data = Uri.parse("foobar://" + inviteNotifiableEvent.eventId)
|
||||
@ -689,7 +689,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||
.setColor(accentColor)
|
||||
.setAutoCancel(true)
|
||||
.apply {
|
||||
val contentIntent = Intent(context, HomeActivity::class.java)
|
||||
val contentIntent = HomeActivity.newIntent(context)
|
||||
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
|
||||
contentIntent.data = Uri.parse("foobar://" + simpleNotifiableEvent.eventId)
|
||||
@ -718,7 +718,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||
|
||||
// Recreate the back stack
|
||||
return TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
|
||||
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||
.addNextIntent(roomIntentTap)
|
||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
@ -93,8 +93,9 @@ class SoftLogoutFragment @Inject constructor(
|
||||
softLogoutViewModel.handle(SoftLogoutAction.SignInAgain(password))
|
||||
}
|
||||
|
||||
override fun signinFallbackSubmit() {
|
||||
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSignModeSelected))
|
||||
override fun signinFallbackSubmit() = withState(loginViewModel) { state ->
|
||||
// The loginViewModel has been prepared for a SSO/login fallback recovery (above)
|
||||
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSignModeSelected(state.signMode)))
|
||||
}
|
||||
|
||||
override fun clearData() {
|
||||
|
@ -2,6 +2,7 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:drawable="@drawable/bg_login_server_checked" android:state_checked="true" />
|
||||
<item android:drawable="@drawable/bg_login_server_checked" android:state_pressed="true" />
|
||||
|
||||
<item android:drawable="@drawable/bg_login_server" />
|
||||
|
||||
|
@ -106,6 +106,16 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginPasswordNotice"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_signin_matrix_id_password_notice"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -43,6 +43,7 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginServerTitle" />
|
||||
|
||||
<!-- Use a CheckableConstraintLayout to keep the pressed state when retrieving login flow -->
|
||||
<im.vector.riotx.core.platform.CheckableConstraintLayout
|
||||
android:id="@+id/loginServerChoiceMatrixOrg"
|
||||
android:layout_width="match_parent"
|
||||
@ -84,7 +85,7 @@
|
||||
|
||||
</im.vector.riotx.core.platform.CheckableConstraintLayout>
|
||||
|
||||
<im.vector.riotx.core.platform.CheckableConstraintLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/loginServerChoiceModular"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -135,9 +136,9 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginServerChoiceModularText" />
|
||||
|
||||
</im.vector.riotx.core.platform.CheckableConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<im.vector.riotx.core.platform.CheckableConstraintLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/loginServerChoiceOther"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -178,45 +179,20 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceOtherTitle" />
|
||||
|
||||
</im.vector.riotx.core.platform.CheckableConstraintLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/loginServerSubmit"
|
||||
style="@style/Style.Vector.Login.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/login_continue"
|
||||
android:transitionName="loginSubmitTransition"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginServerIKnowMyIdNotice"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceOther" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginServerIKnowMyIdNotice"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_connect_using_matrix_id_notice"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginServerSubmit" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/loginServerIKnowMyIdSubmit"
|
||||
style="@style/Style.Vector.Login.Button.Text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/login_connect_using_matrix_id_submit"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginServerIKnowMyIdNotice" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceOther" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
@ -1995,10 +1995,11 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
||||
</plurals>
|
||||
|
||||
<string name="login_connect_using_matrix_id_notice">Alternatively, if you already have an account and you know your Matrix identifier and your password, you can use this method:</string>
|
||||
<string name="login_connect_using_matrix_id_submit">Sign in with my Matrix identifier</string>
|
||||
<string name="login_signin_matrix_id_title">Sign in</string>
|
||||
<string name="login_signin_matrix_id_notice">Enter your identifier and your password</string>
|
||||
<string name="login_signin_matrix_id_hint">User identifier</string>
|
||||
<string name="login_connect_using_matrix_id_submit">Sign in with Matrix ID</string>
|
||||
<string name="login_signin_matrix_id_title">Sign in with Matrix ID</string>
|
||||
<string name="login_signin_matrix_id_notice">If you set up an account on a homeserver, use your Matrix ID (e.g. @user:domain.com) and password below.</string>
|
||||
<string name="login_signin_matrix_id_hint">Matrix ID</string>
|
||||
<string name="login_signin_matrix_id_password_notice">If you don’t know your password, go back to reset it.</string>
|
||||
<string name="login_signin_matrix_id_error_invalid_matrix_id">This is not a valid user identifier. Expected format: \'@user:homeserver.org\'</string>
|
||||
<string name="autodiscover_well_known_error">Unable to find a valid homeserver. Please check your identifier</string>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user