mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 12:00:03 +03:00
Merge tag 'v1.1.15' into sc
Change-Id: I6bc7a7c052ccaae6adec247889b37baac2672ba4 Conflicts: vector/src/main/assets/open_source_licenses.html vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt vector/src/main/res/drawable/ic_insert_emoji.xml vector/src/main/res/layout/composer_layout_constraint_set_compact.xml vector/src/main/res/layout/fragment_room_detail.xml vector/src/main/res/layout/item_timeline_event_base.xml vector/src/main/res/xml/vector_settings_labs.xml
This commit is contained in:
commit
25ba52115b
115 changed files with 3956 additions and 525 deletions
12
CHANGES.md
12
CHANGES.md
|
@ -1,3 +1,15 @@
|
|||
Changes in Element v1.1.15 (2021-07-30)
|
||||
=======================================
|
||||
|
||||
Features ✨
|
||||
----------
|
||||
- Voice Message implementation (Currently under Labs Settings and disabled by default). ([#3598](https://github.com/vector-im/element-android/issues/3598))
|
||||
|
||||
SDK API changes ⚠️
|
||||
------------------
|
||||
- updatePushRuleActions signature has been updated to more explicitly enabled/disable the rule and update the actions. It's behaviour has also been changed to match the web with the enable/disable requests being sent on every invocation and actions sent when needed(not null). ([#3681](https://github.com/vector-im/element-android/issues/3681))
|
||||
|
||||
|
||||
Changes in Element 1.1.14 (2021-07-23)
|
||||
======================================
|
||||
|
||||
|
|
|
@ -48,6 +48,9 @@ allprojects {
|
|||
// Chat effects
|
||||
includeGroupByRegex 'com\\.github\\.jetradarmobile'
|
||||
includeGroupByRegex 'nl\\.dionsegijn'
|
||||
|
||||
// Voice RecordView
|
||||
includeGroupByRegex 'com\\.github\\.Armen101'
|
||||
}
|
||||
}
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
|
||||
|
|
2
fastlane/metadata/android/cs-CZ/changelogs/40101120.txt
Normal file
2
fastlane/metadata/android/cs-CZ/changelogs/40101120.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Hlavní změny v této verzi: aktualizace motivu a stylu a oprava pádu aplikace po videohovoru
|
||||
Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.1.12
|
2
fastlane/metadata/android/en-US/changelogs/40101150.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40101150.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: voice message implementation under labs settings.
|
||||
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.1.15
|
2
fastlane/metadata/android/et/changelogs/40101120.txt
Normal file
2
fastlane/metadata/android/et/changelogs/40101120.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Põhilised muutused selles versioonis: teemade ja välimuse uuendused ning videokõne-järgse kokkujooksmise parandused
|
||||
Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.1.12
|
2
fastlane/metadata/android/hu-HU/changelogs/40101120.txt
Normal file
2
fastlane/metadata/android/hu-HU/changelogs/40101120.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Főbb változtatások ebben a verzióban: kinézet és stílus frissítések és a videóhívás utáni összeomlás javítása
|
||||
Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.1.12
|
2
fastlane/metadata/android/it-IT/changelogs/40101120.txt
Normal file
2
fastlane/metadata/android/it-IT/changelogs/40101120.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Modifiche principali in questa versione: aggiornati tema e stile, corretto un crash dopo videochiamata
|
||||
Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.1.12
|
2
fastlane/metadata/android/pt-BR/changelogs/40101120.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40101120.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Principais mudanças nesta versão: atualização de tema e estilo e consertar um crash depois de chamada de vídeo
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.1.12
|
2
fastlane/metadata/android/zh-TW/changelogs/40101120.txt
Normal file
2
fastlane/metadata/android/zh-TW/changelogs/40101120.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
此版本中的主要變動:佈景主題與樣式更新,以及修復視訊通話後當機的問題
|
||||
完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.12
|
|
@ -60,4 +60,6 @@ dependencies {
|
|||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
|
||||
// dialpad dimen
|
||||
implementation 'im.dlg:android-dialer:1.2.5'
|
||||
// AudioRecordView attr
|
||||
implementation 'com.github.Armen101:AudioRecordView:1.0.5'
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<solid android:color="#F00" />
|
||||
|
||||
<corners android:radius="8dp" />
|
||||
|
||||
</shape>
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<integer name="rtl_x_multiplier">-1</integer>
|
||||
<integer name="rtl_mirror_flip">180</integer>
|
||||
|
||||
</resources>
|
|
@ -128,4 +128,8 @@
|
|||
<color name="vctr_chat_effect_snow_background_light">@color/black_alpha</color>
|
||||
<color name="vctr_chat_effect_snow_background_dark">@android:color/transparent</color>
|
||||
|
||||
<attr name="vctr_voice_message_toast_background" format="color" />
|
||||
<color name="vctr_voice_message_toast_background_light">@color/palette_black_900</color>
|
||||
<color name="vctr_voice_message_toast_background_dark">@color/palette_gray_400</color>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
<integer name="default_animation_offset">200</integer>
|
||||
|
||||
<integer name="rtl_x_multiplier">1</integer>
|
||||
<integer name="rtl_mirror_flip">0</integer>
|
||||
|
||||
<integer name="splash_animation_velocity">750</integer>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
<!-- For light themes -->
|
||||
<color name="palette_gray_25">#F4F6FA</color>
|
||||
<color name="palette_gray_50">#E6E8F0</color>
|
||||
<color name="palette_gray_50">#E3E8F0</color>
|
||||
<color name="palette_gray_100">#C1C6CD</color>
|
||||
<color name="palette_gray_150">#8D97A5</color>
|
||||
<color name="palette_gray_200">#737D8C</color>
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="VoicePlaybackWaveform">
|
||||
<item name="chunkColor">?vctr_content_secondary</item>
|
||||
<item name="chunkAlignTo">center</item>
|
||||
<item name="chunkMinHeight">1dp</item>
|
||||
<item name="chunkRoundedCorners">true</item>
|
||||
<item name="chunkSoftTransition">true</item>
|
||||
<item name="chunkSpace">2dp</item>
|
||||
<item name="chunkWidth">2dp</item>
|
||||
<item name="direction">rightToLeft</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Vector.TextView.Caption.Toast">
|
||||
<item name="android:paddingTop">8dp</item>
|
||||
<item name="android:paddingBottom">8dp</item>
|
||||
<item name="android:paddingStart">12dp</item>
|
||||
<item name="android:paddingEnd">12dp</item>
|
||||
<item name="android:background">@drawable/bg_round_corner_8dp</item>
|
||||
<item name="android:backgroundTint">?vctr_voice_message_toast_background</item>
|
||||
<item name="android:textColor">@color/palette_white</item>
|
||||
<item name="android:gravity">center</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -153,6 +153,8 @@
|
|||
|
||||
<item name="vctr_jump_to_unread_style">@style/Widget.Vector.JumpToUnread.Dark</item>
|
||||
|
||||
<!-- Voice Message -->
|
||||
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_dark</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />
|
||||
|
|
|
@ -155,6 +155,8 @@
|
|||
|
||||
<item name="vctr_jump_to_unread_style">@style/Widget.Vector.JumpToUnread.Light</item>
|
||||
|
||||
<!-- Voice Message -->
|
||||
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_light</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />
|
||||
|
|
|
@ -23,6 +23,7 @@ import io.reactivex.Single
|
|||
import kotlinx.coroutines.rx2.rxCompletable
|
||||
import kotlinx.coroutines.rx2.rxSingle
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
|
@ -146,6 +147,10 @@ class RxRoom(private val room: Room) {
|
|||
fun deleteAvatar(): Completable = rxCompletable {
|
||||
room.deleteAvatar()
|
||||
}
|
||||
|
||||
fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set<String>): Completable = rxCompletable {
|
||||
room.sendMedia(attachment, compressBeforeSending, roomIds)
|
||||
}
|
||||
}
|
||||
|
||||
fun Room.rx(): RxRoom {
|
||||
|
|
|
@ -31,7 +31,13 @@ interface PushRuleService {
|
|||
|
||||
suspend fun addPushRule(kind: RuleKind, pushRule: PushRule)
|
||||
|
||||
suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule)
|
||||
/**
|
||||
* Enables/Disables a push rule and updates the actions if necessary
|
||||
* @param enable Enables/Disables the rule
|
||||
* @param actions Actions to update if not null
|
||||
*/
|
||||
|
||||
suspend fun updatePushRuleActions(kind: RuleKind, ruleId: String, enable: Boolean, actions: List<Action>?)
|
||||
|
||||
suspend fun removePushRule(kind: RuleKind, pushRule: PushRule)
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.pushrules.rest
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.pushrules.Action
|
||||
import org.matrix.android.sdk.api.pushrules.getActions
|
||||
import org.matrix.android.sdk.api.pushrules.toJson
|
||||
|
@ -100,6 +101,13 @@ data class PushRule(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the highlight status. As spec mentions assume false if no tweak present.
|
||||
*/
|
||||
fun getHighlight(): Boolean {
|
||||
return getActions().filterIsInstance<Action.Highlight>().firstOrNull()?.highlight.orFalse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the notification status.
|
||||
*
|
||||
|
|
|
@ -35,7 +35,8 @@ data class ContentAttachmentData(
|
|||
val name: String? = null,
|
||||
val queryUri: Uri,
|
||||
val mimeType: String?,
|
||||
val type: Type
|
||||
val type: Type,
|
||||
val waveform: List<Int>? = null
|
||||
) : Parcelable {
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
|
|
|
@ -24,15 +24,15 @@ data class AudioInfo(
|
|||
/**
|
||||
* The mimetype of the audio e.g. "audio/aac".
|
||||
*/
|
||||
@Json(name = "mimetype") val mimeType: String?,
|
||||
@Json(name = "mimetype") val mimeType: String? = null,
|
||||
|
||||
/**
|
||||
* The size of the audio clip in bytes.
|
||||
*/
|
||||
@Json(name = "size") val size: Long = 0,
|
||||
@Json(name = "size") val size: Long? = null,
|
||||
|
||||
/**
|
||||
* The duration of the audio in milliseconds.
|
||||
*/
|
||||
@Json(name = "duration") val duration: Int = 0
|
||||
@Json(name = "duration") val duration: Int? = null
|
||||
)
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* See https://github.com/matrix-org/matrix-doc/blob/travis/msc/audio-waveform/proposals/3246-audio-waveform.md
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AudioWaveformInfo(
|
||||
@Json(name = "duration")
|
||||
val duration: Int? = null,
|
||||
|
||||
/**
|
||||
* The array should have no less than 30 elements and no more than 120.
|
||||
* List of integers between zero and 1024, inclusive.
|
||||
*/
|
||||
@Json(name = "waveform")
|
||||
val waveform: List<Int>? = null
|
||||
)
|
|
@ -20,6 +20,7 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
@ -50,7 +51,17 @@ data class MessageAudioContent(
|
|||
/**
|
||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||
*/
|
||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null,
|
||||
|
||||
/**
|
||||
* Encapsulates waveform and duration of the audio.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc1767.audio") val audioWaveformInfo: AudioWaveformInfo? = null,
|
||||
|
||||
/**
|
||||
* Indicates that is a voice message.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3245.voice") val voiceMessageIndicator: JsonDict? = null
|
||||
) : MessageWithAttachmentContent {
|
||||
|
||||
override val mimeType: String?
|
||||
|
|
|
@ -31,6 +31,8 @@ object MimeTypes {
|
|||
const val Jpeg = "image/jpeg"
|
||||
const val Gif = "image/gif"
|
||||
|
||||
const val Ogg = "audio/ogg"
|
||||
|
||||
fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this
|
||||
|
||||
fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse()
|
||||
|
|
|
@ -49,6 +49,8 @@ internal class DefaultSessionCreator @Inject constructor(
|
|||
// remove trailing "/"
|
||||
?.trim { it == '/' }
|
||||
?.takeIf { it.isNotBlank() }
|
||||
// It can be the same value, so in this case, do not check again the validity
|
||||
?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() }
|
||||
?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") }
|
||||
?.let { Uri.parse(it) }
|
||||
?.takeIf {
|
||||
|
|
|
@ -18,8 +18,6 @@ package org.matrix.android.sdk.internal.crypto.model
|
|||
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
||||
|
||||
data class CryptoDeviceInfo(
|
||||
val deviceId: String,
|
||||
|
@ -77,7 +75,3 @@ data class CryptoDeviceInfo(
|
|||
internal fun CryptoDeviceInfo.toRest(): DeviceKeys {
|
||||
return CryptoInfoMapper.map(this)
|
||||
}
|
||||
|
||||
internal fun CryptoDeviceInfo.toEntity(): DeviceInfoEntity {
|
||||
return CryptoMapper.mapToEntity(this)
|
||||
}
|
||||
|
|
|
@ -51,7 +51,6 @@ import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
|||
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.internal.crypto.model.toEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
||||
import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo
|
||||
|
@ -280,24 +279,34 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
override fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
if (devices == null) {
|
||||
Timber.d("Remove user $userId")
|
||||
// Remove the user
|
||||
UserEntity.delete(realm, userId)
|
||||
} else {
|
||||
UserEntity.getOrCreate(realm, userId)
|
||||
.let { u ->
|
||||
// Add the devices
|
||||
val currentKnownDevices = u.devices.toList()
|
||||
val new = devices.map { entry -> entry.value.toEntity() }
|
||||
new.forEach { entity ->
|
||||
// Maintain first time seen
|
||||
val existing = currentKnownDevices.firstOrNull { it.deviceId == entity.deviceId && it.identityKey == entity.identityKey }
|
||||
entity.firstTimeSeenLocalTs = existing?.firstTimeSeenLocalTs ?: System.currentTimeMillis()
|
||||
realm.insertOrUpdate(entity)
|
||||
}
|
||||
// Ensure all other devices are deleted
|
||||
u.devices.clearWith { it.deleteOnCascade() }
|
||||
u.devices.addAll(new)
|
||||
}
|
||||
val userEntity = UserEntity.getOrCreate(realm, userId)
|
||||
// First delete the removed devices
|
||||
val deviceIds = devices.keys
|
||||
userEntity.devices.iterator().forEach { deviceInfoEntity ->
|
||||
if (deviceInfoEntity.deviceId !in deviceIds) {
|
||||
Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId")
|
||||
deviceInfoEntity.deleteOnCascade()
|
||||
}
|
||||
}
|
||||
// Then update existing devices or add new one
|
||||
devices.values.forEach { cryptoDeviceInfo ->
|
||||
val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId }
|
||||
if (existingDeviceInfoEntity == null) {
|
||||
// Add the device
|
||||
Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId")
|
||||
val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo)
|
||||
newEntity.firstTimeSeenLocalTs = System.currentTimeMillis()
|
||||
userEntity.devices.add(newEntity)
|
||||
} else {
|
||||
// Update the device
|
||||
Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId")
|
||||
CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ internal object RealmCryptoStoreMigration : RealmMigration {
|
|||
// 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 = 12L
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 13L
|
||||
|
||||
private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema {
|
||||
if (!hasField(fieldName)) {
|
||||
|
@ -93,6 +93,7 @@ internal object RealmCryptoStoreMigration : RealmMigration {
|
|||
if (oldVersion <= 9) migrateTo10(realm)
|
||||
if (oldVersion <= 10) migrateTo11(realm)
|
||||
if (oldVersion <= 11) migrateTo12(realm)
|
||||
if (oldVersion <= 12) migrateTo13(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1Legacy(realm: DynamicRealm) {
|
||||
|
@ -503,4 +504,60 @@ internal object RealmCryptoStoreMigration : RealmMigration {
|
|||
realm.schema.get("CryptoRoomEntity")
|
||||
?.addRealmObjectField(CryptoRoomEntityFields.OUTBOUND_SESSION_INFO.`$`, outboundEntitySchema)
|
||||
}
|
||||
|
||||
// Version 13L delete unreferenced TrustLevelEntity
|
||||
private fun migrateTo13(realm: DynamicRealm) {
|
||||
Timber.d("Step 12 -> 13")
|
||||
|
||||
// Use a trick to do that... Ref: https://stackoverflow.com/questions/55221366
|
||||
val trustLevelEntitySchema = realm.schema.get("TrustLevelEntity")
|
||||
|
||||
/*
|
||||
Creating a new temp field called isLinked which is set to true for those which are
|
||||
references by other objects. Rest of them are set to false. Then removing all
|
||||
those which are false and hence duplicate and unnecessary. Then removing the temp field
|
||||
isLinked
|
||||
*/
|
||||
var mainCounter = 0
|
||||
var deviceInfoCounter = 0
|
||||
var keyInfoCounter = 0
|
||||
val deleteCounter: Int
|
||||
|
||||
trustLevelEntitySchema
|
||||
?.addField("isLinked", Boolean::class.java)
|
||||
?.transform { obj ->
|
||||
// Setting to false for all by default
|
||||
obj.set("isLinked", false)
|
||||
mainCounter++
|
||||
}
|
||||
|
||||
realm.schema.get("DeviceInfoEntity")?.transform { obj ->
|
||||
// Setting to true for those which are referenced in DeviceInfoEntity
|
||||
deviceInfoCounter++
|
||||
obj.getObject("trustLevelEntity")?.set("isLinked", true)
|
||||
}
|
||||
|
||||
realm.schema.get("KeyInfoEntity")?.transform { obj ->
|
||||
// Setting to true for those which are referenced in KeyInfoEntity
|
||||
keyInfoCounter++
|
||||
obj.getObject("trustLevelEntity")?.set("isLinked", true)
|
||||
}
|
||||
|
||||
// Removing all those which are set as false
|
||||
realm.where("TrustLevelEntity")
|
||||
.equalTo("isLinked", false)
|
||||
.findAll()
|
||||
.also { deleteCounter = it.size }
|
||||
.deleteAllFromRealm()
|
||||
|
||||
trustLevelEntitySchema?.removeField("isLinked")
|
||||
|
||||
Timber.w("TrustLevelEntity cleanup: $mainCounter entities")
|
||||
Timber.w("TrustLevelEntity cleanup: $deviceInfoCounter entities referenced in DeviceInfoEntities")
|
||||
Timber.w("TrustLevelEntity cleanup: $keyInfoCounter entities referenced in KeyInfoEntity")
|
||||
Timber.w("TrustLevelEntity cleanup: $deleteCounter entities deleted!")
|
||||
if (mainCounter != deviceInfoCounter + keyInfoCounter + deleteCounter) {
|
||||
Timber.e("TrustLevelEntity cleanup: Something is not correct...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,23 +44,32 @@ object CryptoMapper {
|
|||
))
|
||||
|
||||
internal fun mapToEntity(deviceInfo: CryptoDeviceInfo): DeviceInfoEntity {
|
||||
return DeviceInfoEntity(
|
||||
primaryKey = DeviceInfoEntity.createPrimaryKey(deviceInfo.userId, deviceInfo.deviceId),
|
||||
userId = deviceInfo.userId,
|
||||
deviceId = deviceInfo.deviceId,
|
||||
algorithmListJson = listMigrationAdapter.toJson(deviceInfo.algorithms),
|
||||
keysMapJson = mapMigrationAdapter.toJson(deviceInfo.keys),
|
||||
signatureMapJson = mapMigrationAdapter.toJson(deviceInfo.signatures),
|
||||
isBlocked = deviceInfo.isBlocked,
|
||||
trustLevelEntity = deviceInfo.trustLevel?.let {
|
||||
TrustLevelEntity(
|
||||
crossSignedVerified = it.crossSigningVerified,
|
||||
locallyVerified = it.locallyVerified
|
||||
)
|
||||
},
|
||||
// We store the device name if present now
|
||||
unsignedMapJson = deviceInfo.unsigned?.deviceDisplayName
|
||||
)
|
||||
return DeviceInfoEntity(primaryKey = DeviceInfoEntity.createPrimaryKey(deviceInfo.userId, deviceInfo.deviceId))
|
||||
.also { updateDeviceInfoEntity(it, deviceInfo) }
|
||||
}
|
||||
|
||||
internal fun updateDeviceInfoEntity(entity: DeviceInfoEntity, deviceInfo: CryptoDeviceInfo) {
|
||||
entity.userId = deviceInfo.userId
|
||||
entity.deviceId = deviceInfo.deviceId
|
||||
entity.algorithmListJson = listMigrationAdapter.toJson(deviceInfo.algorithms)
|
||||
entity.keysMapJson = mapMigrationAdapter.toJson(deviceInfo.keys)
|
||||
entity.signatureMapJson = mapMigrationAdapter.toJson(deviceInfo.signatures)
|
||||
entity.isBlocked = deviceInfo.isBlocked
|
||||
val deviceInfoTrustLevel = deviceInfo.trustLevel
|
||||
if (deviceInfoTrustLevel == null) {
|
||||
entity.trustLevelEntity?.deleteFromRealm()
|
||||
entity.trustLevelEntity = null
|
||||
} else {
|
||||
if (entity.trustLevelEntity == null) {
|
||||
// Create a new TrustLevelEntity object
|
||||
entity.trustLevelEntity = TrustLevelEntity()
|
||||
}
|
||||
// Update the existing TrustLevelEntity object
|
||||
entity.trustLevelEntity?.crossSignedVerified = deviceInfoTrustLevel.crossSigningVerified
|
||||
entity.trustLevelEntity?.locallyVerified = deviceInfoTrustLevel.locallyVerified
|
||||
}
|
||||
// We store the device name if present now
|
||||
entity.unsignedMapJson = deviceInfo.unsigned?.deviceDisplayName
|
||||
}
|
||||
|
||||
internal fun mapToModel(deviceInfoEntity: DeviceInfoEntity): CryptoDeviceInfo {
|
||||
|
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.completeWith
|
|||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
|
@ -124,13 +125,21 @@ internal class DefaultFileService @Inject constructor(
|
|||
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
|
||||
.build()
|
||||
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException()
|
||||
val response = try {
|
||||
okHttpClient.newCall(request).execute()
|
||||
} catch (failure: Throwable) {
|
||||
throw if (failure is IOException) {
|
||||
Failure.NetworkConnection(failure)
|
||||
} else {
|
||||
failure
|
||||
}
|
||||
}
|
||||
|
||||
val source = response.body?.source() ?: throw IOException()
|
||||
if (!response.isSuccessful) {
|
||||
throw Failure.NetworkConnection(IOException())
|
||||
}
|
||||
|
||||
val source = response.body?.source() ?: throw Failure.NetworkConnection(IOException())
|
||||
|
||||
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
|
||||
|
||||
|
|
|
@ -113,8 +113,8 @@ internal class DefaultPushRuleService @Inject constructor(
|
|||
addPushRuleTask.execute(AddPushRuleTask.Params(kind, pushRule))
|
||||
}
|
||||
|
||||
override suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule) {
|
||||
updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule))
|
||||
override suspend fun updatePushRuleActions(kind: RuleKind, ruleId: String, enable: Boolean, actions: List<Action>?) {
|
||||
updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, ruleId, enable, actions))
|
||||
}
|
||||
|
||||
override suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) {
|
||||
|
|
|
@ -15,8 +15,9 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.session.pushers
|
||||
|
||||
import org.matrix.android.sdk.api.pushrules.Action
|
||||
import org.matrix.android.sdk.api.pushrules.RuleKind
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||
import org.matrix.android.sdk.api.pushrules.toJson
|
||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
|
@ -25,8 +26,9 @@ import javax.inject.Inject
|
|||
internal interface UpdatePushRuleActionsTask : Task<UpdatePushRuleActionsTask.Params, Unit> {
|
||||
data class Params(
|
||||
val kind: RuleKind,
|
||||
val oldPushRule: PushRule,
|
||||
val newPushRule: PushRule
|
||||
val ruleId: String,
|
||||
val enable: Boolean,
|
||||
val actions: List<Action>?
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -36,20 +38,14 @@ internal class DefaultUpdatePushRuleActionsTask @Inject constructor(
|
|||
) : UpdatePushRuleActionsTask {
|
||||
|
||||
override suspend fun execute(params: UpdatePushRuleActionsTask.Params) {
|
||||
if (params.oldPushRule.enabled != params.newPushRule.enabled) {
|
||||
// First change enabled state
|
||||
executeRequest(globalErrorReceiver) {
|
||||
pushRulesApi.updateEnableRuleStatus(params.kind.value, params.newPushRule.ruleId, params.newPushRule.enabled)
|
||||
pushRulesApi.updateEnableRuleStatus(params.kind.value, params.ruleId, enable = params.enable)
|
||||
}
|
||||
}
|
||||
|
||||
if (params.newPushRule.enabled) {
|
||||
// Also ensure the actions are up to date
|
||||
val body = mapOf("actions" to params.newPushRule.actions)
|
||||
|
||||
executeRequest(globalErrorReceiver) {
|
||||
pushRulesApi.updateRuleActions(params.kind.value, params.newPushRule.ruleId, body)
|
||||
if (params.actions != null) {
|
||||
val body = mapOf("actions" to params.actions.toJson())
|
||||
executeRequest(globalErrorReceiver) {
|
||||
pushRulesApi.updateRuleActions(params.kind.value, params.ruleId, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -184,7 +184,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
mimeType = messageContent.mimeType,
|
||||
name = messageContent.body,
|
||||
queryUri = Uri.parse(messageContent.url),
|
||||
type = ContentAttachmentData.Type.AUDIO
|
||||
type = ContentAttachmentData.Type.AUDIO,
|
||||
waveform = messageContent.audioWaveformInfo?.waveform
|
||||
)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
|
||||
internalSendMedia(listOf(localEcho.root), attachmentData, true)
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.events.model.RelationType
|
|||
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.AudioInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.FileInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.ImageInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
|
@ -74,6 +75,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
private val markdownParser: MarkdownParser,
|
||||
private val textPillsUtils: TextPillsUtils,
|
||||
private val thumbnailExtractor: ThumbnailExtractor,
|
||||
private val waveformSanitizer: WaveFormSanitizer,
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val permalinkFactory: PermalinkFactory
|
||||
) {
|
||||
|
@ -289,14 +291,21 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||
val isVoiceMessage = attachment.waveform != null
|
||||
val content = MessageAudioContent(
|
||||
msgType = MessageType.MSGTYPE_AUDIO,
|
||||
body = attachment.name ?: "audio",
|
||||
audioInfo = AudioInfo(
|
||||
duration = attachment.duration?.toInt(),
|
||||
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() },
|
||||
size = attachment.size
|
||||
),
|
||||
url = attachment.queryUri.toString()
|
||||
url = attachment.queryUri.toString(),
|
||||
audioWaveformInfo = if (!isVoiceMessage) null else AudioWaveformInfo(
|
||||
duration = attachment.duration?.toInt(),
|
||||
waveform = waveformSanitizer.sanitize(attachment.waveform)
|
||||
),
|
||||
voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap()
|
||||
)
|
||||
return createMessageEvent(roomId, content)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
|
||||
internal class WaveFormSanitizer @Inject constructor() {
|
||||
private companion object {
|
||||
const val MIN_NUMBER_OF_VALUES = 30
|
||||
const val MAX_NUMBER_OF_VALUES = 120
|
||||
|
||||
const val MAX_VALUE = 1024
|
||||
}
|
||||
|
||||
/**
|
||||
* The array should have no less than 30 elements and no more than 120.
|
||||
* List of integers between zero and 1024, inclusive.
|
||||
*/
|
||||
fun sanitize(waveForm: List<Int>?): List<Int>? {
|
||||
if (waveForm.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Limit the number of items
|
||||
val sizeInRangeList = mutableListOf<Int>()
|
||||
when {
|
||||
waveForm.size < MIN_NUMBER_OF_VALUES -> {
|
||||
// Repeat the same value to have at least 30 items
|
||||
val repeatTimes = ceil(MIN_NUMBER_OF_VALUES / waveForm.size.toDouble()).toInt()
|
||||
waveForm.map { value ->
|
||||
repeat(repeatTimes) {
|
||||
sizeInRangeList.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
waveForm.size > MAX_NUMBER_OF_VALUES -> {
|
||||
val keepOneOf = ceil(waveForm.size.toDouble() / MAX_NUMBER_OF_VALUES).toInt()
|
||||
waveForm.mapIndexed { idx, value ->
|
||||
if (idx % keepOneOf == 0) {
|
||||
sizeInRangeList.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
sizeInRangeList.addAll(waveForm)
|
||||
}
|
||||
}
|
||||
|
||||
// OK, ensure all items are positive
|
||||
val positiveList = sizeInRangeList.map {
|
||||
abs(it)
|
||||
}
|
||||
|
||||
// Ensure max is not above MAX_VALUE
|
||||
val max = positiveList.maxOrNull() ?: MAX_VALUE
|
||||
|
||||
val finalList = if (max > MAX_VALUE) {
|
||||
// Reduce the values
|
||||
positiveList.map {
|
||||
it * MAX_VALUE / max
|
||||
}
|
||||
} else {
|
||||
positiveList
|
||||
}
|
||||
|
||||
Timber.d("Sanitize from ${waveForm.size} items to ${finalList.size} items. Max value was $max")
|
||||
return finalList
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.amshove.kluent.shouldBeInRange
|
||||
import org.junit.Test
|
||||
|
||||
class WaveFormSanitizerTest {
|
||||
|
||||
private val waveFormSanitizer = WaveFormSanitizer()
|
||||
|
||||
@Test
|
||||
fun sanitizeNull() {
|
||||
waveFormSanitizer.sanitize(null) shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitizeEmpty() {
|
||||
waveFormSanitizer.sanitize(emptyList()) shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitizeSingleton() {
|
||||
val result = waveFormSanitizer.sanitize(listOf(1))!!
|
||||
result.size shouldBe 30
|
||||
checkResult(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitize29() {
|
||||
val list = generateSequence { 1 }.take(29).toList()
|
||||
val result = waveFormSanitizer.sanitize(list)!!
|
||||
checkResult(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitize30() {
|
||||
val list = generateSequence { 1 }.take(30).toList()
|
||||
val result = waveFormSanitizer.sanitize(list)!!
|
||||
result.size shouldBe 30
|
||||
checkResult(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitize31() {
|
||||
val list = generateSequence { 1 }.take(31).toList()
|
||||
val result = waveFormSanitizer.sanitize(list)!!
|
||||
checkResult(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitize119() {
|
||||
val list = generateSequence { 1 }.take(119).toList()
|
||||
val result = waveFormSanitizer.sanitize(list)!!
|
||||
checkResult(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitize120() {
|
||||
val list = generateSequence { 1 }.take(120).toList()
|
||||
val result = waveFormSanitizer.sanitize(list)!!
|
||||
result.size shouldBe 120
|
||||
checkResult(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitize121() {
|
||||
val list = generateSequence { 1 }.take(121).toList()
|
||||
val result = waveFormSanitizer.sanitize(list)!!
|
||||
checkResult(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitize1024() {
|
||||
val list = generateSequence { 1 }.take(1024).toList()
|
||||
val result = waveFormSanitizer.sanitize(list)!!
|
||||
checkResult(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitizeNegative() {
|
||||
val list = generateSequence { -1 }.take(30).toList()
|
||||
val result = waveFormSanitizer.sanitize(list)!!
|
||||
checkResult(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitizeMaxValue() {
|
||||
val list = generateSequence { 1025 }.take(30).toList()
|
||||
val result = waveFormSanitizer.sanitize(list)!!
|
||||
checkResult(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sanitizeNegativeMaxValue() {
|
||||
val list = generateSequence { -1025 }.take(30).toList()
|
||||
val result = waveFormSanitizer.sanitize(list)!!
|
||||
checkResult(result)
|
||||
}
|
||||
|
||||
private fun checkResult(result: List<Int>) {
|
||||
result.forEach {
|
||||
it shouldBeInRange 0..1024
|
||||
}
|
||||
|
||||
result.size shouldBeInRange 30..120
|
||||
}
|
||||
}
|
|
@ -23,5 +23,6 @@ data class MultiPickerAudioType(
|
|||
override val size: Long,
|
||||
override val mimeType: String?,
|
||||
override val contentUri: Uri,
|
||||
val duration: Long
|
||||
val duration: Long,
|
||||
var waveform: List<Int>? = null
|
||||
) : MultiPickerBaseType
|
||||
|
|
|
@ -111,7 +111,7 @@ internal fun Uri.toMultiPickerVideoType(context: Context): MultiPickerVideoType?
|
|||
}
|
||||
}
|
||||
|
||||
internal fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? {
|
||||
fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? {
|
||||
val projection = arrayOf(
|
||||
MediaStore.Audio.Media.DISPLAY_NAME,
|
||||
MediaStore.Audio.Media.SIZE
|
||||
|
@ -141,7 +141,7 @@ internal fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType?
|
|||
MultiPickerAudioType(
|
||||
name,
|
||||
size,
|
||||
context.contentResolver.getType(this),
|
||||
sanitize(context.contentResolver.getType(this)),
|
||||
this,
|
||||
duration
|
||||
)
|
||||
|
@ -150,3 +150,11 @@ internal fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType?
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sanitize(type: String?): String? {
|
||||
if (type == "application/ogg") {
|
||||
// Not supported on old system
|
||||
return "audio/ogg"
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
|
|
@ -162,7 +162,7 @@ Formatter\.formatShortFileSize===1
|
|||
# android\.text\.TextUtils
|
||||
|
||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
||||
enum class===102
|
||||
enum class===103
|
||||
|
||||
### Do not import temporary legacy classes
|
||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||
|
|
|
@ -14,7 +14,7 @@ kapt {
|
|||
// Note: 2 digits max for each value
|
||||
ext.versionMajor = 1
|
||||
ext.versionMinor = 1
|
||||
ext.versionPatch = 14
|
||||
ext.versionPatch = 15
|
||||
|
||||
ext.scVersion = 39
|
||||
|
||||
|
@ -146,6 +146,8 @@ android {
|
|||
|
||||
buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping"
|
||||
|
||||
buildConfigField "Long", "VOICE_MESSAGE_DURATION_LIMIT_MS", "120_000L"
|
||||
|
||||
// If set, MSC3086 asserted identity messages sent on VoIP calls will cause the call to appear in the room corresponding to the asserted identity.
|
||||
// This *must* only be set in trusted environments.
|
||||
buildConfigField "Boolean", "handleCallAssertedIdentityEvents", "false"
|
||||
|
@ -344,7 +346,7 @@ dependencies {
|
|||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation "androidx.fragment:fragment-ktx:$fragment_version"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.0-beta02'
|
||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation "androidx.media:media:1.4.0"
|
||||
|
@ -406,6 +408,7 @@ dependencies {
|
|||
implementation "androidx.autofill:autofill:$autofill_version"
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
|
||||
implementation 'com.github.Armen101:AudioRecordView:1.0.5'
|
||||
|
||||
// Custom Tab
|
||||
implementation 'androidx.browser:browser:1.3.0'
|
||||
|
@ -413,6 +416,9 @@ dependencies {
|
|||
// Passphrase strength helper
|
||||
implementation 'com.nulab-inc:zxcvbn:1.5.2'
|
||||
|
||||
// To convert voice message on old platforms
|
||||
implementation 'com.arthenica:ffmpeg-kit-audio:4.4.LTS'
|
||||
|
||||
//Alerter
|
||||
implementation 'com.tapadoo.android:alerter:7.0.1'
|
||||
|
||||
|
|
|
@ -401,6 +401,11 @@ SOFTWARE.
|
|||
<br/>
|
||||
Copyright 2018, Nick / materialdesignicons.com
|
||||
</li>
|
||||
<li>
|
||||
<b>Armen101 / AudioRecordView</b>
|
||||
<br/>
|
||||
Copyright 2019 Armen Gevorgyan
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
Apache License
|
||||
|
@ -600,5 +605,18 @@ Apache License
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
<br/>
|
||||
Version 3, 29 June 2007
|
||||
</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<b>ffmpeg-kit</b>
|
||||
<br/>
|
||||
Copyright (c) 2021 Taner Sener
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -111,12 +111,12 @@ import im.vector.app.features.roomprofile.settings.RoomSettingsFragment
|
|||
import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment
|
||||
import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment
|
||||
import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment
|
||||
import im.vector.app.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
|
||||
import im.vector.app.features.settings.notifications.VectorSettingsAdvancedNotificationPreferenceFragment
|
||||
import im.vector.app.features.settings.VectorSettingsGeneralFragment
|
||||
import im.vector.app.features.settings.VectorSettingsHelpAboutFragment
|
||||
import im.vector.app.features.settings.VectorSettingsLabsFragment
|
||||
import im.vector.app.features.settings.VectorSettingsNotificationPreferenceFragment
|
||||
import im.vector.app.features.settings.VectorSettingsNotificationsTroubleshootFragment
|
||||
import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceFragment
|
||||
import im.vector.app.features.settings.notifications.VectorSettingsNotificationsTroubleshootFragment
|
||||
import im.vector.app.features.settings.VectorSettingsPinFragment
|
||||
import im.vector.app.features.settings.VectorSettingsPreferencesFragment
|
||||
import im.vector.app.features.settings.VectorSettingsSecurityPrivacyFragment
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.core.error
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.call.dialpad.DialPadLookup
|
||||
import im.vector.app.features.voice.VoiceFailure
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.api.failure.MatrixIdFailure
|
||||
|
@ -123,11 +124,19 @@ class DefaultErrorFormatter @Inject constructor(
|
|||
stringProvider.getString(R.string.call_dial_pad_lookup_error)
|
||||
is MatrixIdFailure.InvalidMatrixId ->
|
||||
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
|
||||
is VoiceFailure -> voiceMessageError(throwable)
|
||||
else -> throwable.localizedMessage
|
||||
}
|
||||
?: stringProvider.getString(R.string.unknown_error)
|
||||
}
|
||||
|
||||
private fun voiceMessageError(throwable: VoiceFailure): String {
|
||||
return when (throwable) {
|
||||
is VoiceFailure.UnableToPlay -> stringProvider.getString(R.string.error_voice_message_unable_to_play)
|
||||
is VoiceFailure.UnableToRecord -> stringProvider.getString(R.string.error_voice_message_unable_to_record)
|
||||
}
|
||||
}
|
||||
|
||||
private fun limitExceededError(error: MatrixError): String {
|
||||
val delay = error.retryAfterMillis
|
||||
|
||||
|
|
|
@ -22,18 +22,23 @@ import android.view.View
|
|||
import android.widget.RadioGroup
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import im.vector.app.R
|
||||
import org.matrix.android.sdk.api.pushrules.Action
|
||||
import org.matrix.android.sdk.api.pushrules.RuleIds
|
||||
import org.matrix.android.sdk.api.pushrules.RuleSetKey
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushRuleAndKind
|
||||
|
||||
class PushRulePreference : VectorPreference {
|
||||
|
||||
enum class NotificationIndex(val index: Int) {
|
||||
OFF(0),
|
||||
SILENT(1),
|
||||
NOISY(2);
|
||||
|
||||
companion object {
|
||||
fun fromInt(index: Int) = values().first { it.index == index }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the selected push rule and its kind
|
||||
* @return the selected push rule index
|
||||
*/
|
||||
var ruleAndKind: PushRuleAndKind? = null
|
||||
var index: NotificationIndex? = null
|
||||
private set
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
@ -47,44 +52,12 @@ class PushRulePreference : VectorPreference {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return the bing rule status index
|
||||
*/
|
||||
private val ruleStatusIndex: Int
|
||||
get() {
|
||||
val safeRule = ruleAndKind?.pushRule ?: return NOTIFICATION_OFF_INDEX
|
||||
|
||||
if (safeRule.ruleId == RuleIds.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) {
|
||||
if (safeRule.shouldNotNotify()) {
|
||||
return if (safeRule.enabled) {
|
||||
NOTIFICATION_OFF_INDEX
|
||||
} else {
|
||||
NOTIFICATION_SILENT_INDEX
|
||||
}
|
||||
} else if (safeRule.shouldNotify()) {
|
||||
return NOTIFICATION_NOISY_INDEX
|
||||
}
|
||||
}
|
||||
|
||||
if (safeRule.enabled) {
|
||||
return if (safeRule.shouldNotNotify()) {
|
||||
NOTIFICATION_OFF_INDEX
|
||||
} else if (safeRule.getNotificationSound() != null) {
|
||||
NOTIFICATION_NOISY_INDEX
|
||||
} else {
|
||||
NOTIFICATION_SILENT_INDEX
|
||||
}
|
||||
}
|
||||
|
||||
return NOTIFICATION_OFF_INDEX
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the push rule.
|
||||
* Update the notification index.
|
||||
*
|
||||
* @param pushRule
|
||||
*/
|
||||
fun setPushRule(pushRuleAndKind: PushRuleAndKind?) {
|
||||
ruleAndKind = pushRuleAndKind
|
||||
fun setIndex(notificationIndex: NotificationIndex?) {
|
||||
index = notificationIndex
|
||||
refreshSummary()
|
||||
}
|
||||
|
||||
|
@ -92,74 +65,13 @@ class PushRulePreference : VectorPreference {
|
|||
* Refresh the summary
|
||||
*/
|
||||
private fun refreshSummary() {
|
||||
summary = context.getString(when (ruleStatusIndex) {
|
||||
NOTIFICATION_OFF_INDEX -> R.string.notification_off
|
||||
NOTIFICATION_SILENT_INDEX -> R.string.notification_silent
|
||||
else -> R.string.notification_noisy
|
||||
summary = context.getString(when (index) {
|
||||
NotificationIndex.OFF -> R.string.notification_off
|
||||
NotificationIndex.SILENT -> R.string.notification_silent
|
||||
NotificationIndex.NOISY, null -> R.string.notification_noisy
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a push rule with the updated required at index.
|
||||
*
|
||||
* @param index index
|
||||
* @return a push rule with the updated flags / null if there is no update
|
||||
*/
|
||||
fun createNewRule(index: Int): PushRule? {
|
||||
val safeRule = ruleAndKind?.pushRule ?: return null
|
||||
val safeKind = ruleAndKind?.kind ?: return null
|
||||
|
||||
return if (index != ruleStatusIndex) {
|
||||
if (safeRule.ruleId == RuleIds.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) {
|
||||
when (index) {
|
||||
NOTIFICATION_OFF_INDEX -> {
|
||||
safeRule.copy(enabled = true)
|
||||
.setNotify(false)
|
||||
.removeNotificationSound()
|
||||
}
|
||||
NOTIFICATION_SILENT_INDEX -> {
|
||||
safeRule.copy(enabled = false)
|
||||
.setNotify(false)
|
||||
}
|
||||
NOTIFICATION_NOISY_INDEX -> {
|
||||
safeRule.copy(enabled = true)
|
||||
.setNotify(true)
|
||||
.setNotificationSound()
|
||||
}
|
||||
else -> safeRule
|
||||
}
|
||||
} else {
|
||||
if (NOTIFICATION_OFF_INDEX == index) {
|
||||
if (safeKind == RuleSetKey.UNDERRIDE || safeRule.ruleId == RuleIds.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) {
|
||||
safeRule.setNotify(false)
|
||||
} else {
|
||||
safeRule.copy(enabled = false)
|
||||
}
|
||||
} else {
|
||||
val newRule = safeRule.copy(enabled = true)
|
||||
.setNotify(true)
|
||||
.setHighlight(safeKind != RuleSetKey.UNDERRIDE
|
||||
&& safeRule.ruleId != RuleIds.RULE_ID_INVITE_ME
|
||||
&& NOTIFICATION_NOISY_INDEX == index)
|
||||
|
||||
if (NOTIFICATION_NOISY_INDEX == index) {
|
||||
newRule.setNotificationSound(
|
||||
if (safeRule.ruleId == RuleIds.RULE_ID_CALL) {
|
||||
Action.ACTION_OBJECT_VALUE_VALUE_RING
|
||||
} else {
|
||||
Action.ACTION_OBJECT_VALUE_VALUE_DEFAULT
|
||||
}
|
||||
)
|
||||
} else {
|
||||
newRule.removeNotificationSound()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
safeRule
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
|
||||
|
@ -170,14 +82,14 @@ class PushRulePreference : VectorPreference {
|
|||
val radioGroup = holder.findViewById(R.id.bingPreferenceRadioGroup) as? RadioGroup
|
||||
radioGroup?.setOnCheckedChangeListener(null)
|
||||
|
||||
when (ruleStatusIndex) {
|
||||
NOTIFICATION_OFF_INDEX -> {
|
||||
when (index) {
|
||||
NotificationIndex.OFF -> {
|
||||
radioGroup?.check(R.id.bingPreferenceRadioBingRuleOff)
|
||||
}
|
||||
NOTIFICATION_SILENT_INDEX -> {
|
||||
NotificationIndex.SILENT -> {
|
||||
radioGroup?.check(R.id.bingPreferenceRadioBingRuleSilent)
|
||||
}
|
||||
else -> {
|
||||
NotificationIndex.NOISY -> {
|
||||
radioGroup?.check(R.id.bingPreferenceRadioBingRuleNoisy)
|
||||
}
|
||||
}
|
||||
|
@ -185,23 +97,15 @@ class PushRulePreference : VectorPreference {
|
|||
radioGroup?.setOnCheckedChangeListener { _, checkedId ->
|
||||
when (checkedId) {
|
||||
R.id.bingPreferenceRadioBingRuleOff -> {
|
||||
onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_OFF_INDEX)
|
||||
onPreferenceChangeListener?.onPreferenceChange(this, NotificationIndex.OFF)
|
||||
}
|
||||
R.id.bingPreferenceRadioBingRuleSilent -> {
|
||||
onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_SILENT_INDEX)
|
||||
onPreferenceChangeListener?.onPreferenceChange(this, NotificationIndex.SILENT)
|
||||
}
|
||||
R.id.bingPreferenceRadioBingRuleNoisy -> {
|
||||
onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_NOISY_INDEX)
|
||||
onPreferenceChangeListener?.onPreferenceChange(this, NotificationIndex.NOISY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
// index in mRuleStatuses
|
||||
private const val NOTIFICATION_OFF_INDEX = 0
|
||||
private const val NOTIFICATION_SILENT_INDEX = 1
|
||||
private const val NOTIFICATION_NOISY_INDEX = 2
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,6 +136,9 @@ class CallRingPlayerOutgoing(
|
|||
mediaPlayer.setAudioAttributes(AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
// TODO Change to ?
|
||||
// .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
|
||||
// .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||
.build())
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
|
|
|
@ -21,16 +21,17 @@ import java.util.concurrent.TimeUnit
|
|||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class CountUpTimer(private val intervalInMs: Long) {
|
||||
class CountUpTimer(private val intervalInMs: Long = 1_000) {
|
||||
|
||||
private val elapsedTime: AtomicLong = AtomicLong()
|
||||
private val resumed: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
private val disposable = Observable.interval(intervalInMs, TimeUnit.MILLISECONDS)
|
||||
private val disposable = Observable.interval(intervalInMs / 10, TimeUnit.MILLISECONDS)
|
||||
.filter { resumed.get() }
|
||||
.doOnNext { elapsedTime.addAndGet(intervalInMs) }
|
||||
.map { elapsedTime.addAndGet(intervalInMs / 10) }
|
||||
.filter { it % intervalInMs == 0L }
|
||||
.subscribe {
|
||||
tickListener?.onTick(elapsedTime.get())
|
||||
tickListener?.onTick(it)
|
||||
}
|
||||
|
||||
var tickListener: TickListener? = null
|
||||
|
|
|
@ -34,6 +34,7 @@ import im.vector.app.core.platform.VectorBaseActivity
|
|||
// Permissions sets
|
||||
val PERMISSIONS_FOR_AUDIO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO)
|
||||
val PERMISSIONS_FOR_VIDEO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
val PERMISSIONS_FOR_VOICE_MESSAGE = listOf(Manifest.permission.RECORD_AUDIO)
|
||||
val PERMISSIONS_FOR_TAKING_PHOTO = listOf(Manifest.permission.CAMERA)
|
||||
val PERMISSIONS_FOR_MEMBERS_SEARCH = listOf(Manifest.permission.READ_CONTACTS)
|
||||
val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA)
|
||||
|
|
|
@ -57,7 +57,8 @@ fun MultiPickerAudioType.toContentAttachmentData(): ContentAttachmentData {
|
|||
size = size,
|
||||
name = displayName,
|
||||
duration = duration,
|
||||
queryUri = contentUri
|
||||
queryUri = contentUri,
|
||||
waveform = waveform
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.net.Uri
|
|||
import android.view.View
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
|
@ -107,5 +108,14 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
|
||||
// Failed messages
|
||||
object RemoveAllFailedMessages : RoomDetailAction()
|
||||
|
||||
data class RoomUpgradeSuccess(val replacementRoomId: String): RoomDetailAction()
|
||||
|
||||
// Voice Message
|
||||
object StartRecordingVoiceMessage : RoomDetailAction()
|
||||
data class EndRecordingVoiceMessage(val isCancelled: Boolean) : RoomDetailAction()
|
||||
object PauseRecordingVoiceMessage : RoomDetailAction()
|
||||
data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : RoomDetailAction()
|
||||
object PlayOrPauseRecordingPlayback : RoomDetailAction()
|
||||
object EndAllVoiceActions : RoomDetailAction()
|
||||
}
|
||||
|
|
|
@ -51,6 +51,8 @@ class RoomDetailActivity :
|
|||
return ActivityRoomDetailBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
override fun getCoordinatorLayout() = views.coordinatorLayout
|
||||
|
||||
private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel
|
||||
private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel()
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import android.os.Build
|
|||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.text.Spannable
|
||||
import android.text.format.DateUtils
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
|
@ -81,6 +82,7 @@ import im.vector.app.core.extensions.showKeyboard
|
|||
import im.vector.app.core.extensions.trackItemsVisibilityChange
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.glide.GlideRequests
|
||||
import im.vector.app.core.hardware.vibrate
|
||||
import im.vector.app.core.intent.getFilenameFromUri
|
||||
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
|
@ -94,6 +96,7 @@ import im.vector.app.core.ui.views.NotificationAreaView
|
|||
import im.vector.app.core.utils.Debouncer
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.core.utils.KeyboardStateUtils
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_MESSAGE
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES
|
||||
import im.vector.app.core.utils.checkPermissions
|
||||
import im.vector.app.core.utils.colorizeMatchingText
|
||||
|
@ -102,6 +105,7 @@ import im.vector.app.core.utils.createJSonViewerStyleProvider
|
|||
import im.vector.app.core.utils.createUIHandler
|
||||
import im.vector.app.core.utils.isValidUrl
|
||||
import im.vector.app.core.utils.onPermissionDeniedDialog
|
||||
import im.vector.app.core.utils.onPermissionDeniedSnackbar
|
||||
import im.vector.app.core.utils.openUrlInExternalBrowser
|
||||
import im.vector.app.core.utils.registerForPermissionsResult
|
||||
import im.vector.app.core.utils.saveMedia
|
||||
|
@ -127,6 +131,7 @@ import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
|||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.ScSdkPreferences
|
||||
import im.vector.app.features.home.room.detail.composer.TextComposerView
|
||||
import im.vector.app.features.home.room.detail.composer.VoiceMessageRecorderView
|
||||
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction
|
||||
|
@ -134,12 +139,14 @@ import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBot
|
|||
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
|
||||
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
|
@ -164,6 +171,7 @@ import im.vector.app.features.settings.VectorSettingsActivity
|
|||
import im.vector.app.features.share.SharedData
|
||||
import im.vector.app.features.spaces.share.ShareSpaceBottomSheet
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import im.vector.app.features.voice.VoiceFailure
|
||||
import im.vector.app.features.widgets.WidgetActivity
|
||||
import im.vector.app.features.widgets.WidgetArgs
|
||||
import im.vector.app.features.widgets.WidgetKind
|
||||
|
@ -176,11 +184,13 @@ import nl.dionsegijn.konfetti.models.Shape
|
|||
import nl.dionsegijn.konfetti.models.Size
|
||||
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
||||
import org.commonmark.parser.Parser
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
|
||||
|
@ -231,7 +241,8 @@ class RoomDetailFragment @Inject constructor(
|
|||
private val imageContentRenderer: ImageContentRenderer,
|
||||
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||
private val callManager: WebRtcCallManager
|
||||
private val callManager: WebRtcCallManager,
|
||||
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker
|
||||
) :
|
||||
VectorBaseFragment<FragmentRoomDetailBinding>(),
|
||||
TimelineEventController.Callback,
|
||||
|
@ -338,6 +349,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
setupConfBannerView()
|
||||
setupEmojiPopup()
|
||||
setupFailedMessagesWarningView()
|
||||
setupVoiceMessageView()
|
||||
|
||||
views.roomToolbarContentView.debouncedClicks {
|
||||
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
|
||||
|
@ -379,7 +391,12 @@ class RoomDetailFragment @Inject constructor(
|
|||
|
||||
roomDetailViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
|
||||
is RoomDetailViewEvents.Failure -> {
|
||||
if (it.throwable is VoiceFailure.UnableToRecord) {
|
||||
onCannotRecord()
|
||||
}
|
||||
showErrorInSnackbar(it.throwable)
|
||||
}
|
||||
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
|
||||
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
|
||||
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
|
||||
|
@ -421,6 +438,11 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onCannotRecord() {
|
||||
// Update the UI, cancel the animation
|
||||
views.voiceMessageRecorderView.initVoiceRecordingViews()
|
||||
}
|
||||
|
||||
private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
|
||||
val intent = VectorCallActivity.newIntent(
|
||||
context = vectorBaseActivity,
|
||||
|
@ -607,6 +629,45 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private val permissionVoiceMessageLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
|
||||
if (allGranted) {
|
||||
// In this case, let the user start again the gesture
|
||||
} else if (deniedPermanently) {
|
||||
vectorBaseActivity.onPermissionDeniedSnackbar(R.string.denied_permission_voice_message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupVoiceMessageView() {
|
||||
views.voiceMessageRecorderView.voiceMessagePlaybackTracker = voiceMessagePlaybackTracker
|
||||
|
||||
views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback {
|
||||
override fun onVoiceRecordingStarted(): Boolean {
|
||||
return if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
|
||||
views.composerLayout.isInvisible = true
|
||||
roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage)
|
||||
vibrate(requireContext())
|
||||
true
|
||||
} else {
|
||||
// Permission dialog is displayed
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVoiceRecordingEnded(isCancelled: Boolean) {
|
||||
views.composerLayout.isInvisible = false
|
||||
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled))
|
||||
}
|
||||
|
||||
override fun onVoiceRecordingPlaybackModeOn() {
|
||||
roomDetailViewModel.handle(RoomDetailAction.PauseRecordingVoiceMessage)
|
||||
}
|
||||
|
||||
override fun onVoicePlaybackButtonClicked() {
|
||||
roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseRecordingPlayback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
|
||||
navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo))
|
||||
}
|
||||
|
@ -922,6 +983,8 @@ class RoomDetailFragment @Inject constructor(
|
|||
autoCompleter.exitSpecialMode()
|
||||
views.composerLayout.collapse()
|
||||
|
||||
views.voiceMessageRecorderView.isVisible = text.isBlank() && vectorPreferences.labsUseVoiceMessage()
|
||||
|
||||
updateComposerText(text)
|
||||
views.composerLayout.views.sendButton.contentDescription = getString(R.string.send)
|
||||
}
|
||||
|
@ -938,7 +1001,12 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
val messageContent: MessageContent? = event.getLastMessageContent()
|
||||
val nonFormattedBody = messageContent?.body ?: ""
|
||||
val nonFormattedBody = if (messageContent is MessageAudioContent && messageContent.voiceMessageIndicator != null) {
|
||||
val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
|
||||
getString(R.string.voice_message_reply_content, formattedDuration)
|
||||
} else {
|
||||
messageContent?.body ?: ""
|
||||
}
|
||||
var formattedBody: CharSequence? = null
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
|
||||
val parser = Parser.builder().build()
|
||||
|
@ -968,6 +1036,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
// need to do it here also when not using quick reply
|
||||
focusComposerAndShowKeyboard()
|
||||
views.composerLayout.views.composerRelatedMessageImage.isVisible = isImageVisible
|
||||
views.voiceMessageRecorderView.isVisible = false
|
||||
}
|
||||
}
|
||||
focusComposerAndShowKeyboard()
|
||||
|
@ -1008,6 +1077,10 @@ class RoomDetailFragment @Inject constructor(
|
|||
notificationDrawerManager.setCurrentRoom(null)
|
||||
|
||||
roomDetailViewModel.handle(RoomDetailAction.SaveDraft(views.composerLayout.text.toString()))
|
||||
|
||||
// We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed.
|
||||
roomDetailViewModel.handle(RoomDetailAction.EndAllVoiceActions)
|
||||
views.voiceMessageRecorderView.initVoiceRecordingViews()
|
||||
}
|
||||
|
||||
private val attachmentFileActivityResultLauncher = registerStartForActivityResult {
|
||||
|
@ -1146,6 +1219,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
return when (model) {
|
||||
is MessageFileItem,
|
||||
is MessageVoiceItem,
|
||||
is MessageImageVideoItem,
|
||||
is MessageTextItem -> {
|
||||
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
|
||||
|
@ -1247,7 +1321,14 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onTextBlankStateChanged(isBlank: Boolean) {
|
||||
// No op
|
||||
if (!views.composerLayout.views.sendButton.isVisible && vectorPreferences.labsUseVoiceMessage()) {
|
||||
// Animate alpha to prevent overlapping with the animation of the send button
|
||||
views.voiceMessageRecorderView.alpha = 0f
|
||||
views.voiceMessageRecorderView.isVisible = true
|
||||
views.voiceMessageRecorderView.animate().alpha(1f).setDuration(300).start()
|
||||
} else {
|
||||
views.voiceMessageRecorderView.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1258,8 +1339,9 @@ class RoomDetailFragment @Inject constructor(
|
|||
return
|
||||
}
|
||||
if (text.isNotBlank()) {
|
||||
// We collapse ASAP, if not there will be a slight anoying delay
|
||||
// We collapse ASAP, if not there will be a slight annoying delay
|
||||
views.composerLayout.collapse(true)
|
||||
views.voiceMessageRecorderView.isVisible = vectorPreferences.labsUseVoiceMessage()
|
||||
lockSendButton = true
|
||||
roomDetailViewModel.handle(RoomDetailAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
|
||||
emojiPopup.dismiss()
|
||||
|
@ -1308,22 +1390,28 @@ class RoomDetailFragment @Inject constructor(
|
|||
views.jumpToBottomView.count = summary.notificationCount
|
||||
views.jumpToBottomView.drawBadge = summary.scIsUnread(ScSdkPreferences(context))
|
||||
timelineEventController.update(state)
|
||||
views.inviteView.visibility = View.GONE
|
||||
views.inviteView.isVisible = false
|
||||
if (state.tombstoneEvent == null) {
|
||||
if (state.canSendMessage) {
|
||||
views.composerLayout.visibility = View.VISIBLE
|
||||
views.composerLayout.setRoomEncrypted(summary.isEncrypted)
|
||||
views.notificationAreaView.render(NotificationAreaView.State.Hidden)
|
||||
if (!views.voiceMessageRecorderView.isActive()) {
|
||||
views.composerLayout.isVisible = true
|
||||
views.voiceMessageRecorderView.isVisible = vectorPreferences.labsUseVoiceMessage() && views.composerLayout.text?.isBlank().orFalse()
|
||||
views.composerLayout.setRoomEncrypted(summary.isEncrypted)
|
||||
views.notificationAreaView.render(NotificationAreaView.State.Hidden)
|
||||
views.composerLayout.alwaysShowSendButton = !vectorPreferences.labsUseVoiceMessage()
|
||||
}
|
||||
} else {
|
||||
views.composerLayout.visibility = View.GONE
|
||||
views.composerLayout.isVisible = false
|
||||
views.voiceMessageRecorderView.isVisible = false
|
||||
views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost)
|
||||
}
|
||||
} else {
|
||||
views.composerLayout.visibility = View.GONE
|
||||
views.composerLayout.isVisible = false
|
||||
views.voiceMessageRecorderView.isVisible = false
|
||||
views.notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent))
|
||||
}
|
||||
} else if (summary?.membership == Membership.INVITE && inviter != null) {
|
||||
views.inviteView.visibility = View.VISIBLE
|
||||
views.inviteView.isVisible = true
|
||||
views.inviteView.render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState)
|
||||
// Intercept click event
|
||||
views.inviteView.setOnClickListener { }
|
||||
|
@ -1749,6 +1837,10 @@ class RoomDetailFragment @Inject constructor(
|
|||
navigator.openBigImageViewer(requireActivity(), sharedView, mxcUrl, title)
|
||||
}
|
||||
|
||||
override fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent))
|
||||
}
|
||||
|
||||
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
||||
if (action.messageContent is MessageTextContent) {
|
||||
shareText(requireContext(), action.messageContent.body)
|
||||
|
@ -1852,13 +1944,21 @@ class RoomDetailFragment @Inject constructor(
|
|||
roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
|
||||
}
|
||||
is EventSharedAction.Edit -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))
|
||||
if (!views.voiceMessageRecorderView.isActive()) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))
|
||||
} else {
|
||||
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
||||
}
|
||||
}
|
||||
is EventSharedAction.Quote -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString()))
|
||||
}
|
||||
is EventSharedAction.Reply -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString()))
|
||||
if (!views.voiceMessageRecorderView.isActive()) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString()))
|
||||
} else {
|
||||
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
||||
}
|
||||
}
|
||||
is EventSharedAction.CopyPermalink -> {
|
||||
val permalink = session.permalinkService().createPermalink(roomDetailArgs.roomId, action.eventId)
|
||||
|
|
|
@ -38,6 +38,7 @@ import im.vector.app.core.extensions.exhaustive
|
|||
import im.vector.app.core.mvrx.runCatchingToAsync
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.attachments.toContentAttachmentData
|
||||
import im.vector.app.features.call.conference.JitsiService
|
||||
import im.vector.app.features.call.lookup.CallProtocolsChecker
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
|
@ -46,6 +47,7 @@ import im.vector.app.features.command.ParsedCommand
|
|||
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
|
||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||
import im.vector.app.features.home.room.detail.composer.VoiceMessageHelper
|
||||
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
||||
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
|
||||
|
@ -56,6 +58,7 @@ import im.vector.app.features.home.room.typing.TypingHelper
|
|||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.voice.VoicePlayerHelper
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
@ -121,6 +124,8 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
private val chatEffectManager: ChatEffectManager,
|
||||
private val directRoomHelper: DirectRoomHelper,
|
||||
private val jitsiService: JitsiService,
|
||||
private val voiceMessageHelper: VoiceMessageHelper,
|
||||
private val voicePlayerHelper: VoicePlayerHelper,
|
||||
timelineFactory: TimelineFactory
|
||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
||||
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
|
||||
|
@ -326,6 +331,12 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
||||
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
|
||||
RoomDetailAction.ResendAll -> handleResendAll()
|
||||
RoomDetailAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
|
||||
is RoomDetailAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled)
|
||||
is RoomDetailAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
|
||||
RoomDetailAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
|
||||
RoomDetailAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
|
||||
RoomDetailAction.EndAllVoiceActions -> handleEndAllVoiceActions()
|
||||
is RoomDetailAction.RoomUpgradeSuccess -> {
|
||||
setState {
|
||||
copy(joinUpgradedRoomAsync = Success(action.replacementRoomId))
|
||||
|
@ -617,6 +628,56 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleStartRecordingVoiceMessage() {
|
||||
try {
|
||||
voiceMessageHelper.startRecording()
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.Failure(failure))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean) {
|
||||
voiceMessageHelper.stopPlayback()
|
||||
if (isCancelled) {
|
||||
voiceMessageHelper.deleteRecording()
|
||||
} else {
|
||||
voiceMessageHelper.stopRecording()?.let { audioType ->
|
||||
if (audioType.duration > 1000) {
|
||||
room.sendMedia(audioType.toContentAttachmentData(), false, emptySet())
|
||||
} else {
|
||||
voiceMessageHelper.deleteRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePlayOrPauseVoicePlayback(action: RoomDetailAction.PlayOrPauseVoicePlayback) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Download can fail
|
||||
val audioFile = session.fileService().downloadFile(action.messageAudioContent)
|
||||
// Conversion can fail, fallback to the original file in this case and let the player fail for us
|
||||
val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile
|
||||
// Play can fail
|
||||
voiceMessageHelper.startOrPausePlayback(action.eventId, convertedFile)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.Failure(failure))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePlayOrPauseRecordingPlayback() {
|
||||
voiceMessageHelper.startOrPauseRecordingPlayback()
|
||||
}
|
||||
|
||||
private fun handleEndAllVoiceActions() {
|
||||
voiceMessageHelper.stopAllVoiceActions()
|
||||
}
|
||||
|
||||
private fun handlePauseRecordingVoiceMessage() {
|
||||
voiceMessageHelper.pauseRecording()
|
||||
}
|
||||
|
||||
private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled()
|
||||
|
||||
fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state ->
|
||||
|
|
|
@ -25,6 +25,7 @@ import android.view.ViewGroup
|
|||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.ChangeBounds
|
||||
|
@ -35,6 +36,7 @@ import androidx.transition.TransitionSet
|
|||
import im.vector.app.R
|
||||
import im.vector.app.databinding.ComposerLayoutBinding
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
||||
/**
|
||||
* Encapsulate the timeline composer UX.
|
||||
|
@ -61,6 +63,13 @@ class TextComposerView @JvmOverloads constructor(
|
|||
val text: Editable?
|
||||
get() = views.composerEditText.text
|
||||
|
||||
var alwaysShowSendButton = false
|
||||
set(value) {
|
||||
field = value
|
||||
val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || text?.isNotBlank().orFalse() || value
|
||||
views.sendButton.isInvisible = !shouldShowSendButton
|
||||
}
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.composer_layout, this)
|
||||
views = ComposerLayoutBinding.bind(this)
|
||||
|
@ -73,19 +82,19 @@ class TextComposerView @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
override fun onTextBlankStateChanged(isBlank: Boolean) {
|
||||
callback?.onTextBlankStateChanged(isBlank)
|
||||
/*
|
||||
val shouldBeVisible = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isBlank
|
||||
val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isBlank || alwaysShowSendButton
|
||||
TransitionManager.endTransitions(this@TextComposerView)
|
||||
if (views.sendButton.isVisible != shouldBeVisible) {
|
||||
if (views.sendButton.isVisible != shouldShowSendButton) {
|
||||
TransitionManager.beginDelayedTransition(
|
||||
this@TextComposerView,
|
||||
AutoTransition().also { it.duration = 150 }
|
||||
)
|
||||
views.sendButton.isVisible = shouldBeVisible
|
||||
views.sendButton.isInvisible = !shouldShowSendButton
|
||||
}
|
||||
*/
|
||||
updateSendButtonColor(isBlank)
|
||||
callback?.onTextBlankStateChanged(isBlank)
|
||||
}
|
||||
}
|
||||
views.composerRelatedMessageCloseButton.setOnClickListener {
|
||||
|
@ -115,7 +124,11 @@ class TextComposerView @JvmOverloads constructor(
|
|||
}
|
||||
currentConstraintSetId = R.layout.composer_layout_constraint_set_compact
|
||||
applyNewConstraintSet(animate, transitionComplete)
|
||||
//views.sendButton.isVisible = !views.composerEditText.text.isNullOrEmpty()
|
||||
|
||||
/*
|
||||
val shouldShowSendButton = !views.composerEditText.text.isNullOrEmpty() || alwaysShowSendButton
|
||||
views.sendButton.isInvisible = !shouldShowSendButton
|
||||
*/
|
||||
}
|
||||
|
||||
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
||||
|
@ -125,7 +138,7 @@ class TextComposerView @JvmOverloads constructor(
|
|||
}
|
||||
currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded
|
||||
applyNewConstraintSet(animate, transitionComplete)
|
||||
//views.sendButton.isVisible = true
|
||||
//views.sendButton.isInvisible = false
|
||||
}
|
||||
|
||||
private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.composer
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import androidx.core.content.FileProvider
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.core.utils.CountUpTimer
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.voice.VoiceFailure
|
||||
import im.vector.app.features.voice.VoiceRecorder
|
||||
import im.vector.app.features.voice.VoiceRecorderProvider
|
||||
import im.vector.lib.multipicker.entity.MultiPickerAudioType
|
||||
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Helper class to record audio for voice messages.
|
||||
*/
|
||||
class VoiceMessageHelper @Inject constructor(
|
||||
private val context: Context,
|
||||
private val playbackTracker: VoiceMessagePlaybackTracker,
|
||||
voiceRecorderProvider: VoiceRecorderProvider
|
||||
) {
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
private var voiceRecorder: VoiceRecorder = voiceRecorderProvider.provideVoiceRecorder()
|
||||
|
||||
private val amplitudeList = mutableListOf<Int>()
|
||||
|
||||
private var amplitudeTicker: CountUpTimer? = null
|
||||
private var playbackTicker: CountUpTimer? = null
|
||||
|
||||
fun startRecording() {
|
||||
stopPlayback()
|
||||
playbackTracker.makeAllPlaybacksIdle()
|
||||
amplitudeList.clear()
|
||||
|
||||
try {
|
||||
voiceRecorder.startRecord()
|
||||
} catch (failure: Throwable) {
|
||||
throw VoiceFailure.UnableToRecord(failure)
|
||||
}
|
||||
startRecordingAmplitudes()
|
||||
}
|
||||
|
||||
fun stopRecording(): MultiPickerAudioType? {
|
||||
tryOrNull("Cannot stop media recording amplitude") {
|
||||
stopRecordingAmplitudes()
|
||||
}
|
||||
val voiceMessageFile = tryOrNull("Cannot stop media recorder!") {
|
||||
voiceRecorder.stopRecord()
|
||||
voiceRecorder.getVoiceMessageFile()
|
||||
}
|
||||
try {
|
||||
voiceMessageFile?.let {
|
||||
val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it)
|
||||
return outputFileUri
|
||||
?.toMultiPickerAudioType(context)
|
||||
?.apply {
|
||||
waveform = if (amplitudeList.size < 50) {
|
||||
amplitudeList
|
||||
} else {
|
||||
amplitudeList.chunked(amplitudeList.size / 50) { items -> items.maxOrNull() ?: 0 }
|
||||
}
|
||||
}
|
||||
} ?: return null
|
||||
} catch (e: FileNotFoundException) {
|
||||
Timber.e(e, "Cannot stop voice recording")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When entering in playback mode actually
|
||||
*/
|
||||
fun pauseRecording() {
|
||||
voiceRecorder.stopRecord()
|
||||
stopRecordingAmplitudes()
|
||||
}
|
||||
|
||||
fun deleteRecording() {
|
||||
tryOrNull("Cannot stop media recording amplitude") {
|
||||
stopRecordingAmplitudes()
|
||||
}
|
||||
tryOrNull("Cannot stop media recorder!") {
|
||||
voiceRecorder.cancelRecord()
|
||||
}
|
||||
}
|
||||
|
||||
fun startOrPauseRecordingPlayback() {
|
||||
voiceRecorder.getCurrentRecord()?.let {
|
||||
startOrPausePlayback(VoiceMessagePlaybackTracker.RECORDING_ID, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun startOrPausePlayback(id: String, file: File) {
|
||||
stopPlayback()
|
||||
stopRecordingAmplitudes()
|
||||
if (playbackTracker.getPlaybackState(id) is VoiceMessagePlaybackTracker.Listener.State.Playing) {
|
||||
playbackTracker.pausePlayback(id)
|
||||
} else {
|
||||
startPlayback(id, file)
|
||||
playbackTracker.startPlayback(id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPlayback(id: String, file: File) {
|
||||
val currentPlaybackTime = playbackTracker.getPlaybackTime(id)
|
||||
|
||||
try {
|
||||
FileInputStream(file).use { fis ->
|
||||
mediaPlayer = MediaPlayer().apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
// Do not use CONTENT_TYPE_SPEECH / USAGE_VOICE_COMMUNICATION because we want to play loud here
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
setDataSource(fis.fd)
|
||||
prepare()
|
||||
start()
|
||||
seekTo(currentPlaybackTime)
|
||||
}
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
throw VoiceFailure.UnableToPlay(failure)
|
||||
}
|
||||
startPlaybackTicker(id)
|
||||
}
|
||||
|
||||
fun stopPlayback() {
|
||||
mediaPlayer?.stop()
|
||||
stopPlaybackTicker()
|
||||
}
|
||||
|
||||
private fun startRecordingAmplitudes() {
|
||||
amplitudeTicker?.stop()
|
||||
amplitudeTicker = CountUpTimer(50).apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
onAmplitudeTick()
|
||||
}
|
||||
}
|
||||
resume()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAmplitudeTick() {
|
||||
try {
|
||||
val maxAmplitude = voiceRecorder.getMaxAmplitude()
|
||||
amplitudeList.add(maxAmplitude)
|
||||
playbackTracker.updateCurrentRecording(VoiceMessagePlaybackTracker.RECORDING_ID, amplitudeList)
|
||||
} catch (e: IllegalStateException) {
|
||||
Timber.e(e, "Cannot get max amplitude. Amplitude recording timer will be stopped.")
|
||||
stopRecordingAmplitudes()
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.e(e, "Cannot get max amplitude (native error). Amplitude recording timer will be stopped.")
|
||||
stopRecordingAmplitudes()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopRecordingAmplitudes() {
|
||||
amplitudeTicker?.stop()
|
||||
amplitudeTicker = null
|
||||
}
|
||||
|
||||
private fun startPlaybackTicker(id: String) {
|
||||
playbackTicker?.stop()
|
||||
playbackTicker = CountUpTimer().apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
onPlaybackTick(id)
|
||||
}
|
||||
}
|
||||
resume()
|
||||
}
|
||||
onPlaybackTick(id)
|
||||
}
|
||||
|
||||
private fun onPlaybackTick(id: String) {
|
||||
if (mediaPlayer?.isPlaying.orFalse()) {
|
||||
val currentPosition = mediaPlayer?.currentPosition ?: 0
|
||||
playbackTracker.updateCurrentPlaybackTime(id, currentPosition)
|
||||
} else {
|
||||
playbackTracker.stopPlayback(id)
|
||||
stopPlaybackTicker()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopPlaybackTicker() {
|
||||
playbackTicker?.stop()
|
||||
playbackTicker = null
|
||||
}
|
||||
|
||||
fun stopAllVoiceActions() {
|
||||
stopRecording()
|
||||
stopPlayback()
|
||||
deleteRecording()
|
||||
playbackTracker.clear()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,519 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.composer
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.DateUtils
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.hardware.vibrate
|
||||
import im.vector.app.core.utils.CountUpTimer
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import timber.log.Timber
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
* Encapsulates the voice message recording view and animations.
|
||||
*/
|
||||
class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr), VoiceMessagePlaybackTracker.Listener {
|
||||
|
||||
interface Callback {
|
||||
// Return true if the recording is started
|
||||
fun onVoiceRecordingStarted(): Boolean
|
||||
fun onVoiceRecordingEnded(isCancelled: Boolean)
|
||||
fun onVoiceRecordingPlaybackModeOn()
|
||||
fun onVoicePlaybackButtonClicked()
|
||||
}
|
||||
|
||||
private val views: ViewVoiceMessageRecorderBinding
|
||||
|
||||
var callback: Callback? = null
|
||||
var voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker? = null
|
||||
set(value) {
|
||||
field = value
|
||||
value?.track(VoiceMessagePlaybackTracker.RECORDING_ID, this)
|
||||
}
|
||||
|
||||
private var recordingState: RecordingState = RecordingState.NONE
|
||||
|
||||
private var firstX: Float = 0f
|
||||
private var firstY: Float = 0f
|
||||
private var lastX: Float = 0f
|
||||
private var lastY: Float = 0f
|
||||
private var lastDistanceX: Float = 0f
|
||||
private var lastDistanceY: Float = 0f
|
||||
|
||||
private var recordingTicker: CountUpTimer? = null
|
||||
|
||||
private val dimensionConverter = DimensionConverter(context.resources)
|
||||
private val minimumMove = dimensionConverter.dpToPx(16)
|
||||
private val distanceToLock = dimensionConverter.dpToPx(48).toFloat()
|
||||
private val distanceToCancel = dimensionConverter.dpToPx(120).toFloat()
|
||||
private val rtlXMultiplier = context.resources.getInteger(R.integer.rtl_x_multiplier)
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.view_voice_message_recorder, this)
|
||||
views = ViewVoiceMessageRecorderBinding.bind(this)
|
||||
|
||||
initVoiceRecordingViews()
|
||||
initListeners()
|
||||
}
|
||||
|
||||
fun initVoiceRecordingViews() {
|
||||
recordingState = RecordingState.NONE
|
||||
|
||||
hideRecordingViews(null)
|
||||
stopRecordingTicker()
|
||||
|
||||
views.voiceMessageMicButton.isVisible = true
|
||||
views.voiceMessageSendButton.isVisible = false
|
||||
|
||||
views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() }
|
||||
}
|
||||
|
||||
private fun initListeners() {
|
||||
views.voiceMessageSendButton.setOnClickListener {
|
||||
stopRecordingTicker()
|
||||
hideRecordingViews(isCancelled = false)
|
||||
views.voiceMessageSendButton.isVisible = false
|
||||
recordingState = RecordingState.NONE
|
||||
}
|
||||
|
||||
views.voiceMessageDeletePlayback.setOnClickListener {
|
||||
stopRecordingTicker()
|
||||
hideRecordingViews(isCancelled = true)
|
||||
views.voiceMessageSendButton.isVisible = false
|
||||
recordingState = RecordingState.NONE
|
||||
}
|
||||
|
||||
views.voicePlaybackWaveform.setOnClickListener {
|
||||
if (recordingState != RecordingState.PLAYBACK) {
|
||||
recordingState = RecordingState.PLAYBACK
|
||||
showPlaybackViews()
|
||||
}
|
||||
}
|
||||
|
||||
views.voicePlaybackControlButton.setOnClickListener {
|
||||
callback?.onVoicePlaybackButtonClicked()
|
||||
}
|
||||
|
||||
views.voiceMessageMicButton.setOnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
handleMicActionDown(event)
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
handleMicActionUp()
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (recordingState == RecordingState.CANCELLED) return@setOnTouchListener false
|
||||
handleMicActionMove(event)
|
||||
true
|
||||
}
|
||||
else ->
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMicActionDown(event: MotionEvent) {
|
||||
val recordingStarted = callback?.onVoiceRecordingStarted().orFalse()
|
||||
if (recordingStarted) {
|
||||
startRecordingTicker()
|
||||
renderToast(context.getString(R.string.voice_message_release_to_send_toast))
|
||||
recordingState = RecordingState.STARTED
|
||||
showRecordingViews()
|
||||
|
||||
firstX = event.rawX
|
||||
firstY = event.rawY
|
||||
lastX = firstX
|
||||
lastY = firstY
|
||||
lastDistanceX = 0F
|
||||
lastDistanceY = 0F
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMicActionUp() {
|
||||
if (recordingState != RecordingState.LOCKED && recordingState != RecordingState.NONE) {
|
||||
stopRecordingTicker()
|
||||
val isCancelled = recordingState == RecordingState.NONE || recordingState == RecordingState.CANCELLED
|
||||
recordingState = RecordingState.NONE
|
||||
hideRecordingViews(isCancelled = isCancelled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMicActionMove(event: MotionEvent) {
|
||||
val currentX = event.rawX
|
||||
val currentY = event.rawY
|
||||
|
||||
val distanceX = abs(firstX - currentX)
|
||||
val distanceY = abs(firstY - currentY)
|
||||
|
||||
val isRecordingStateChanged = updateRecordingState(currentX, currentY, distanceX, distanceY)
|
||||
|
||||
when (recordingState) {
|
||||
RecordingState.CANCELLING -> {
|
||||
val translationAmount = distanceX.coerceAtMost(distanceToCancel)
|
||||
views.voiceMessageMicButton.translationX = -translationAmount * rtlXMultiplier
|
||||
views.voiceMessageSlideToCancel.translationX = -translationAmount / 2 * rtlXMultiplier
|
||||
val reducedAlpha = (1 - translationAmount / distanceToCancel / 1.5).toFloat()
|
||||
views.voiceMessageSlideToCancel.alpha = reducedAlpha
|
||||
views.voiceMessageTimerIndicator.alpha = reducedAlpha
|
||||
views.voiceMessageTimer.alpha = reducedAlpha
|
||||
views.voiceMessageLockBackground.isVisible = false
|
||||
views.voiceMessageLockImage.isVisible = false
|
||||
views.voiceMessageLockArrow.isVisible = false
|
||||
// Reset Y translations
|
||||
views.voiceMessageMicButton.translationY = 0F
|
||||
views.voiceMessageLockArrow.translationY = 0F
|
||||
}
|
||||
RecordingState.LOCKING -> {
|
||||
views.voiceMessageLockImage.setImageResource(R.drawable.ic_voice_message_locked)
|
||||
val translationAmount = -distanceY.coerceIn(0F, distanceToLock)
|
||||
views.voiceMessageMicButton.translationY = translationAmount
|
||||
views.voiceMessageLockArrow.translationY = translationAmount
|
||||
views.voiceMessageLockArrow.alpha = 1 - (-translationAmount / distanceToLock)
|
||||
// Reset X translations
|
||||
views.voiceMessageMicButton.translationX = 0F
|
||||
views.voiceMessageSlideToCancel.translationX = 0F
|
||||
}
|
||||
RecordingState.CANCELLED -> {
|
||||
hideRecordingViews(isCancelled = true)
|
||||
}
|
||||
RecordingState.LOCKED -> {
|
||||
if (isRecordingStateChanged) { // Do not update views if it was already in locked state.
|
||||
views.voiceMessageLockImage.setImageResource(R.drawable.ic_voice_message_locked)
|
||||
views.voiceMessageLockImage.postDelayed({
|
||||
showRecordingLockedViews()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
RecordingState.STARTED -> {
|
||||
showRecordingViews()
|
||||
}
|
||||
RecordingState.NONE -> Timber.d("VoiceMessageRecorderView shouldn't be in NONE state while moving.")
|
||||
RecordingState.PLAYBACK -> Timber.d("VoiceMessageRecorderView shouldn't be in PLAYBACK state while moving.")
|
||||
}
|
||||
lastX = currentX
|
||||
lastY = currentY
|
||||
lastDistanceX = distanceX
|
||||
lastDistanceY = distanceY
|
||||
}
|
||||
|
||||
private fun updateRecordingState(currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): Boolean {
|
||||
val previousRecordingState = recordingState
|
||||
if (recordingState == RecordingState.STARTED) {
|
||||
// Determine if cancelling or locking for the first move action.
|
||||
if (((currentX < firstX && rtlXMultiplier == 1) || (currentX > firstX && rtlXMultiplier == -1))
|
||||
&& distanceX > distanceY) {
|
||||
recordingState = RecordingState.CANCELLING
|
||||
} else if (currentY < firstY && distanceY > distanceX) {
|
||||
recordingState = RecordingState.LOCKING
|
||||
}
|
||||
} else if (recordingState == RecordingState.CANCELLING) {
|
||||
// Check if cancelling conditions met, also check if it should be initial state
|
||||
if (distanceX < minimumMove && distanceX < lastDistanceX) {
|
||||
recordingState = RecordingState.STARTED
|
||||
} else if (shouldCancelRecording(distanceX)) {
|
||||
recordingState = RecordingState.CANCELLED
|
||||
}
|
||||
} else if (recordingState == RecordingState.LOCKING) {
|
||||
// Check if locking conditions met, also check if it should be initial state
|
||||
if (distanceY < minimumMove && distanceY < lastDistanceY) {
|
||||
recordingState = RecordingState.STARTED
|
||||
} else if (shouldLockRecording(distanceY)) {
|
||||
recordingState = RecordingState.LOCKED
|
||||
}
|
||||
}
|
||||
return previousRecordingState != recordingState
|
||||
}
|
||||
|
||||
private fun shouldCancelRecording(distanceX: Float): Boolean {
|
||||
return distanceX >= distanceToCancel
|
||||
}
|
||||
|
||||
private fun shouldLockRecording(distanceY: Float): Boolean {
|
||||
return distanceY >= distanceToLock
|
||||
}
|
||||
|
||||
private fun startRecordingTicker() {
|
||||
recordingTicker?.stop()
|
||||
recordingTicker = CountUpTimer().apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
onRecordingTick(milliseconds)
|
||||
}
|
||||
}
|
||||
resume()
|
||||
}
|
||||
onRecordingTick(0L)
|
||||
}
|
||||
|
||||
private fun onRecordingTick(milliseconds: Long) {
|
||||
renderRecordingTimer(milliseconds / 1_000)
|
||||
val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds
|
||||
if (timeDiffToRecordingLimit <= 0) {
|
||||
views.voiceMessageRecordingLayout.post {
|
||||
recordingState = RecordingState.PLAYBACK
|
||||
showPlaybackViews()
|
||||
stopRecordingTicker()
|
||||
}
|
||||
} else if (timeDiffToRecordingLimit in 10_000..10_999) {
|
||||
views.voiceMessageRecordingLayout.post {
|
||||
renderToast(context.getString(R.string.voice_message_n_seconds_warning_toast, floor(timeDiffToRecordingLimit / 1000f).toInt()))
|
||||
vibrate(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderToast(message: String) {
|
||||
views.voiceMessageToast.removeCallbacks(hideToastRunnable)
|
||||
views.voiceMessageToast.text = message
|
||||
views.voiceMessageToast.isVisible = true
|
||||
views.voiceMessageToast.postDelayed(hideToastRunnable, 2_000)
|
||||
}
|
||||
|
||||
private fun hideToast() {
|
||||
views.voiceMessageToast.isVisible = false
|
||||
}
|
||||
|
||||
private val hideToastRunnable = Runnable {
|
||||
views.voiceMessageToast.isVisible = false
|
||||
}
|
||||
|
||||
private fun renderRecordingTimer(recordingTimeMillis: Long) {
|
||||
val formattedTimerText = DateUtils.formatElapsedTime(recordingTimeMillis)
|
||||
if (recordingState == RecordingState.LOCKED) {
|
||||
views.voicePlaybackTime.apply {
|
||||
post {
|
||||
text = formattedTimerText
|
||||
}
|
||||
}
|
||||
} else {
|
||||
views.voiceMessageTimer.post {
|
||||
views.voiceMessageTimer.text = formattedTimerText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderRecordingWaveform(amplitudeList: List<Int>) {
|
||||
views.voicePlaybackWaveform.apply {
|
||||
post {
|
||||
amplitudeList.forEach { amplitude ->
|
||||
update(amplitude)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopRecordingTicker() {
|
||||
recordingTicker?.stop()
|
||||
recordingTicker = null
|
||||
}
|
||||
|
||||
private fun showRecordingViews() {
|
||||
views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic_recording)
|
||||
views.voiceMessageMicButton.updateLayoutParams<MarginLayoutParams> {
|
||||
setMargins(0, 0, 0, 0)
|
||||
}
|
||||
views.voiceMessageMicButton.animate().scaleX(1.5f).scaleY(1.5f).setDuration(300).start()
|
||||
|
||||
views.voiceMessageLockBackground.isVisible = true
|
||||
views.voiceMessageLockBackground.animate().setDuration(300).translationY(-dimensionConverter.dpToPx(180).toFloat()).start()
|
||||
views.voiceMessageLockImage.isVisible = true
|
||||
views.voiceMessageLockImage.setImageResource(R.drawable.ic_voice_message_unlocked)
|
||||
views.voiceMessageLockImage.animate().setDuration(500).translationY(-dimensionConverter.dpToPx(180).toFloat()).start()
|
||||
views.voiceMessageLockArrow.isVisible = true
|
||||
views.voiceMessageLockArrow.alpha = 1f
|
||||
views.voiceMessageSlideToCancel.isVisible = true
|
||||
views.voiceMessageTimerIndicator.isVisible = true
|
||||
views.voiceMessageTimer.isVisible = true
|
||||
views.voiceMessageSlideToCancel.alpha = 1f
|
||||
views.voiceMessageTimerIndicator.alpha = 1f
|
||||
views.voiceMessageTimer.alpha = 1f
|
||||
views.voiceMessageSendButton.isVisible = false
|
||||
}
|
||||
|
||||
private fun hideRecordingViews(isCancelled: Boolean?) {
|
||||
// We need to animate the lock image first
|
||||
if (recordingState != RecordingState.LOCKED || isCancelled.orFalse()) {
|
||||
views.voiceMessageLockImage.isVisible = false
|
||||
views.voiceMessageLockImage.animate().translationY(0f).start()
|
||||
views.voiceMessageLockBackground.isVisible = false
|
||||
views.voiceMessageLockBackground.animate().translationY(0f).start()
|
||||
} else {
|
||||
animateLockImageWithBackground()
|
||||
}
|
||||
views.voiceMessageLockArrow.isVisible = false
|
||||
views.voiceMessageLockArrow.animate().translationY(0f).start()
|
||||
views.voiceMessageSlideToCancel.isVisible = false
|
||||
views.voiceMessageSlideToCancel.animate().translationX(0f).translationY(0f).start()
|
||||
views.voiceMessagePlaybackLayout.isVisible = false
|
||||
|
||||
if (recordingState != RecordingState.LOCKED) {
|
||||
views.voiceMessageMicButton
|
||||
.animate()
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.translationX(0f)
|
||||
.translationY(0f)
|
||||
.setDuration(150)
|
||||
.withEndAction {
|
||||
views.voiceMessageTimerIndicator.isVisible = false
|
||||
views.voiceMessageTimer.isVisible = false
|
||||
resetMicButtonUi()
|
||||
isCancelled?.let {
|
||||
callback?.onVoiceRecordingEnded(it)
|
||||
}
|
||||
}
|
||||
.start()
|
||||
} else {
|
||||
views.voiceMessageTimerIndicator.isVisible = false
|
||||
views.voiceMessageTimer.isVisible = false
|
||||
views.voiceMessageMicButton.apply {
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
translationX = 0f
|
||||
translationY = 0f
|
||||
}
|
||||
isCancelled?.let {
|
||||
callback?.onVoiceRecordingEnded(it)
|
||||
}
|
||||
}
|
||||
|
||||
// Hide toasts if user cancelled recording before the timeout of the toast.
|
||||
if (recordingState == RecordingState.CANCELLED || recordingState == RecordingState.NONE) {
|
||||
hideToast()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetMicButtonUi() {
|
||||
views.voiceMessageMicButton.isVisible = true
|
||||
views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic)
|
||||
views.voiceMessageMicButton.updateLayoutParams<MarginLayoutParams> {
|
||||
if (rtlXMultiplier == -1) {
|
||||
// RTL
|
||||
setMargins(dimensionConverter.dpToPx(12), 0, 0, dimensionConverter.dpToPx(12))
|
||||
} else {
|
||||
setMargins(0, 0, dimensionConverter.dpToPx(12), dimensionConverter.dpToPx(12))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateLockImageWithBackground() {
|
||||
views.voiceMessageLockBackground.updateLayoutParams {
|
||||
height = dimensionConverter.dpToPx(78)
|
||||
}
|
||||
views.voiceMessageLockBackground.apply {
|
||||
animate()
|
||||
.scaleX(0f)
|
||||
.scaleY(0f)
|
||||
.setDuration(400L)
|
||||
.withEndAction {
|
||||
updateLayoutParams {
|
||||
height = dimensionConverter.dpToPx(180)
|
||||
}
|
||||
isVisible = false
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
animate().translationY(0f).start()
|
||||
}
|
||||
.start()
|
||||
}
|
||||
|
||||
// Lock image animation
|
||||
views.voiceMessageMicButton.isInvisible = true
|
||||
views.voiceMessageLockImage.apply {
|
||||
isVisible = true
|
||||
animate()
|
||||
.scaleX(0f)
|
||||
.scaleY(0f)
|
||||
.setDuration(400L)
|
||||
.withEndAction {
|
||||
isVisible = false
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
translationY = 0f
|
||||
resetMicButtonUi()
|
||||
}
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRecordingLockedViews() {
|
||||
hideRecordingViews(null)
|
||||
views.voiceMessagePlaybackLayout.isVisible = true
|
||||
views.voiceMessagePlaybackTimerIndicator.isVisible = true
|
||||
views.voicePlaybackControlButton.isVisible = false
|
||||
views.voiceMessageSendButton.isVisible = true
|
||||
renderToast(context.getString(R.string.voice_message_tap_to_stop_toast))
|
||||
}
|
||||
|
||||
private fun showPlaybackViews() {
|
||||
views.voiceMessagePlaybackTimerIndicator.isVisible = false
|
||||
views.voicePlaybackControlButton.isVisible = true
|
||||
callback?.onVoiceRecordingPlaybackModeOn()
|
||||
}
|
||||
|
||||
private enum class RecordingState {
|
||||
NONE,
|
||||
STARTED,
|
||||
CANCELLING,
|
||||
CANCELLED,
|
||||
LOCKING,
|
||||
LOCKED,
|
||||
PLAYBACK
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the voice message is recording or is in playback mode
|
||||
*/
|
||||
fun isActive() = recordingState !in listOf(RecordingState.NONE, RecordingState.CANCELLED)
|
||||
|
||||
override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Recording -> {
|
||||
renderRecordingWaveform(state.amplitudeList)
|
||||
}
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Playing -> {
|
||||
views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||
val formattedTimerText = DateUtils.formatElapsedTime((state.playbackTime / 1000).toLong())
|
||||
views.voicePlaybackTime.text = formattedTimerText
|
||||
}
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Paused,
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Idle -> {
|
||||
views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -66,6 +66,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
|
|||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
|
@ -112,6 +113,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
|
||||
// Introduce ViewModel scoped component (or Hilt?)
|
||||
fun getPreviewUrlRetriever(): PreviewUrlRetriever
|
||||
|
||||
fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent)
|
||||
}
|
||||
|
||||
interface ReactionPillCallback {
|
||||
|
|
|
@ -25,6 +25,7 @@ import android.text.style.ForegroundColorSpan
|
|||
import android.view.View
|
||||
import dagger.Lazy
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.files.LocalFilesHelper
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
|
@ -38,6 +39,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStat
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem_
|
||||
|
@ -50,6 +52,8 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageOptionsItem_
|
|||
import im.vector.app.features.home.room.detail.timeline.item.MessagePollItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
|
||||
|
@ -110,7 +114,8 @@ class MessageItemFactory @Inject constructor(
|
|||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||
private val spanUtils: SpanUtils,
|
||||
private val session: Session) {
|
||||
private val session: Session,
|
||||
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker) {
|
||||
|
||||
// TODO inject this properly?
|
||||
private var roomId: String = ""
|
||||
|
@ -154,7 +159,13 @@ class MessageItemFactory @Inject constructor(
|
|||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
|
||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
|
||||
is MessageAudioContent -> {
|
||||
if (messageContent.voiceMessageIndicator != null) {
|
||||
buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes)
|
||||
} else {
|
||||
buildAudioMessageItem(messageContent, informationData, highlight, attributes)
|
||||
}
|
||||
}
|
||||
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessagePollResponseContent -> noticeItemFactory.create(params)
|
||||
|
@ -223,6 +234,46 @@ class MessageItemFactory @Inject constructor(
|
|||
.iconRes(R.drawable.ic_headphones)
|
||||
}
|
||||
|
||||
private fun buildVoiceMessageItem(params: TimelineItemFactoryParams,
|
||||
messageContent: MessageAudioContent,
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
attributes: AbsMessageItem.Attributes): MessageVoiceItem? {
|
||||
val fileUrl = messageContent.getFileUrl()?.let {
|
||||
if (informationData.sentByMe && !informationData.sendState.isSent()) {
|
||||
it
|
||||
} else {
|
||||
it.takeIf { it.startsWith("mxc://") }
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
val playbackControlButtonClickListener: ClickListener = object : ClickListener {
|
||||
override fun invoke(view: View) {
|
||||
params.callback?.onVoiceControlButtonClicked(informationData.eventId, messageContent)
|
||||
}
|
||||
}
|
||||
|
||||
return MessageVoiceItem_()
|
||||
.attributes(attributes)
|
||||
.duration(messageContent.audioWaveformInfo?.duration ?: 0)
|
||||
.waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty())
|
||||
.playbackControlButtonClickListener(playbackControlButtonClickListener)
|
||||
.voiceMessagePlaybackTracker(voiceMessagePlaybackTracker)
|
||||
.izLocalFile(localFilesHelper.isLocalFile(fileUrl))
|
||||
.izDownloaded(session.fileService().isFileInCache(
|
||||
fileUrl,
|
||||
messageContent.getFileName(),
|
||||
messageContent.mimeType,
|
||||
messageContent.encryptedFileInfo?.toElementToDecrypt())
|
||||
)
|
||||
.mxcUrl(fileUrl)
|
||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||
.highlighted(highlight)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
}
|
||||
|
||||
private fun buildVerificationRequestMessageItem(messageContent: MessageVerificationRequestContent,
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
informationData: MessageInformationData,
|
||||
|
@ -571,6 +622,13 @@ class MessageItemFactory @Inject constructor(
|
|||
.highlighted(highlight)
|
||||
}
|
||||
|
||||
private fun List<Int>?.toFft(): List<Int>? {
|
||||
return this?.map {
|
||||
// Value comes from AudioRecordView.maxReportableAmp, and 1024 is the max value in the Matrix spec
|
||||
it * 22760 / 1024
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import im.vector.app.core.resources.StringProvider
|
|||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
|
||||
|
@ -72,7 +73,11 @@ class DisplayableEventFormatter @Inject constructor(
|
|||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor)
|
||||
}
|
||||
MessageType.MSGTYPE_AUDIO -> {
|
||||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
|
||||
if ((messageContent as? MessageAudioContent)?.voiceMessageIndicator != null) {
|
||||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor)
|
||||
} else {
|
||||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
|
||||
}
|
||||
}
|
||||
MessageType.MSGTYPE_VIDEO -> {
|
||||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video), appendAuthor)
|
||||
|
|
|
@ -76,7 +76,11 @@ class EventDetailsFormatter @Inject constructor(
|
|||
*/
|
||||
private fun formatForAudioMessage(event: Event): CharSequence? {
|
||||
return event.getClearContent().toModel<MessageAudioContent>()?.audioInfo
|
||||
?.let { "${it.duration.asDuration()} - ${it.size.asFileSize()}" }
|
||||
?.let { audioInfo ->
|
||||
listOfNotNull(audioInfo.duration?.asDuration(), audioInfo.size?.asFileSize())
|
||||
.joinToString(" - ")
|
||||
.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.helper
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import im.vector.app.core.di.ScreenScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ScreenScope
|
||||
class VoiceMessagePlaybackTracker @Inject constructor() {
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val listeners = mutableMapOf<String, Listener>()
|
||||
private val states = mutableMapOf<String, Listener.State>()
|
||||
|
||||
fun track(id: String, listener: Listener) {
|
||||
listeners[id] = listener
|
||||
|
||||
val currentState = states[id] ?: Listener.State.Idle
|
||||
mainHandler.post {
|
||||
listener.onUpdate(currentState)
|
||||
}
|
||||
}
|
||||
|
||||
fun unTrack(id: String) {
|
||||
listeners.remove(id)
|
||||
}
|
||||
|
||||
fun makeAllPlaybacksIdle() {
|
||||
listeners.keys.forEach { key ->
|
||||
setState(key, Listener.State.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set state and notify the listeners
|
||||
*/
|
||||
private fun setState(key: String, state: Listener.State) {
|
||||
states[key] = state
|
||||
mainHandler.post {
|
||||
listeners[key]?.onUpdate(state)
|
||||
}
|
||||
}
|
||||
|
||||
fun startPlayback(id: String) {
|
||||
val currentPlaybackTime = getPlaybackTime(id)
|
||||
val currentState = Listener.State.Playing(currentPlaybackTime)
|
||||
setState(id, currentState)
|
||||
// Pause any active playback
|
||||
states
|
||||
.filter { it.key != id }
|
||||
.keys
|
||||
.forEach { key ->
|
||||
val state = states[key]
|
||||
if (state is Listener.State.Playing) {
|
||||
setState(key, Listener.State.Paused(state.playbackTime))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pausePlayback(id: String) {
|
||||
val currentPlaybackTime = getPlaybackTime(id)
|
||||
setState(id, Listener.State.Paused(currentPlaybackTime))
|
||||
}
|
||||
|
||||
fun stopPlayback(id: String) {
|
||||
setState(id, Listener.State.Idle)
|
||||
}
|
||||
|
||||
fun updateCurrentPlaybackTime(id: String, time: Int) {
|
||||
setState(id, Listener.State.Playing(time))
|
||||
}
|
||||
|
||||
fun updateCurrentRecording(id: String, amplitudeList: List<Int>) {
|
||||
setState(id, Listener.State.Recording(amplitudeList))
|
||||
}
|
||||
|
||||
fun getPlaybackState(id: String) = states[id]
|
||||
|
||||
fun getPlaybackTime(id: String): Int {
|
||||
return when (val state = states[id]) {
|
||||
is Listener.State.Playing -> state.playbackTime
|
||||
is Listener.State.Paused -> state.playbackTime
|
||||
/* Listener.State.Idle, */
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
listeners.forEach {
|
||||
it.value.onUpdate(Listener.State.Idle)
|
||||
}
|
||||
listeners.clear()
|
||||
states.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val RECORDING_ID = "RECORDING_ID"
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
|
||||
fun onUpdate(state: State)
|
||||
|
||||
sealed class State {
|
||||
object Idle : State()
|
||||
data class Playing(val playbackTime: Int) : State()
|
||||
data class Paused(val playbackTime: Int) : State()
|
||||
data class Recording(val amplitudeList: List<Int>) : State()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -51,6 +51,8 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
lateinit var dimensionConverter: DimensionConverter
|
||||
|
||||
protected var ignoreSendStatusVisibility = false
|
||||
|
||||
@CallSuper
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
|
@ -61,6 +63,14 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||
this.marginStart = leftGuideline
|
||||
}
|
||||
}
|
||||
// Ignore visibility of the send status icon?
|
||||
holder.contentContainer.updateLayoutParams<RelativeLayout.LayoutParams> {
|
||||
if (ignoreSendStatusVisibility) {
|
||||
addRule(RelativeLayout.ALIGN_PARENT_END)
|
||||
} else {
|
||||
removeRule(RelativeLayout.ALIGN_PARENT_END)
|
||||
}
|
||||
}
|
||||
holder.checkableBackground.isChecked = highlighted
|
||||
|
||||
updateMessageBubble(holder.checkableBackground.context, holder)
|
||||
|
@ -68,6 +78,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||
|
||||
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
|
||||
val leftGuideline by bind<View>(R.id.messageStartGuideline)
|
||||
val contentContainer by bind<View>(R.id.viewStubContainer)
|
||||
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
||||
|
||||
override fun bindView(itemView: View) {
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.visualizer.amplitude.AudioRecordView
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
||||
|
||||
init {
|
||||
ignoreSendStatusVisibility = true
|
||||
}
|
||||
|
||||
@EpoxyAttribute
|
||||
var mxcUrl: String = ""
|
||||
|
||||
@EpoxyAttribute
|
||||
var duration: Int = 0
|
||||
|
||||
@EpoxyAttribute
|
||||
var waveform: List<Int> = emptyList()
|
||||
|
||||
@EpoxyAttribute
|
||||
var izLocalFile = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var izDownloaded = false
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var playbackControlButtonClickListener: ClickListener? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
renderSendState(holder.voiceLayout, null)
|
||||
if (!attributes.informationData.sendState.hasFailed()) {
|
||||
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, izLocalFile, holder.progressLayout)
|
||||
} else {
|
||||
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_cross)
|
||||
holder.progressLayout.isVisible = false
|
||||
}
|
||||
|
||||
holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
|
||||
holder.voicePlaybackWaveform.post {
|
||||
holder.voicePlaybackWaveform.recreate()
|
||||
waveform.forEach { amplitude ->
|
||||
holder.voicePlaybackWaveform.update(amplitude)
|
||||
}
|
||||
}
|
||||
|
||||
holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
|
||||
|
||||
voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
|
||||
override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun renderIdleState(holder: Holder) {
|
||||
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
holder.voicePlaybackTime.text = formatPlaybackTime(duration)
|
||||
}
|
||||
|
||||
private fun renderPlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing) {
|
||||
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||
holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
||||
}
|
||||
|
||||
private fun renderPausedState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Paused) {
|
||||
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
||||
}
|
||||
|
||||
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
super.unbind(holder)
|
||||
contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId)
|
||||
contentDownloadStateTrackerBinder.unbind(mxcUrl)
|
||||
voiceMessagePlaybackTracker.unTrack(attributes.informationData.eventId)
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val voiceLayout by bind<ViewGroup>(R.id.voiceLayout)
|
||||
val voicePlaybackControlButton by bind<ImageButton>(R.id.voicePlaybackControlButton)
|
||||
val voicePlaybackTime by bind<TextView>(R.id.voicePlaybackTime)
|
||||
val voicePlaybackWaveform by bind<AudioRecordView>(R.id.voicePlaybackWaveform)
|
||||
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentVoiceStub
|
||||
}
|
||||
}
|
|
@ -156,6 +156,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
|||
const val SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE = "SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE"
|
||||
const val SETTINGS_LABS_SPACES_HOME_AS_ORPHAN = "SETTINGS_LABS_SPACES_HOME_AS_ORPHAN"
|
||||
|
||||
const val SETTINGS_LABS_VOICE_MESSAGE = "SETTINGS_LABS_VOICE_MESSAGE"
|
||||
|
||||
private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
|
||||
private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
|
||||
private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
|
||||
|
@ -1092,4 +1094,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
|||
putInt(TAKE_PHOTO_VIDEO_MODE, mode)
|
||||
}
|
||||
}
|
||||
|
||||
fun labsUseVoiceMessage(): Boolean {
|
||||
return defaultPrefs.getBoolean(SETTINGS_LABS_VOICE_MESSAGE, false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import im.vector.app.core.extensions.replaceFragment
|
|||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.databinding.ActivityVectorSettingsBinding
|
||||
import im.vector.app.features.settings.devices.VectorSettingsDevicesFragment
|
||||
import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceFragment
|
||||
|
||||
import org.matrix.android.sdk.api.failure.GlobalError
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.notifications
|
||||
|
||||
import im.vector.app.core.preference.PushRulePreference
|
||||
import org.matrix.android.sdk.api.pushrules.RuleIds
|
||||
|
||||
fun getStandardAction(ruleId: String, index: PushRulePreference.NotificationIndex): StandardActions? {
|
||||
return when (ruleId) {
|
||||
RuleIds.RULE_ID_CONTAIN_DISPLAY_NAME ->
|
||||
when (index) {
|
||||
PushRulePreference.NotificationIndex.OFF -> StandardActions.Disabled
|
||||
PushRulePreference.NotificationIndex.SILENT -> StandardActions.Notify
|
||||
PushRulePreference.NotificationIndex.NOISY -> StandardActions.HighlightDefaultSound
|
||||
}
|
||||
RuleIds.RULE_ID_CONTAIN_USER_NAME ->
|
||||
when (index) {
|
||||
PushRulePreference.NotificationIndex.OFF -> StandardActions.Disabled
|
||||
PushRulePreference.NotificationIndex.SILENT -> StandardActions.Notify
|
||||
PushRulePreference.NotificationIndex.NOISY -> StandardActions.HighlightDefaultSound
|
||||
}
|
||||
RuleIds.RULE_ID_ROOM_NOTIF ->
|
||||
when (index) {
|
||||
PushRulePreference.NotificationIndex.OFF -> StandardActions.Disabled
|
||||
PushRulePreference.NotificationIndex.SILENT -> StandardActions.Notify
|
||||
PushRulePreference.NotificationIndex.NOISY -> StandardActions.Highlight
|
||||
}
|
||||
RuleIds.RULE_ID_ONE_TO_ONE_ROOM ->
|
||||
when (index) {
|
||||
PushRulePreference.NotificationIndex.OFF -> StandardActions.DontNotify
|
||||
PushRulePreference.NotificationIndex.SILENT -> StandardActions.Notify
|
||||
PushRulePreference.NotificationIndex.NOISY -> StandardActions.NotifyDefaultSound
|
||||
}
|
||||
RuleIds.RULE_ID_ONE_TO_ONE_ENCRYPTED_ROOM ->
|
||||
when (index) {
|
||||
PushRulePreference.NotificationIndex.OFF -> StandardActions.DontNotify
|
||||
PushRulePreference.NotificationIndex.SILENT -> StandardActions.Notify
|
||||
PushRulePreference.NotificationIndex.NOISY -> StandardActions.NotifyDefaultSound
|
||||
}
|
||||
RuleIds.RULE_ID_ALL_OTHER_MESSAGES_ROOMS ->
|
||||
when (index) {
|
||||
PushRulePreference.NotificationIndex.OFF -> StandardActions.DontNotify
|
||||
PushRulePreference.NotificationIndex.SILENT -> StandardActions.Notify
|
||||
PushRulePreference.NotificationIndex.NOISY -> StandardActions.NotifyDefaultSound
|
||||
}
|
||||
RuleIds.RULE_ID_ENCRYPTED ->
|
||||
when (index) {
|
||||
PushRulePreference.NotificationIndex.OFF -> StandardActions.DontNotify
|
||||
PushRulePreference.NotificationIndex.SILENT -> StandardActions.Notify
|
||||
PushRulePreference.NotificationIndex.NOISY -> StandardActions.NotifyDefaultSound
|
||||
}
|
||||
RuleIds.RULE_ID_INVITE_ME ->
|
||||
when (index) {
|
||||
PushRulePreference.NotificationIndex.OFF -> StandardActions.Disabled
|
||||
PushRulePreference.NotificationIndex.SILENT -> StandardActions.Notify
|
||||
PushRulePreference.NotificationIndex.NOISY -> StandardActions.NotifyDefaultSound
|
||||
}
|
||||
RuleIds.RULE_ID_CALL ->
|
||||
when (index) {
|
||||
PushRulePreference.NotificationIndex.OFF -> StandardActions.Disabled
|
||||
PushRulePreference.NotificationIndex.SILENT -> StandardActions.Notify
|
||||
PushRulePreference.NotificationIndex.NOISY -> StandardActions.NotifyRingSound
|
||||
}
|
||||
RuleIds.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS ->
|
||||
when (index) {
|
||||
PushRulePreference.NotificationIndex.OFF -> StandardActions.DontNotify
|
||||
PushRulePreference.NotificationIndex.SILENT -> StandardActions.Disabled
|
||||
PushRulePreference.NotificationIndex.NOISY -> StandardActions.NotifyDefaultSound
|
||||
}
|
||||
RuleIds.RULE_ID_TOMBSTONE ->
|
||||
when (index) {
|
||||
PushRulePreference.NotificationIndex.OFF -> StandardActions.Disabled
|
||||
PushRulePreference.NotificationIndex.SILENT -> StandardActions.Notify
|
||||
PushRulePreference.NotificationIndex.NOISY -> StandardActions.Highlight
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.notifications
|
||||
|
||||
import org.matrix.android.sdk.api.pushrules.Action
|
||||
|
||||
sealed class StandardActions(
|
||||
val actions: List<Action>?
|
||||
) {
|
||||
object Notify : StandardActions(actions = listOf(Action.Notify))
|
||||
object NotifyDefaultSound : StandardActions(actions = listOf(Action.Notify, Action.Sound()))
|
||||
object NotifyRingSound : StandardActions(actions = listOf(Action.Notify, Action.Sound(sound = Action.ACTION_OBJECT_VALUE_VALUE_RING)))
|
||||
object Highlight : StandardActions(actions = listOf(Action.Notify, Action.Highlight(highlight = true)))
|
||||
object HighlightDefaultSound : StandardActions(actions = listOf(Action.Notify, Action.Highlight(highlight = true), Action.Sound()))
|
||||
object DontNotify : StandardActions(actions = listOf(Action.DoNotNotify))
|
||||
object Disabled : StandardActions(actions = null)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -13,17 +13,21 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.app.features.settings
|
||||
package im.vector.app.features.settings.notifications
|
||||
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.preference.PushRulePreference.NotificationIndex
|
||||
import im.vector.app.core.preference.PushRulePreference
|
||||
import im.vector.app.core.preference.VectorPreference
|
||||
import im.vector.app.core.utils.toast
|
||||
import im.vector.app.features.settings.VectorSettingsBaseFragment
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.pushrules.RuleIds
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||
import org.matrix.android.sdk.api.pushrules.rest.PushRuleAndKind
|
||||
import org.matrix.android.sdk.api.pushrules.toJson
|
||||
import javax.inject.Inject
|
||||
|
||||
class VectorSettingsAdvancedNotificationPreferenceFragment @Inject constructor()
|
||||
|
@ -45,24 +49,29 @@ class VectorSettingsAdvancedNotificationPreferenceFragment @Inject constructor()
|
|||
preference.isVisible = false
|
||||
} else {
|
||||
preference.isVisible = true
|
||||
preference.setPushRule(ruleAndKind)
|
||||
val initialIndex = getNotificationIndexForRule(ruleAndKind.pushRule)
|
||||
preference.setIndex(initialIndex)
|
||||
preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val newRule = preference.createNewRule(newValue as Int)
|
||||
if (newRule != null) {
|
||||
val newIndex = newValue as NotificationIndex
|
||||
val standardAction = getStandardAction(ruleAndKind.pushRule.ruleId, newIndex)
|
||||
if (standardAction != null) {
|
||||
val enabled = standardAction != StandardActions.Disabled
|
||||
val newActions = standardAction.actions
|
||||
displayLoadingView()
|
||||
|
||||
lifecycleScope.launch {
|
||||
val result = runCatching {
|
||||
session.updatePushRuleActions(ruleAndKind.kind,
|
||||
preference.ruleAndKind?.pushRule ?: ruleAndKind.pushRule,
|
||||
newRule)
|
||||
ruleAndKind.pushRule.ruleId,
|
||||
enabled,
|
||||
newActions)
|
||||
}
|
||||
if (!isAdded) {
|
||||
return@launch
|
||||
}
|
||||
hideLoadingView()
|
||||
result.onSuccess {
|
||||
preference.setPushRule(ruleAndKind.copy(pushRule = newRule))
|
||||
preference.setIndex(newIndex)
|
||||
}
|
||||
result.onFailure { failure ->
|
||||
// Restore the previous value
|
||||
|
@ -78,6 +87,28 @@ class VectorSettingsAdvancedNotificationPreferenceFragment @Inject constructor()
|
|||
}
|
||||
}
|
||||
|
||||
private fun getNotificationIndexForRule(rule: PushRule): NotificationIndex? {
|
||||
return NotificationIndex.values().firstOrNull {
|
||||
// Get the actions for the index
|
||||
val standardAction = getStandardAction(rule.ruleId, it) ?: return@firstOrNull false
|
||||
val indexActions = standardAction.actions ?: listOf()
|
||||
// Check if the input rule matches a rule generated from the static rule definitions
|
||||
val targetRule = rule.copy(enabled = standardAction != StandardActions.Disabled, actions = indexActions.toJson())
|
||||
ruleMatches(rule, targetRule)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ruleMatches(rule: PushRule, targetRule: PushRule): Boolean {
|
||||
// Rules match if both are disabled, or if both are enabled and their highlight/sound/notify actions match up.
|
||||
return (!rule.enabled && !targetRule.enabled)
|
||||
|| (rule.enabled
|
||||
&& targetRule.enabled
|
||||
&& rule.getHighlight() == targetRule.getHighlight()
|
||||
&& rule.getNotificationSound() == targetRule.getNotificationSound()
|
||||
&& rule.shouldNotify() == targetRule.shouldNotify()
|
||||
&& rule.shouldNotNotify() == targetRule.shouldNotNotify())
|
||||
}
|
||||
|
||||
private fun refreshDisplay() {
|
||||
listView?.adapter?.notifyDataSetChanged()
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings
|
||||
package im.vector.app.features.settings.notifications
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
|
@ -37,6 +37,11 @@ import im.vector.app.core.pushers.PushersManager
|
|||
import im.vector.app.core.utils.isIgnoringBatteryOptimizations
|
||||
import im.vector.app.core.utils.requestDisablingBatteryOptimization
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import im.vector.app.features.settings.BackgroundSyncMode
|
||||
import im.vector.app.features.settings.BackgroundSyncModeChooserDialog
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.settings.VectorSettingsBaseFragment
|
||||
import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener
|
||||
import im.vector.app.push.fcm.FcmHelper
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -13,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.app.features.settings
|
||||
package im.vector.app.features.settings.notifications
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
|
@ -36,6 +36,7 @@ import im.vector.app.core.platform.VectorBaseFragment
|
|||
import im.vector.app.databinding.FragmentSettingsNotificationsTroubleshootBinding
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import im.vector.app.features.rageshake.BugReporter
|
||||
import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener
|
||||
import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager
|
||||
import im.vector.app.features.settings.troubleshoot.TroubleshootTest
|
||||
import im.vector.app.push.fcm.NotificationTroubleshootTestManagerFactory
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.voice
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaRecorder
|
||||
import android.os.Build
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
abstract class AbstractVoiceRecorder(
|
||||
context: Context,
|
||||
private val filenameExt: String
|
||||
) : VoiceRecorder {
|
||||
private val outputDirectory = File(context.cacheDir, "voice_records")
|
||||
|
||||
private var mediaRecorder: MediaRecorder? = null
|
||||
private var outputFile: File? = null
|
||||
|
||||
init {
|
||||
if (!outputDirectory.exists()) {
|
||||
outputDirectory.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun setOutputFormat(mediaRecorder: MediaRecorder)
|
||||
abstract fun convertFile(recordedFile: File?): File?
|
||||
|
||||
private fun init() {
|
||||
MediaRecorder().let {
|
||||
it.setAudioSource(MediaRecorder.AudioSource.DEFAULT)
|
||||
setOutputFormat(it)
|
||||
it.setAudioEncodingBitRate(24000)
|
||||
it.setAudioSamplingRate(48000)
|
||||
mediaRecorder = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun startRecord() {
|
||||
init()
|
||||
outputFile = File(outputDirectory, "Voice message.$filenameExt")
|
||||
|
||||
val mr = mediaRecorder ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
mr.setOutputFile(outputFile)
|
||||
} else {
|
||||
mr.setOutputFile(FileOutputStream(outputFile).fd)
|
||||
}
|
||||
mr.prepare()
|
||||
mr.start()
|
||||
}
|
||||
|
||||
override fun stopRecord() {
|
||||
// Can throw when the record is less than 1 second.
|
||||
mediaRecorder?.let {
|
||||
it.stop()
|
||||
it.reset()
|
||||
it.release()
|
||||
}
|
||||
mediaRecorder = null
|
||||
}
|
||||
|
||||
override fun cancelRecord() {
|
||||
stopRecord()
|
||||
|
||||
outputFile?.delete()
|
||||
outputFile = null
|
||||
}
|
||||
|
||||
override fun getMaxAmplitude(): Int {
|
||||
return mediaRecorder?.maxAmplitude ?: 0
|
||||
}
|
||||
|
||||
override fun getCurrentRecord(): File? {
|
||||
return outputFile
|
||||
}
|
||||
|
||||
override fun getVoiceMessageFile(): File? {
|
||||
return convertFile(outputFile)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.voice
|
||||
|
||||
sealed class VoiceFailure(cause: Throwable? = null) : Throwable(cause = cause) {
|
||||
data class UnableToPlay(val throwable: Throwable) : VoiceFailure(throwable)
|
||||
data class UnableToRecord(val throwable: Throwable) : VoiceFailure(throwable)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.voice
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.ReturnCode
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class VoicePlayerHelper @Inject constructor(
|
||||
context: Context
|
||||
) {
|
||||
private val outputDirectory = File(context.cacheDir, "voice_records")
|
||||
|
||||
init {
|
||||
if (!outputDirectory.exists()) {
|
||||
outputDirectory.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the file is encoded using aac audio codec
|
||||
*/
|
||||
fun convertFile(file: File): File? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Nothing to do
|
||||
file
|
||||
} else {
|
||||
// Convert to mp4
|
||||
val targetFile = File(outputDirectory, "Voice.mp4")
|
||||
if (targetFile.exists()) {
|
||||
targetFile.delete()
|
||||
}
|
||||
val start = System.currentTimeMillis()
|
||||
val session = FFmpegKit.execute("-i \"${file.path}\" -c:a aac \"${targetFile.path}\"")
|
||||
val duration = System.currentTimeMillis() - start
|
||||
Timber.d("Convert to mp4 in $duration ms. Size in bytes from ${file.length()} to ${targetFile.length()}")
|
||||
return when {
|
||||
ReturnCode.isSuccess(session.returnCode) -> {
|
||||
// SUCCESS
|
||||
targetFile
|
||||
}
|
||||
ReturnCode.isCancel(session.returnCode) -> {
|
||||
// CANCEL
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
// FAILURE
|
||||
Timber.e("Command failed with state ${session.state} and rc ${session.returnCode}.${session.failStackTrace}")
|
||||
// TODO throw?
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.voice
|
||||
|
||||
import java.io.File
|
||||
|
||||
interface VoiceRecorder {
|
||||
/**
|
||||
* Start the recording
|
||||
*/
|
||||
fun startRecord()
|
||||
|
||||
/**
|
||||
* Stop the recording
|
||||
*/
|
||||
fun stopRecord()
|
||||
|
||||
/**
|
||||
* Remove the file
|
||||
*/
|
||||
fun cancelRecord()
|
||||
|
||||
fun getMaxAmplitude(): Int
|
||||
|
||||
/**
|
||||
* Not guaranteed to be a ogg file
|
||||
*/
|
||||
fun getCurrentRecord(): File?
|
||||
|
||||
/**
|
||||
* Guaranteed to be a ogg file
|
||||
*/
|
||||
fun getVoiceMessageFile(): File?
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.voice
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaRecorder
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||
import com.arthenica.ffmpegkit.Level
|
||||
import com.arthenica.ffmpegkit.ReturnCode
|
||||
import im.vector.app.BuildConfig
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
class VoiceRecorderL(context: Context) : AbstractVoiceRecorder(context, "mp4") {
|
||||
override fun setOutputFormat(mediaRecorder: MediaRecorder) {
|
||||
// Use AAC/MP4 format here
|
||||
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
||||
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
|
||||
}
|
||||
|
||||
override fun convertFile(recordedFile: File?): File? {
|
||||
if (BuildConfig.DEBUG) {
|
||||
FFmpegKitConfig.setLogLevel(Level.AV_LOG_INFO)
|
||||
}
|
||||
recordedFile ?: return null
|
||||
// Convert to OGG
|
||||
val targetFile = File(recordedFile.path.removeSuffix("mp4") + "ogg")
|
||||
if (targetFile.exists()) {
|
||||
targetFile.delete()
|
||||
}
|
||||
val start = System.currentTimeMillis()
|
||||
val session = FFmpegKit.execute("-i \"${recordedFile.path}\" -c:a libvorbis \"${targetFile.path}\"")
|
||||
val duration = System.currentTimeMillis() - start
|
||||
Timber.d("Convert to ogg in $duration ms. Size in bytes from ${recordedFile.length()} to ${targetFile.length()}")
|
||||
return when {
|
||||
ReturnCode.isSuccess(session.returnCode) -> {
|
||||
// SUCCESS
|
||||
targetFile
|
||||
}
|
||||
ReturnCode.isCancel(session.returnCode) -> {
|
||||
// CANCEL
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
// FAILURE
|
||||
Timber.e("Command failed with state ${session.state} and rc ${session.returnCode}.${session.failStackTrace}")
|
||||
// TODO throw?
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.voice
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import javax.inject.Inject
|
||||
|
||||
class VoiceRecorderProvider @Inject constructor(
|
||||
private val context: Context
|
||||
) {
|
||||
fun provideVoiceRecorder(): VoiceRecorder {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
VoiceRecorderQ(context)
|
||||
} else {
|
||||
VoiceRecorderL(context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.voice
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaRecorder
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.io.File
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
class VoiceRecorderQ(context: Context) : AbstractVoiceRecorder(context, "ogg") {
|
||||
override fun setOutputFormat(mediaRecorder: MediaRecorder) {
|
||||
// We can directly use OGG here
|
||||
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.OGG)
|
||||
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS)
|
||||
}
|
||||
|
||||
override fun convertFile(recordedFile: File?): File? {
|
||||
// Nothing to do here
|
||||
return recordedFile
|
||||
}
|
||||
}
|
14
vector/src/main/res/drawable/bg_voice_message_lock.xml
Normal file
14
vector/src/main/res/drawable/bg_voice_message_lock.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size
|
||||
android:width="78dp"
|
||||
android:height="160dp" />
|
||||
|
||||
<solid android:color="#F00" />
|
||||
|
||||
<corners
|
||||
android:bottomLeftRadius="39dp"
|
||||
android:bottomRightRadius="39dp"
|
||||
android:topLeftRadius="39dp"
|
||||
android:topRightRadius="39dp" />
|
||||
</shape>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
|
||||
<size android:width="32dp" android:height="32dp" />
|
||||
<!-- Tint color is provided by the theme -->
|
||||
<solid android:color="@android:color/black" />
|
||||
</shape>
|
13
vector/src/main/res/drawable/bg_voice_playback.xml
Normal file
13
vector/src/main/res/drawable/bg_voice_playback.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Tint color is provided by the theme -->
|
||||
<solid android:color="@android:color/black" />
|
||||
<size
|
||||
android:width="240dp"
|
||||
android:height="44dp" />
|
||||
<corners
|
||||
android:bottomLeftRadius="12dp"
|
||||
android:bottomRightRadius="12dp"
|
||||
android:topLeftRadius="12dp"
|
||||
android:topRightRadius="12dp" />
|
||||
</shape>
|
12
vector/src/main/res/drawable/ic_play_pause_pause.xml
Normal file
12
vector/src/main/res/drawable/ic_play_pause_pause.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#F00"
|
||||
android:pathData="M3,3C3,2.4477 3.4477,2 4,2H5C5.5523,2 6,2.4477 6,3V13C6,13.5523 5.5523,14 5,14H4C3.4477,14 3,13.5523 3,13V3Z" />
|
||||
<path
|
||||
android:fillColor="#F00"
|
||||
android:pathData="M10,3C10,2.4477 10.4477,2 11,2H12C12.5523,2 13,2.4477 13,3V13C13,13.5523 12.5523,14 12,14H11C10.4477,14 10,13.5523 10,13V3Z" />
|
||||
</vector>
|
9
vector/src/main/res/drawable/ic_play_pause_play.xml
Normal file
9
vector/src/main/res/drawable/ic_play_pause_play.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#F00"
|
||||
android:pathData="M3,14.2104V1.7896C3,1.0072 3.8578,0.5279 4.5241,0.9379L14.6161,7.1483C15.2506,7.5388 15.2506,8.4612 14.6161,8.8517L4.5241,15.0621C3.8578,15.4721 3,14.9928 3,14.2104Z" />
|
||||
</vector>
|
9
vector/src/main/res/drawable/ic_recycle_bin.xml
Normal file
9
vector/src/main/res/drawable/ic_recycle_bin.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="14dp"
|
||||
android:height="18dp"
|
||||
android:viewportWidth="14"
|
||||
android:viewportHeight="18">
|
||||
<path
|
||||
android:pathData="M1,16C1,17.1 1.9,18 3,18H11C12.1,18 13,17.1 13,16V6C13,4.9 12.1,4 11,4H3C1.9,4 1,4.9 1,6V16ZM13,1H10.5L9.79,0.29C9.61,0.11 9.35,0 9.09,0H4.91C4.65,0 4.39,0.11 4.21,0.29L3.5,1H1C0.45,1 0,1.45 0,2C0,2.55 0.45,3 1,3H13C13.55,3 14,2.55 14,2C14,1.45 13.55,1 13,1Z"
|
||||
android:fillColor="#8D99A5"/>
|
||||
</vector>
|
13
vector/src/main/res/drawable/ic_voice_lock_arrow.xml
Normal file
13
vector/src/main/res/drawable/ic_voice_lock_arrow.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M6,15L12,9L18,15"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#F00"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
5
vector/src/main/res/drawable/ic_voice_message_locked.xml
Normal file
5
vector/src/main/res/drawable/ic_voice_message_locked.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#0DBD8B" android:fillType="evenOdd" android:pathData="M11.3333,2C8.3878,2 6,4.3878 6,7.3333V10C4.8954,10 4,10.8954 4,12V20C4,21.1046 4.8954,22 6,22H18C19.1046,22 20,21.1046 20,20V12C20,10.8954 19.1046,10 18,10V7.3333C18,4.3878 15.6122,2 12.6667,2H11.3333ZM15.3333,10V7.3333C15.3333,5.8606 14.1394,4.6667 12.6667,4.6667H11.3333C9.8606,4.6667 8.6667,5.8606 8.6667,7.3333V10H15.3333Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="16dp"
|
||||
android:viewportHeight="16" android:viewportWidth="16"
|
||||
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#8E99A4" android:fillType="evenOdd" android:pathData="M7.4444,0C4.9899,0 3,1.9334 3,4.3183V6.1369C2.4115,6.3926 2,6.979 2,7.6615V14.3385C2,15.2561 2.7439,16 3.6615,16H12.3385C13.2561,16 14,15.2561 14,14.3385V7.6615C14,6.7439 13.2561,6 12.3385,6H5.2222V4.3183C5.2222,3.1259 6.2171,2.1592 7.4444,2.1592H8.5556C9.7829,2.1592 10.7778,3.1259 10.7778,4.3183H13C13,1.9334 11.0102,0 8.5556,0H7.4444Z"/>
|
||||
</vector>
|
12
vector/src/main/res/drawable/ic_voice_mic.xml
Normal file
12
vector/src/main/res/drawable/ic_voice_mic.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M10.8,8.2C10.8,5.3281 13.1282,3 16,3C18.8719,3 21.2,5.3281 21.2,8.2V15.9767C21.2,18.8486 18.8719,21.1767 16,21.1767C13.1282,21.1767 10.8,18.8486 10.8,15.9767V8.2Z"
|
||||
android:fillColor="?vctr_content_tertiary"/>
|
||||
<path
|
||||
android:pathData="M6.8998,14.3167C7.8203,14.3167 8.5665,15.0629 8.5665,15.9834C8.5665,20.0737 11.8818,23.3944 15.98,23.4051C15.9867,23.405 15.9934,23.4049 16.0001,23.4049C16.0068,23.4049 16.0134,23.405 16.0201,23.4051C20.1181,23.3941 23.4332,20.0735 23.4332,15.9834C23.4332,15.0629 24.1793,14.3167 25.0998,14.3167C26.0203,14.3167 26.7665,15.0629 26.7665,15.9834C26.7665,21.3586 22.8201,25.8101 17.6667,26.6103V27.6683C17.6667,28.5888 16.9206,29.335 16.0001,29.335C15.0796,29.335 14.3334,28.5888 14.3334,27.6683V26.6104C9.1798,25.8104 5.2332,21.3587 5.2332,15.9834C5.2332,15.0629 5.9794,14.3167 6.8998,14.3167Z"
|
||||
android:fillColor="?vctr_content_tertiary"/>
|
||||
</vector>
|
18
vector/src/main/res/drawable/ic_voice_mic_recording.xml
Normal file
18
vector/src/main/res/drawable/ic_voice_mic_recording.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="52dp"
|
||||
android:height="52dp"
|
||||
android:viewportWidth="52"
|
||||
android:viewportHeight="52">
|
||||
<path
|
||||
android:pathData="M26.173,26.1729m-22.7631,0a22.7631,22.7631 0,1 1,45.5262 0a22.7631,22.7631 0,1 1,-45.5262 0"
|
||||
android:fillColor="#0DBD8B"/>
|
||||
<path
|
||||
android:pathData="M26,26m-26,0a26,26 0,1 1,52 0a26,26 0,1 1,-52 0"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#0DBD8B"
|
||||
android:fillAlpha="0.2"/>
|
||||
<path
|
||||
android:pathData="M21.2414,18.7749C21.2414,16.051 23.4496,13.8429 26.1734,13.8429C28.8973,13.8429 31.1054,16.051 31.1054,18.7749V26.1509C31.1054,28.8747 28.8973,31.0829 26.1734,31.0829C23.4496,31.0829 21.2414,28.8747 21.2414,26.1509V18.7749ZM17.542,24.2475C18.5968,24.2475 19.4518,25.1025 19.4518,26.1572C19.4518,29.8561 22.4509,32.8596 26.1586,32.8675C26.1637,32.8674 26.1689,32.8674 26.174,32.8674C26.179,32.8674 26.184,32.8674 26.189,32.8675C29.896,32.8589 32.8944,29.8556 32.8944,26.1572C32.8944,25.1025 33.7494,24.2475 34.8041,24.2475C35.8588,24.2475 36.7138,25.1025 36.7138,26.1572C36.7138,31.3227 32.9916,35.6165 28.0837,36.5143V37.24C28.0837,38.2947 27.2287,39.1497 26.174,39.1497C25.1193,39.1497 24.2643,38.2947 24.2643,37.24V36.5147C19.3555,35.6176 15.6323,31.3233 15.6323,26.1572C15.6323,25.1025 16.4873,24.2475 17.542,24.2475Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M15,18L9,12L15,6"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#F00"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
|
@ -140,4 +140,16 @@
|
|||
app:tint="?vctr_content_tertiary"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<!--
|
||||
<ImageButton
|
||||
android:id="@+id/voiceMessageMicButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:contentDescription="@string/a11y_start_voice_message"
|
||||
android:src="@drawable/ic_voice_mic" />
|
||||
-->
|
||||
|
||||
</merge>
|
||||
|
|
|
@ -189,4 +189,18 @@
|
|||
tools:ignore="MissingPrefix"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!--
|
||||
<ImageButton
|
||||
android:id="@+id/voiceMessageMicButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:contentDescription="@string/a11y_start_voice_message"
|
||||
android:src="@drawable/ic_voice_mic"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
-->
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -130,15 +130,15 @@
|
|||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/jumpToReadMarkerView"
|
||||
style="?vctr_jump_to_unread_style"
|
||||
app:chipIcon="@drawable/ic_jump_to_unread"
|
||||
app:chipIconTint="?colorPrimary"
|
||||
app:closeIcon="@drawable/ic_close_24dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/room_jump_to_first_unread"
|
||||
android:visibility="invisible"
|
||||
app:chipIcon="@drawable/ic_jump_to_unread"
|
||||
app:chipIconTint="?colorPrimary"
|
||||
app:closeIcon="@drawable/ic_close_24dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/activeConferenceView"
|
||||
|
@ -171,9 +171,21 @@
|
|||
android:background="?android:colorBackground"
|
||||
android:minHeight="48dp"
|
||||
android:transitionName="composer"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<im.vector.app.features.home.room.detail.composer.VoiceMessageRecorderView
|
||||
android:id="@+id/voiceMessageRecorderView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<im.vector.app.features.invite.VectorInviteView
|
||||
android:id="@+id/inviteView"
|
||||
|
|
|
@ -186,6 +186,13 @@
|
|||
android:layout_marginEnd="56dp"
|
||||
android:layout="@layout/item_timeline_event_option_buttons_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentVoiceStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout_marginEnd="56dp"
|
||||
android:layout="@layout/item_timeline_event_voice_stub"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!--
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/voiceLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/voicePlaybackLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_voice_playback"
|
||||
android:backgroundTint="?vctr_content_quinary"
|
||||
android:minHeight="48dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="6dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/voicePlaybackControlButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="@drawable/bg_voice_play_pause_button"
|
||||
android:backgroundTint="?android:colorBackground"
|
||||
android:contentDescription="@string/a11y_play_voice_message"
|
||||
android:src="@drawable/ic_play_pause_play"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?vctr_content_secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/voicePlaybackTime"
|
||||
style="@style/Widget.Vector.TextView.Body.Medium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="@id/voicePlaybackControlButton"
|
||||
app:layout_constraintStart_toEndOf="@id/voicePlaybackControlButton"
|
||||
app:layout_constraintTop_toTopOf="@id/voicePlaybackControlButton"
|
||||
tools:text="0:23" />
|
||||
|
||||
<com.visualizer.amplitude.AudioRecordView
|
||||
android:id="@+id/voicePlaybackWaveform"
|
||||
style="@style/VoicePlaybackWaveform"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/voicePlaybackTime"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
<include
|
||||
android:id="@+id/messageFileUploadProgressLayout"
|
||||
layout="@layout/media_upload_download_progress_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/voicePlaybackLayout"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
240
vector/src/main/res/layout/view_voice_message_recorder.xml
Normal file
240
vector/src/main/res/layout/view_voice_message_recorder.xml
Normal file
|
@ -0,0 +1,240 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/voice_message_recording_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="200dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/voiceMessageLockBackground"
|
||||
android:layout_width="78dp"
|
||||
android:layout_height="180dp"
|
||||
android:background="@drawable/bg_voice_message_lock"
|
||||
android:backgroundTint="?vctr_system"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/voiceMessageMicButton"
|
||||
tools:translationY="-180dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/voiceMessageMicButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:contentDescription="@string/a11y_start_voice_message"
|
||||
android:src="@drawable/ic_voice_mic"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/voiceMessageSendButton"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="@drawable/bg_send"
|
||||
android:contentDescription="@string/send"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_send"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:layout_marginBottom="180dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/voiceMessageTimerIndicator"
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:contentDescription="@string/a11y_recording_voice_message"
|
||||
android:src="@drawable/circle"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/voiceMessageMicButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/voiceMessageMicButton"
|
||||
app:tint="@color/palette_vermilion"
|
||||
tools:ignore="MissingPrefix"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/voiceMessageTimer"
|
||||
style="@style/Widget.Vector.TextView.Body.Medium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/voiceMessageMicButton"
|
||||
app:layout_constraintStart_toEndOf="@id/voiceMessageTimerIndicator"
|
||||
app:layout_constraintTop_toTopOf="@id/voiceMessageMicButton"
|
||||
tools:text="0:03"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/voiceMessageSlideToCancel"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/voice_message_slide_to_cancel"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_voice_slide_to_cancel_arrow"
|
||||
app:drawableTint="?vctr_content_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="@id/voiceMessageMicButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/voiceMessageMicButton"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- Slide to cancel text should go under this view -->
|
||||
<View
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="?android:colorBackground"
|
||||
app:layout_constraintBottom_toBottomOf="@id/voiceMessageTimer"
|
||||
app:layout_constraintStart_toEndOf="@id/voiceMessageTimer"
|
||||
app:layout_constraintTop_toTopOf="@id/voiceMessageTimer" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/voiceMessageLockImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:contentDescription="@string/a11y_lock_voice_message"
|
||||
android:src="@drawable/ic_voice_message_unlocked"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="@id/voiceMessageMicButton"
|
||||
app:layout_constraintStart_toStartOf="@id/voiceMessageMicButton"
|
||||
app:layout_constraintTop_toTopOf="@id/voiceMessageLockBackground"
|
||||
tools:translationY="-180dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/voiceMessageLockArrow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_voice_lock_arrow"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@id/voiceMessageMicButton"
|
||||
app:layout_constraintEnd_toEndOf="@id/voiceMessageLockBackground"
|
||||
app:layout_constraintStart_toStartOf="@id/voiceMessageLockBackground"
|
||||
app:tint="?vctr_content_secondary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/voiceMessagePlaybackLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/voiceMessageMicButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:layout_marginBottom="120dp"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/voiceMessageDeletePlayback"
|
||||
android:layout_width="@dimen/layout_touch_size"
|
||||
android:layout_height="0dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:contentDescription="@string/a11y_delete_recorded_voice_message"
|
||||
android:src="@drawable/ic_recycle_bin"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?vctr_content_tertiary"
|
||||
tools:ignore="MissingPrefix" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/bg_voice_playback"
|
||||
android:backgroundTint="?vctr_content_quinary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/voiceMessageDeletePlayback"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/voiceMessagePlaybackTimerIndicator"
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/a11y_recording_voice_message"
|
||||
android:src="@drawable/circle"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginStart="24dp"
|
||||
app:tint="@color/palette_vermilion"
|
||||
tools:ignore="MissingPrefix" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/voicePlaybackControlButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/bg_voice_play_pause_button"
|
||||
android:backgroundTint="?vctr_system"
|
||||
android:contentDescription="@string/a11y_play_voice_message"
|
||||
android:src="@drawable/ic_play_pause_play"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?vctr_content_secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/voicePlaybackTime"
|
||||
style="@style/Widget.Vector.TextView.Body.Medium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="@id/voicePlaybackControlButton"
|
||||
app:layout_constraintStart_toEndOf="@id/voicePlaybackControlButton"
|
||||
app:layout_constraintTop_toTopOf="@id/voicePlaybackControlButton"
|
||||
app:layout_goneMarginStart="24dp"
|
||||
tools:text="0:23" />
|
||||
|
||||
<com.visualizer.amplitude.AudioRecordView
|
||||
android:id="@+id/voicePlaybackWaveform"
|
||||
style="@style/VoicePlaybackWaveform"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/voicePlaybackTime"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/voiceMessageToast"
|
||||
style="@style/Widget.Vector.TextView.Caption.Toast"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="84dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="@string/voice_message_release_to_send_toast"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -639,7 +639,7 @@
|
|||
<string name="room_resend_unsent_messages">Znovu odeslat neodeslané zprávy</string>
|
||||
<string name="room_delete_unsent_messages">Smazat neodeslané zprávy</string>
|
||||
<string name="room_message_file_not_found">Soubor nenalezen</string>
|
||||
<string name="room_do_not_have_permission_to_post">Nemáte právo odesílat v této místnosti</string>
|
||||
<string name="room_do_not_have_permission_to_post">Nemáte právo odesílat v této místnosti.</string>
|
||||
<plurals name="room_new_messages_notification">
|
||||
<item quantity="one">%d nová zpráva</item>
|
||||
<item quantity="few">%d nové zprávy</item>
|
||||
|
@ -1356,7 +1356,7 @@
|
|||
<string name="deactivate_account_submit">Deaktivovat účet</string>
|
||||
<string name="error_empty_field_enter_user_name">Zadejte, prosím, uživatelské jméno.</string>
|
||||
<string name="error_empty_field_your_password">Prosím, zadejte své heslo.</string>
|
||||
<string name="room_tombstone_versioned_description">Tato místnost byla nahrazena a není již aktivní</string>
|
||||
<string name="room_tombstone_versioned_description">Tato místnost byla nahrazena a není již aktivní.</string>
|
||||
<string name="room_tombstone_continuation_link">Konverzace pokračuje tady</string>
|
||||
<string name="room_tombstone_continuation_description">Tato místnost je pokračováním jiné konverzace</string>
|
||||
<string name="room_tombstone_predecessor_link">Po kliknutí zde uvidíte starší zprávy</string>
|
||||
|
@ -2868,4 +2868,29 @@
|
|||
<string name="create_space_alias_hint">Adresa prostoru</string>
|
||||
<string name="space_settings_alias_subtitle">Prohlédnout a spravovat adresy tohoto prostoru.</string>
|
||||
<string name="space_settings_alias_title">Adresy prostorů</string>
|
||||
<string name="room_upgrade_to_recommended_version">Aktualizujte na doporučenou verzi místnosti</string>
|
||||
<string name="room_using_unstable_room_version">Tato místnost používá místnost verze %s, kterou homeserver označil za nestabilní.</string>
|
||||
<string name="upgrade_room_no_power_to_manage">K aktualizaci místnosti potřebujete oprávnění</string>
|
||||
<string name="upgrade_room_update_parent_space">Automaticky aktualizovat mateřský prostor</string>
|
||||
<string name="upgrade_room_auto_invite">Automaticky pozvat uživatele</string>
|
||||
<string name="upgrade_public_room_from_to">Budete aktualizovat tuto místnost z %s na %s.</string>
|
||||
<string name="upgrade_room_warning">Aktualizace místnosti je pokročilá akce a obvykle se doporučuje tehdy, je-li místnost nestabilní kvůli chybám, chybějícím funkcím nebo slabým místům v zabezpečení.
|
||||
\nObvykle má vliv pouze na to, jak server místnost zpracovává.</string>
|
||||
<string name="upgrade_private_room">Aktualizovat soukromou místnost</string>
|
||||
<string name="upgrade_public_room">Aktualizovat veřejnou místnost</string>
|
||||
<string name="upgrade">Aktualizace</string>
|
||||
<string name="it_may_take_some_time">Buďte, prosím, trpěliví, může to chvíli trvat.</string>
|
||||
<string name="joining_replacement_room">Vstupte do náhradní místnosti</string>
|
||||
<string name="command_description_upgrade_room">Aktualizuje místnost na novou verzi</string>
|
||||
<string name="settings_server_room_version_unstable">nestabilní</string>
|
||||
<string name="settings_server_room_version_stable">stabilní</string>
|
||||
<string name="settings_server_default_room_version">Výchozí verze</string>
|
||||
<string name="settings_server_room_versions">Verze místností 👓</string>
|
||||
<string name="verification_scan_self_emoji_subtitle">Raději ověřit porovnáním emoji</string>
|
||||
<string name="verification_scan_with_this_device">Oskenovat tímto zařízením</string>
|
||||
<string name="verification_scan_self_notice">Oskenujte kód svým dalším zařízením nebo přepněte a oskenujte tímto zařízením</string>
|
||||
<string name="hs_client_url">URL API Homeserveru</string>
|
||||
<string name="missing_permissions_title">Chybějící oprávnění</string>
|
||||
<string name="denied_permission_camera">Pro provedení této akce udělte, prosím, oprávnění Fotoaparát v systémových nastaveních.</string>
|
||||
<string name="denied_permission_generic">Některá z oprávnění potřebných k provedení akce chybí, prosím, udělte oprávnění v systémových nastaveních.</string>
|
||||
</resources>
|
|
@ -8,7 +8,7 @@
|
|||
<string name="notice_room_join">%1$s hat den Raum betreten</string>
|
||||
<string name="notice_room_leave">%1$s hat den Raum verlassen</string>
|
||||
<string name="notice_room_reject">%1$s hat die Einladung abgelehnt</string>
|
||||
<string name="notice_room_kick">%1$s hat %2$s gekickt</string>
|
||||
<string name="notice_room_kick">%1$s hat %2$s entfernt</string>
|
||||
<string name="notice_room_unban">%1$s hat den Bann von %2$s aufgehoben</string>
|
||||
<string name="notice_room_ban">%1$s hat %2$s gebannt</string>
|
||||
<string name="notice_room_withdraw">%1$s hat die Einladung für %2$s zurückgezogen</string>
|
||||
|
@ -95,7 +95,7 @@
|
|||
<string name="notice_room_join_with_reason">%1$s ist dem Raum beigetreten. Grund: %2$s</string>
|
||||
<string name="notice_room_leave_with_reason">%1$s hat den Raum verlassen. Grund: %2$s</string>
|
||||
<string name="notice_room_reject_with_reason">%1$s hat die Einladung abgelehnt. Grund: %2$s</string>
|
||||
<string name="notice_room_kick_with_reason">%1$s hat %2$s gekickt. Grund: %3$s</string>
|
||||
<string name="notice_room_kick_with_reason">%1$s hat %2$s entfernt. Grund: %3$s</string>
|
||||
<string name="notice_room_unban_with_reason">%1$s hat Sperre von %2$s aufgehoben. Grund: %3$s</string>
|
||||
<string name="notice_room_ban_with_reason">%1$s hat %2$s verbannt. Grund: %3$s</string>
|
||||
<string name="notice_room_third_party_invite_with_reason">%1$s hat eine Einladung an %2$s gesandt um diesem Raum beizutreten. Grund: %3$s</string>
|
||||
|
@ -952,7 +952,7 @@
|
|||
<string name="filter_group_members">Filter Gruppen-Mitglieder</string>
|
||||
<string name="filter_group_rooms">Filter Gruppen-Räume</string>
|
||||
<string name="group_no_long_description">Der Community-Administrator hat keine lange Beschreibung für diese Community zur Verfügung gestellt.</string>
|
||||
<string name="has_been_kicked">Du wurdest von %2$s aus %1$s gekickt</string>
|
||||
<string name="has_been_kicked">Du wurdest von %2$s aus %1$s entfernt</string>
|
||||
<string name="has_been_banned">Du wurdest von %2$s aus %1$s verbannt</string>
|
||||
<string name="reason_colon">Grund: %1$s</string>
|
||||
<string name="rejoin">Erneut beitreten</string>
|
||||
|
@ -1075,7 +1075,7 @@
|
|||
<string name="command_description_join_room">Tritt dem Raum mit angegebenen Alias bei</string>
|
||||
<string name="command_description_part_room">Verlasse Raum</string>
|
||||
<string name="command_description_topic">Raumthema ändern</string>
|
||||
<string name="command_description_kick_user">Kickt Benutzer mit angegebener ID</string>
|
||||
<string name="command_description_kick_user">Entfernt die Person angegebener ID</string>
|
||||
<string name="command_description_nick">Ändert deinen Anzeigenamen</string>
|
||||
<string name="command_description_markdown">(De-)Aktiviert Markdown</string>
|
||||
<string name="command_description_clear_scalar_token">Um das Matrix-App-Management zu reparieren</string>
|
||||
|
@ -2628,7 +2628,7 @@
|
|||
<string name="login_social_continue_with">Mit %s weitermachen</string>
|
||||
<string name="settings_show_emoji_keyboard_summary">Knopf zum Nachrichteneditor hinzufügen, der die Emoji-Tastatur öffnet</string>
|
||||
<string name="settings_show_emoji_keyboard">Emoji-Tastatur anzeigen</string>
|
||||
<string name="settings_chat_effects_description">Nutze /confetti Kommando oder sende Nachrichten, die ❄️ oder 🎉 enthalten</string>
|
||||
<string name="settings_chat_effects_description">Nutze /confetti oder sende Nachrichten mit ❄️ oder 🎉</string>
|
||||
<string name="settings_chat_effects_title">Chateffekte</string>
|
||||
<string name="room_permissions_change_topic">Thema ändern</string>
|
||||
<string name="room_permissions_upgrade_the_room">Raum aktualisieren</string>
|
||||
|
@ -2877,4 +2877,11 @@
|
|||
<string name="space_settings_alias_subtitle">Adressen des Spaces anzeigen und verwalten.</string>
|
||||
<string name="create_space_alias_hint">Space-Adressen</string>
|
||||
<string name="error_failed_to_join_room">Beim Versuch %s beizutreten, ist leider ein Fehler aufgetreten</string>
|
||||
<string name="room_upgrade_to_recommended_version">Zur empfohlenen Raumversion upgraden</string>
|
||||
<string name="joining_replacement_room">Ersatzraum betreten</string>
|
||||
<string name="command_description_upgrade_room">Raum zu neuer Version upgraden</string>
|
||||
<string name="settings_server_room_version_stable">stabil</string>
|
||||
<string name="settings_server_room_version_unstable">instabil</string>
|
||||
<string name="settings_server_room_versions">Raumversionen 👓</string>
|
||||
<string name="missing_permissions_title">Fehlende Berechtigungen</string>
|
||||
</resources>
|
|
@ -533,7 +533,7 @@
|
|||
<string name="e2e_re_request_encryption_key">Küsi oma muudest sessioonidest krüptimisvõtmed uuesti.</string>
|
||||
<string name="e2e_re_request_encryption_key_sent">Võtmete jagamise päring on saadetud.</string>
|
||||
<string name="e2e_re_request_encryption_key_dialog_title">Päring on saadetud</string>
|
||||
<string name="template_e2e_re_request_encryption_key_dialog_content">Palun käivita ${app_name} mõnes muus seadmes, mis suudab neid sõnumeid dekrüptoda ja seega saata krüptovõtmeid siia sessiooni.</string>
|
||||
<string name="template_e2e_re_request_encryption_key_dialog_content">Palun käivita ${app_name} mõnes muus seadmes, mis suudab neid sõnumeid dekrüptida ja seega saata krüptovõtmeid siia sessiooni.</string>
|
||||
<string name="read_receipts_list">Lugemisteatiste loend</string>
|
||||
<string name="groups_list">Gruppide loend</string>
|
||||
<plurals name="membership_changes">
|
||||
|
@ -1197,7 +1197,7 @@
|
|||
<string name="room_resend_unsent_messages">Saada saatmata sõnumid uuesti</string>
|
||||
<string name="room_delete_unsent_messages">Kustuta saatmata sõnumid</string>
|
||||
<string name="room_message_file_not_found">Faili ei leidunud</string>
|
||||
<string name="room_do_not_have_permission_to_post">Sul ei ole õigusi siia jututuppa kirjutamiseks</string>
|
||||
<string name="room_do_not_have_permission_to_post">Sul ei ole õigusi siia jututuppa kirjutamiseks.</string>
|
||||
<plurals name="room_new_messages_notification">
|
||||
<item quantity="one">%d uus sõnum</item>
|
||||
<item quantity="other">%d uut sõnumit</item>
|
||||
|
@ -1608,7 +1608,7 @@
|
|||
<string name="deactivate_account_submit">Deaktiveeri konto</string>
|
||||
<string name="error_empty_field_enter_user_name">Palun sisesta kasutajanimi.</string>
|
||||
<string name="error_empty_field_your_password">Palun sisesta oma salasõna.</string>
|
||||
<string name="room_tombstone_versioned_description">See jututuba on asendatud teise jututoaga ning ei ole enam kasutusel</string>
|
||||
<string name="room_tombstone_versioned_description">See jututuba on asendatud teise jututoaga ning ei ole enam kasutusel.</string>
|
||||
<string name="room_tombstone_continuation_link">Vestlus jätkub siin</string>
|
||||
<string name="room_tombstone_continuation_description">See jututuba on järg varasemale vestlusele</string>
|
||||
<string name="room_tombstone_predecessor_link">Vanemate sõnumite nägemiseks klõpsi siin</string>
|
||||
|
@ -2815,4 +2815,29 @@
|
|||
<string name="create_space_alias_hint">Kogukonnakeskuse aadressid</string>
|
||||
<string name="space_settings_alias_subtitle">Selle kogukonnakeskuse hallatud ja nähtavad aadressid.</string>
|
||||
<string name="space_settings_alias_title">Kogukonnakeskuse aadressid</string>
|
||||
<string name="settings_server_room_version_unstable">ebapüsiv</string>
|
||||
<string name="settings_server_room_version_stable">stabiilne</string>
|
||||
<string name="room_upgrade_to_recommended_version">Uuenda see jututoa versioon soovitatud versioonini</string>
|
||||
<string name="room_using_unstable_room_version">Selle jututoa versioon on %s ning see koduserver on tema märkinud ebastabiilseks.</string>
|
||||
<string name="upgrade_room_no_power_to_manage">Jututoa versiooni uuendamiseks on sul vaja õigusi</string>
|
||||
<string name="upgrade_room_update_parent_space">Uuenda automaatselt ka kogukonnakeskust, kus jututuba osaleb</string>
|
||||
<string name="upgrade_room_auto_invite">Kutsu automaatselt kasutajaid</string>
|
||||
<string name="upgrade_public_room_from_to">Sa oled uuendamas jututoa versiooni: %s -> %s.</string>
|
||||
<string name="upgrade_room_warning">Jututoa versiooni uuendamine on erandlik tegevus ning soovitame seda vaid siis kui ta on vigade tõttu raskestikasutatav, tal puuduvad uued funktsionaalsused või temas leidub turvavigu.
|
||||
\nTavaliselt versiooniuuendus mõjutab vaid seda viisi kuidas andmeid töödeldakse serveris.</string>
|
||||
<string name="upgrade_private_room">Uuenda omavaheline jututuba</string>
|
||||
<string name="upgrade_public_room">Uuenda avalik jututuba</string>
|
||||
<string name="upgrade">Uuenda</string>
|
||||
<string name="it_may_take_some_time">Palun oota rahulikult. Selleks võib kuluda natuke aega.</string>
|
||||
<string name="joining_replacement_room">Liitu asenduseks mõeldud jututoaga</string>
|
||||
<string name="command_description_upgrade_room">Uuendab jututoa uue versioonini</string>
|
||||
<string name="settings_server_default_room_version">Vaikimisi versioon</string>
|
||||
<string name="settings_server_room_versions">Jututubade versioonid 👓</string>
|
||||
<string name="verification_scan_self_emoji_subtitle">Selle asemel verifitseeri võrreldes emoji\'sid</string>
|
||||
<string name="verification_scan_with_this_device">Skaneeri selle seadmega</string>
|
||||
<string name="verification_scan_self_notice">Skaneeri koodi oma teise seadmega või vaheta pooli ning skaneeri selle seadmega</string>
|
||||
<string name="hs_client_url">Koduserveri API aadress</string>
|
||||
<string name="missing_permissions_title">Õigused on puudu</string>
|
||||
<string name="denied_permission_camera">Selle tegevuse jaoks palun luba seadistustes sellele rakendusele kaamera kasutamine.</string>
|
||||
<string name="denied_permission_generic">Selle tegevuse jaoks puuduvad sul õigused. Palun jaga vajalikud õigused süsteemi seadistustest.</string>
|
||||
</resources>
|
|
@ -223,7 +223,7 @@
|
|||
<string name="notification_listening_for_events">در حال گوش دادن به رویدادها</string>
|
||||
<string name="title_activity_home">پیامها</string>
|
||||
<string name="title_activity_room">اتاق</string>
|
||||
<string name="title_activity_settings">ساماندهی</string>
|
||||
<string name="title_activity_settings">تنظیمات</string>
|
||||
<string name="title_activity_member_details">جزئیات اعضا</string>
|
||||
<string name="title_activity_bug_report">گزارش اشکال</string>
|
||||
<string name="loading">در حال بارگذاری…</string>
|
||||
|
@ -333,7 +333,7 @@
|
|||
<string name="actions">کنشها</string>
|
||||
<string name="dialog_title_confirmation">تایید</string>
|
||||
<string name="dialog_title_error">خطا</string>
|
||||
<string name="bottom_action_favourites">پسندیدهها</string>
|
||||
<string name="bottom_action_favourites">محبوبها</string>
|
||||
<string name="bottom_action_people">افراد</string>
|
||||
<string name="bottom_action_groups">انجمنها</string>
|
||||
<string name="home_filter_placeholder_home">پالایش نامهای اتاق</string>
|
||||
|
@ -355,7 +355,7 @@
|
|||
<string name="send_bug_report_include_logs">ارسال رخدادنگارها</string>
|
||||
<string name="send_bug_report_include_crash_logs">ارسال رخدادنگارهای خطا</string>
|
||||
<string name="send_bug_report_include_screenshot">ارسال تصویر صفحه</string>
|
||||
<string name="send_bug_report_description">لطفاً اشکال پیش آمده را توصیف کنید. شما چه کاری انجام دادید؟ انتظار داشتید چه اتفاقی بیفتد؟ چه اتفاقی رخ داد؟</string>
|
||||
<string name="send_bug_report_description">لطفاً اشکال را شرح دهید. چهکار کردید؟ انتظار داشتید چه شود؟ چه شد؟</string>
|
||||
<string name="send_bug_report_description_in_english">ترجیحاً توضیحات را به زبان انگلیسی بنویسید.</string>
|
||||
<string name="send_bug_report_placeholder">مشکل خود را اینجا شرح دهید</string>
|
||||
<string name="send_bug_report_logs_description">برای کمک به اشکالیابی، رخدادنگارهای مربوط به این دستگاه به همراه گزارش اشکال ارسال خواهند شد. البته گزارش اشکال شما و پیوستهای آن به صورت عمومی منتشر نمیشوند. میتوانید با برداشتن علامت گزینههای زیر، اطلاعات کمتری را ارسال نمایید:</string>
|
||||
|
@ -394,11 +394,11 @@
|
|||
<string name="settings_call_category">تماسها</string>
|
||||
<string name="call">تماس</string>
|
||||
<string name="media_slider_saved">ذخیره شد</string>
|
||||
<string name="yes">بَلِہ</string>
|
||||
<string name="no">نَه</string>
|
||||
<string name="yes">بله</string>
|
||||
<string name="no">نه</string>
|
||||
<string name="media_slider_saved_message">در دانلودها ذخیره شود؟</string>
|
||||
<string name="_continue">ادامه</string>
|
||||
<string name="remove">زُدودَن</string>
|
||||
<string name="remove">برداشتن</string>
|
||||
<string name="join">پیوستن</string>
|
||||
<string name="preview">پیشنمایش</string>
|
||||
<string name="reject">رد کردن</string>
|
||||
|
@ -449,12 +449,12 @@
|
|||
<string name="room_participants_header_admin_tools">ابزارهای مدیر</string>
|
||||
<string name="room_participants_header_call">تماس</string>
|
||||
<string name="room_participants_header_direct_chats">گپهای مستقیم</string>
|
||||
<string name="room_participants_header_devices">نِشَستھا</string>
|
||||
<string name="room_participants_header_devices">نشستها</string>
|
||||
<string name="room_participants_action_invite">دعوت</string>
|
||||
<string name="room_participants_action_leave">ترک این اتاق</string>
|
||||
<string name="room_participants_action_remove">حذف از این اتاق</string>
|
||||
<string name="stay">ماندن</string>
|
||||
<string name="redact">حذف</string>
|
||||
<string name="redact">برداشتن</string>
|
||||
<string name="download">بارگیری</string>
|
||||
<string name="ongoing_conference_call">کنفرانس در حال برگذاری است.
|
||||
\nبه صورت %1$s یا %2$s به آن بپیوندید</string>
|
||||
|
@ -466,7 +466,7 @@
|
|||
<string name="done">انجام شد</string>
|
||||
<string name="abort">انصراف</string>
|
||||
<string name="ignore">نادیدهگرفتن</string>
|
||||
<string name="action_sign_out_confirmation_simple">آیا میخواهید از حساب کاربری خود خارج شوید؟</string>
|
||||
<string name="action_sign_out_confirmation_simple">مطمئنید که میخواهید از حسابتان خارج شوید؟</string>
|
||||
<string name="action_mark_room_read">علامتگذاری به عنوان خوانده شده</string>
|
||||
<string name="auth_login_sso">ورود با سامانههای احراز هویت مرکزی</string>
|
||||
<string name="notification_sync_init">در حال راهاندازی سرویس</string>
|
||||
|
@ -478,8 +478,8 @@
|
|||
<string name="title_activity_keys_backup_setup">پشتیبانگیری کلید</string>
|
||||
<string name="title_activity_keys_backup_restore">استفاده از پشتیبان کلید</string>
|
||||
<string name="keys_backup_is_not_finished_please_wait">پشتیبانگیری از کلید هنوز به پایان نرسیده است، لطفاً شکیبا باشید…</string>
|
||||
<string name="sign_out_bottom_sheet_warning_no_backup">در صورتی که اکنون از حساب خود خارج شوید، پیامهای رمزنگاری شده خود را از دست خواهید داد</string>
|
||||
<string name="sign_out_bottom_sheet_warning_backing_up">پشتیبانگیری کلید در جریان است. در صورتی که اکنون از حساب خود خارج شوید، پیامهای رمزنگاری شده خود را از دست خواهید داد.</string>
|
||||
<string name="sign_out_bottom_sheet_warning_no_backup">اگر اکنون از حسابتان خارج شوید، پیامهای رمزنگاشتهتان را از دست خواهید داد</string>
|
||||
<string name="sign_out_bottom_sheet_warning_backing_up">پشتیبانگیری کلید در جریان است. اگر اکنون از حسابتان خارج شوید، پیامهای رمزنگاشتهتان را از دست خواهید داد.</string>
|
||||
<string name="sign_out_bottom_sheet_warning_backup_not_active">برای از دست ندادن دسترسی به پیامهای رمزنگاری شده، باید پشتیبان کلید امن روی تمام نشستهایتان فعّال باشد.</string>
|
||||
<string name="sign_out_bottom_sheet_dont_want_secure_messages">پیامهای رمزنگاری شده خود را نمیخواهم</string>
|
||||
<string name="sign_out_bottom_sheet_backing_up_keys">در حال پشتیبانگیری از کلیدها…</string>
|
||||
|
@ -545,7 +545,7 @@
|
|||
<string name="settings_add_3pid_confirm_password_title">تأیید گذرواژهتان</string>
|
||||
<string name="template_settings_add_3pid_flow_not_supported">نمیتوانید با المنت همراه، این کار را بکنید</string>
|
||||
<string name="settings_add_3pid_authentication_needed">نیاز به تأیید هویت است</string>
|
||||
<string name="settings_notification_advanced">ساماندھیِ پیشرَفتِہیِ آگَہداد</string>
|
||||
<string name="settings_notification_advanced">تنظمیات پیشرفتهٔ آگاهی</string>
|
||||
<string name="settings_notification_troubleshoot">آگاهیهای رفعاشکال</string>
|
||||
<string name="settings_troubleshoot_test_system_settings_success">آگاهیها در تنظیمات سامانه به کار افتادهاند.</string>
|
||||
<string name="settings_troubleshoot_test_system_settings_failed">آگاهیّا در تنظیمات سامانه از کار افتادهاند.
|
||||
|
@ -775,9 +775,9 @@
|
|||
<string name="auth_reset_password_missing_password">گذرواژه جدیدی باید وارد شود.</string>
|
||||
<string name="auth_reset_password_email_validation_message">رایانامهای به %s فرستاده شد. هنگامی که پیوند داخلش را دنبال کردید، پایین را کلیک کنید.</string>
|
||||
<string name="auth_reset_password_error_unauthorized">شکست در تأیید نشانی رایانامه: مطمئن شوید که پیوند درون رایانامه را کلیک کردهاید</string>
|
||||
<string name="auth_reset_password_success_message">گذرواژه بازنشانی شد.
|
||||
<string name="auth_reset_password_success_message">گذرواژهتان بازنشانی شد.
|
||||
\n
|
||||
\nشما از همه نشستها خارج شدید و دیگر آگاهی ها را دریافت نخواهید کرد. برای فعالسازی دوباره آگاهیها، در هر دستگاه دوباره وارد شوید.</string>
|
||||
\nاز تمامی نشستها خارج شدید و دیگر آگاهیها را دریافت نخواهید کرد. برای به کار انداختن دوبارهٔ آگاهیها، دوباره در هر دستگاه وارد شوید.</string>
|
||||
<string name="auth_accept_policies">لطفاً سیاستهای این کارساز خانگی را بررسی کرده و بپذیرید:</string>
|
||||
<string name="login_error_must_start_http">نشانی باید با http[s]:// آغاز شود</string>
|
||||
<string name="login_error_registration_network_error">نمیتوان ثبتنام کرد: خطای شبکه</string>
|
||||
|
@ -802,10 +802,10 @@
|
|||
<string name="template_permissions_rationale_msg_camera">المنت برای گرفتن عکس و تماسهای ویدیویی نیاز به اجازه دارد.</string>
|
||||
<string name="open_chat_header">بازکردن سرتیتر</string>
|
||||
<string name="room_sync_in_progress">در حال همگامسازی…</string>
|
||||
<string name="room_jump_to_first_unread">پریدن به نخستین پیام خوانده نشده.</string>
|
||||
<string name="room_jump_to_first_unread">پرش به ناخوانده</string>
|
||||
<string name="room_preview_invitation_format">شما برای پیوستن به این اتاق توسط %s دعوت شدید</string>
|
||||
<string name="room_preview_try_join_an_unknown_room_default">یک اتاق</string>
|
||||
<string name="room_creation_title">گَپِ نو</string>
|
||||
<string name="room_creation_title">گپ جدید</string>
|
||||
<string name="room_creation_add_member">افزودن عضو</string>
|
||||
<plurals name="room_header_active_members_count">
|
||||
<item quantity="one">%d عضو فعّال</item>
|
||||
|
@ -852,7 +852,7 @@
|
|||
<string name="pause_video">توقف</string>
|
||||
<string name="action_copy">رونوشت</string>
|
||||
<string name="dialog_title_success">موفقیت</string>
|
||||
<string name="bottom_action_notification">آگَہداد</string>
|
||||
<string name="bottom_action_notification">آگاهیها</string>
|
||||
<string name="dismiss">خاتمه</string>
|
||||
<string name="no_permissions_to_start_conf_call">اجازهٔ شروع تماس کنفرانسی در این اتاق را ندارید</string>
|
||||
<string name="no_permissions_to_start_webrtc_call">اجازهٔ شروع تماس در این اتاق را ندارید</string>
|
||||
|
@ -913,10 +913,10 @@
|
|||
<string name="people_search_invite_by_id_dialog_title">دعوت کاربر با شناسه</string>
|
||||
<string name="people_search_invite_by_id_dialog_description">لطفاً یک یا چند نشانی رایانامه یا شناسهٔ ماتریکس را وارد کنید</string>
|
||||
<string name="people_search_invite_by_id_dialog_hint">رایانامه یا شناسهٔ ماتریکس</string>
|
||||
<string name="room_menu_search">جُستُجو</string>
|
||||
<string name="room_one_user_is_typing">%s دارد مینِویسَد…</string>
|
||||
<string name="room_two_users_are_typing">%1$s و %2$s دارَند مینِویسَند…</string>
|
||||
<string name="room_many_users_are_typing">%1$s و %2$s و دیگَران دارَند مینِویسَند…</string>
|
||||
<string name="room_menu_search">جستوجو</string>
|
||||
<string name="room_one_user_is_typing">%s دارد مینویسد…</string>
|
||||
<string name="room_two_users_are_typing">%1$s و %2$s دارند مینویسند…</string>
|
||||
<string name="room_many_users_are_typing">%1$s و %2$s و دیگران دارند مینویسند…</string>
|
||||
<string name="room_message_placeholder_encrypted">فرستادن پیام رمزشده…</string>
|
||||
<string name="room_message_placeholder_not_encrypted">فرستادن پیام (رمز نشده)…</string>
|
||||
<string name="room_offline_notification">اتّصال به کارساز از دست رفت.</string>
|
||||
|
@ -925,7 +925,7 @@
|
|||
<string name="room_resend_unsent_messages">بازفرستادن پیامهای فرستادهنشده</string>
|
||||
<string name="room_delete_unsent_messages">حذف پیامهای فرستادهنشده</string>
|
||||
<string name="room_message_file_not_found">پرونده پیدا نشد</string>
|
||||
<string name="room_do_not_have_permission_to_post">اجازهٔ فرستادن در این اتاق را ندارید</string>
|
||||
<string name="room_do_not_have_permission_to_post">اجازهٔ فرستادن به این اتاق را ندارید.</string>
|
||||
<plurals name="room_new_messages_notification">
|
||||
<item quantity="one">%d پیام جدید</item>
|
||||
<item quantity="other">%d پیام جدید</item>
|
||||
|
@ -1019,7 +1019,7 @@
|
|||
\nمدیرهای یکپارچگی، دادههای پیکربندی را دریافت کرده و میتوانند از طرف شما ابزارکها را تغییر داده، دعوتهای اتاق فرستاده و سطوح قدرت را تنظیم کنند.</string>
|
||||
<string name="settings_always_show_timestamps">نمایش برچسب زمانی برای تمامی پیامها</string>
|
||||
<string name="settings_12_24_timestamps">نمایش برچسبهای زمانی در قالب ۱۲ساعته</string>
|
||||
<string name="settings_show_room_member_state_events_summary">شامل رویدادهای دعوت/پیوستن/خروج/اخراج/تحریم و تغییرهای آواتار/نام نمایشی.</string>
|
||||
<string name="settings_show_room_member_state_events_summary">شامل رویدادهای دعوت/پیوستن/ترک/اخراج/تحریم و تغییرهای چهرک/نام نمایشی.</string>
|
||||
<string name="settings_secure_backup_section_title">پشتیبان امن</string>
|
||||
<string name="settings_secure_backup_manage">مدیریت</string>
|
||||
<string name="settings_secure_backup_setup">برپایی پشتیبان امن</string>
|
||||
|
@ -1275,7 +1275,7 @@
|
|||
<string name="attachment_type_contact">مخاطب</string>
|
||||
<string name="attachment_type_camera">دوربین</string>
|
||||
<string name="attachment_type_audio">صدا</string>
|
||||
<string name="attachment_type_gallery">گالری</string>
|
||||
<string name="attachment_type_gallery">جُنگ</string>
|
||||
<string name="attachment_type_sticker">برچسب</string>
|
||||
<string name="uploads_media_title">رسانه</string>
|
||||
<string name="uploads_media_no_result">هیچ رسانهای در این اتاق نیست</string>
|
||||
|
@ -1732,11 +1732,11 @@
|
|||
<string name="settings_troubleshoot_test_token_registration_title">ثبت توکن</string>
|
||||
<string name="settings_troubleshoot_test_fcm_failed_account_missing_quick_fix">افزودن حساب کاربری</string>
|
||||
<string name="template_settings_troubleshoot_test_fcm_failed_account_missing">[%1$s]
|
||||
\nاین خطا از کنترل المنت خارج است. هیچ حساب Googleای روی تلفن وجود ندارد. لطفاً مدیریت حساب را در تنظیمات گوشی باز کرده و یک حساب Google اضافه کنید.</string>
|
||||
\nاین خطا خارج از مهار ${app_name} است. هیچ حساب گوگلی روی تلفن نیست. لطفاً مدیر حساب را گشوده و حساب گوگلی بیفزایید.</string>
|
||||
<string name="template_settings_troubleshoot_test_fcm_failed_service_not_available">[%1$s]
|
||||
\nاین خطا از کنترل المنت خارج است و به چند دلیل ممکن است رخ داده باشد. امکان دارد در صورت تلاش مجدد مشکل رفع شود، همچنین میتوانید بررسی کنید که سرویس Google Play در استفاده از اینترنت در تنظیمات گوشی محدودیتی نداشته باشد یا ساعت دستگاه شما درست باشد. همچنین ممکن است به علت استفاده از ROM سفارشیشده این خطا رخ داده باشد.</string>
|
||||
\nاین خطا خارج از مهار ${app_name} است و ممکن است به دلایل مختلفی رخ داده باشد. ممکن است اگر بعداً دوباره تلاش کنید، کار کند. میتوانید بررسی کنید که مصرف دادهٔ خدمات پلی گوگل در تنظیمات سامانه محدود نشده باشد یا ساعت افزارهتان درست باشد. همچنین ممکن است روی رامهای سفارشی اتفاق بیفتد.</string>
|
||||
<string name="template_settings_troubleshoot_test_fcm_failed_too_many_registration">[%1$s]
|
||||
\nاین خطا از کنترل المنت خارج است و طبق گفته گوگل ، این خطا نشان می دهد که دستگاه بیش از حد مجاز، برنامه های ثبت شده در FCM دارد. این خطا فقط در مواردی رخ می دهد که تعداد زیادی برنامه وجود دارد، بنابراین نباید بر کاربر عادی رخ دهد.</string>
|
||||
\nاین خطا، خارج از مهار ${app_name} است و طبق گفتهٔ گوگل، نشانگر ثبت بیشاز حد کارهها در FCM است. خطا فقط در مواردی که تعداد خیلی زیادی کاره وجود داشته باشد رخ میدهد، پس کاربران معمولی نباید تحت تأثیر قرار گیرند.</string>
|
||||
<string name="settings_troubleshoot_test_fcm_failed">بازیابی توکن FCM با مشکل مواجه شد:
|
||||
\n%1$s</string>
|
||||
<string name="settings_troubleshoot_test_fcm_success">توکن FCM با موفقیت بازیابی شد:
|
||||
|
@ -1845,9 +1845,9 @@
|
|||
<string name="bootstrap_progress_compute_curve_key">محاسبهی کلید curve</string>
|
||||
<string name="auth_invalid_login_param_space_in_password">نامکاربری یا گذواژهی اشتباه. ابتدا یا انتهای گذرواژهی وارد شده کاراکتر space قرار دارد، لطفا بررسی کنید.</string>
|
||||
<string name="command_description_plain">پیامها را بدون در نظرگرفتن markdown ارسال کن</string>
|
||||
<string name="bootstrap_cancel_text">اگر اکنون لغو کنید و از حساب خود خارج شوید، دسترسی به پیامهای رمزشده را از دست میدهید.
|
||||
<string name="bootstrap_cancel_text">اگر اکنون لغو کنید، ممکن است در صورت قطع دسترسی به ورودهایتان، دادهها و پیامهای رمزنگاشته را از دست بدهد.
|
||||
\n
|
||||
\nالبته شما همچنان قادر هستید که پشتیبان امن و کلیدهای آن را در تنظیمات، مدیریت کنید.</string>
|
||||
\nهمچنین میتوانید در تنظیمات، پشتیبان امن برپا کرده و کلیدهایتان را مدیریت کنید.</string>
|
||||
<string name="bootstrap_skip_text_no_gen_key">تنظیم کلید امنیتی به شما اماکن امنکردن پیامها و دسترسی به پیامهای رمزشده را میدهد.</string>
|
||||
<string name="bootstrap_skip_text">تنظیم کلید امنیتی به شما اجازه دسترسی به پیامهای رمز شده را میدهد.
|
||||
\n
|
||||
|
@ -1939,7 +1939,7 @@
|
|||
\n
|
||||
\nاگر کارتان با این افزاره تمام شده یا میخواهید به حساب دیگری وارد شوید، پاکشان کنید.</string>
|
||||
<string name="soft_logout_signin_e2e_warning_notice">برای بازیابی کلیدهای رمزگذاری ذخیره شده در این دستگاه، وارد حساب خود شوید. شما برای خواندن همه پیامهای رمزشدهی خود در هر دستگاهی به این کلیدها نیاز دارید.</string>
|
||||
<string name="soft_logout_signin_notice">ادمین سرور (%1$s) شما را از حسابتان خارج کردهاست %2$s (%3$s).</string>
|
||||
<string name="soft_logout_signin_notice">مدیر کارساز خانگیتان (%1$s) شما (%2$s) را از حسابتان (%3$s) خارج کرد.</string>
|
||||
<string name="signed_out_notice">این می تواند به دلایل مختلف باشد:
|
||||
\n
|
||||
\n• شما گذرواژه خود را در نشست دیگری تغییر دادهاید.
|
||||
|
@ -2034,11 +2034,11 @@
|
|||
<string name="content_reported_content">این محتوا گزارش شدهاست.
|
||||
\n
|
||||
\nاگر نمیخواهید محتوای بیشتری از این کاربر مشاهده کنید، می توانید او را نادیده بگیرید تا پیامهای او را مشاهده نکنید.</string>
|
||||
<string name="error_handling_incoming_share">امکان به اشتراکگذاری این محتوا وجود ندارد</string>
|
||||
<string name="error_handling_incoming_share">نتوانست همرسانی داده را مدیریت کند</string>
|
||||
<string name="rotate_and_crop_screen_title">چرخش و برش</string>
|
||||
<string name="attachment_type_dialog_title">اضافه کردن تصویر از</string>
|
||||
<string name="attachment_type_dialog_title">افزودن تصویر از</string>
|
||||
<string name="error_attachment">هنگام دریافت پیوست خطایی رخ داد.</string>
|
||||
<string name="error_file_too_big">پرونده \'%1$s\' (%2$s) برای آپلود بسیار بزرگ است. محدودیت آپلود %3$s است.</string>
|
||||
<string name="error_file_too_big">پروندهٔ «%1$s» (%2$s) برای بارگذاری بسیار بزرگ است. کران %3$s است.</string>
|
||||
<plurals name="fallback_users_read">
|
||||
<item quantity="one">%d کاربر خواند</item>
|
||||
<item quantity="other">%d کاربر خواندند</item>
|
||||
|
@ -2098,7 +2098,7 @@
|
|||
<string name="create_room_topic_section">توضیح در مورد اتاق (اختیاری)</string>
|
||||
<string name="create_room_name_section">نام اتاق</string>
|
||||
<string name="event_redacted">پیام پاک شد</string>
|
||||
<string name="error_user_already_logged_in">به نظر میرسد تلاش میکنید تا به کارساز خانگی دیگری وصل شوید. میخواهید خارج شوید؟</string>
|
||||
<string name="error_user_already_logged_in">به نظر در تلاش برای وصل شدن به کارساز خانگی دیگری هستید. میخواهید خارج شوید؟</string>
|
||||
<string name="identity_server_not_defined_for_password_reset">برای بازنشانی گذرواژهی خود نیاز به پیکربندی سرور هویتسنجی دارید.</string>
|
||||
<string name="identity_server_not_defined">شما از سرور هویتسنجی استفاده نمیکنید</string>
|
||||
<string name="sas_error_unknown">خطای نامشخص</string>
|
||||
|
@ -2138,7 +2138,7 @@
|
|||
<string name="sas_verify_start_button_title">شروع فرآیند تایید کردن</string>
|
||||
<string name="sas_security_advise">برای امنیت بیشتر پیشنهاد میکنیم که این بخش را یا به صورت حضوری یا با استفاده از یک راهکار ارتباطی امن دیگر تکمیل کنید.</string>
|
||||
<string name="sas_verify_title">با مقایسهکردن یک رشتهی متنی کوتاه تایید کنید.</string>
|
||||
<string name="invalid_or_expired_credentials">شما به دلیل اطلاعات نادرست حساب کاربری یا انقضای نشست از حساب خارج شدید.</string>
|
||||
<string name="invalid_or_expired_credentials">به خاطر ورود منقضی یا نامعتبر، از حسابتان خارج شدید.</string>
|
||||
<string name="autodiscover_well_known_autofill_confirm">از پیکربندی استفاده کنید</string>
|
||||
<string name="template_autodiscover_well_known_autofill_dialog_message">المنت یک پیکربندی اختصاصی سرور برای دامنهی شناسهی کاربری شما \"%1$s\" تشخیص داده است:
|
||||
\n%2$s</string>
|
||||
|
@ -2196,7 +2196,7 @@
|
|||
<string name="keys_backup_restore_with_recovery_key">از کلید بازیابی برای رمزگشایی پیامهای رمزشدهی قبلی خود استفاده کنید</string>
|
||||
<string name="keys_backup_restore_with_passphrase_helper_with_link">کلید امنیتی خود را نمیدانید؟ شما میتوانید %s.</string>
|
||||
<string name="keys_backup_restore_with_passphrase">برای قفلگشایی تاریخچهٔ پیامهای رمزشدهتان از عبارت عبور بازیابیتان استفاده کنید</string>
|
||||
<string name="keys_backup_setup_skip_msg">اگر از دستگاه خارج شوید یا دستگاه خود را از دست دهید، ممکن است امکان دسترسی به پیام های خود را نداشته باشید.</string>
|
||||
<string name="keys_backup_setup_skip_msg">ممکن است در صورت خروج از حساب یا از دست دادن این افزاره، دسترسی به پیامهایتان را از دست بدهید.</string>
|
||||
<string name="keys_backup_setup_backup_started_message">کلیدهای رمزگذاری شما اکنون در پس زمینه در حال پشتیبانگیری بر روی سرور است. تهیه نسخهی پشتیبان اولیه ممکن است چند دقیقه طول بکشد.</string>
|
||||
<string name="keys_backup_setup_step3_generating_key_status">در حال تولید کلید پشتیبان با استفاده از کلید امنیتی، این ممکن است چند ثانیه زمان ببرد.</string>
|
||||
<string name="keys_backup_setup_override_backup_prompt_description">به نظر میرسد شما در یک نشست دیگر کلید پشتیبان تهیه کردهاید. آیا میخواهید آن را با موردی که ایجاد میکنید جایگزین کنید؟</string>
|
||||
|
@ -2244,13 +2244,13 @@
|
|||
<string name="resource_limit_soft_default">سرور از یکی از محدودیت های منابع خود فراتر رفته است <b>بنابراین برخی از کاربران نمی توانند وارد سیستم شوند</b>.</string>
|
||||
<string name="room_tombstone_continuation_description">این اتاق ادامهی گفتگوی دیگر است</string>
|
||||
<string name="room_tombstone_continuation_link">گفتگو در اینجا ادامه دارد</string>
|
||||
<string name="room_tombstone_versioned_description">این اتاق جایگزین شده و دیگر فعال نیست</string>
|
||||
<string name="room_tombstone_versioned_description">این اتاق جایگزین شده و دیگر فعّال نیست.</string>
|
||||
<string name="deactivate_account_delete_checkbox">لطفاً تمام پیامهای ارسال شدهی من را حدف کن (هشدار: این امر باعث می شود کاربران آینده مکالمات را به صورت ناقص ببینند)</string>
|
||||
<string name="deactivate_account_content">این کار باعث می شود حساب شما برای همیشه غیرقابل استفاده شود. شما قادر به ورود به سیستم نخواهید بود و هیچ کس نمی تواند شناسه کاربری مشابه را دوباره ثبت کند. این باعث می شود که حساب شما از همه اتاق هایی که در آن شرکت می کند خارج شود و جزئیات حساب شما از سرور هویت حذف می شود. <b>این اقدام برگشت ناپذیر است </b>.
|
||||
<string name="deactivate_account_content">این کار حسابتان را برای همیشه غیر قابل استفاده خواهد کرد. قادر به ورود نخواهید بود و هیچکس نخواهد توانست دوباره این شناسهٔ کاربری را ثبت کند. این کار موجب ترک حسابتان از تمامی اتاقها شده و جزییات حسابتان را از کارساز هویتتان برخواهد داشت. <b>این عمل بازگشتپذیر نیست</b>.
|
||||
\n
|
||||
\nغیرفعال کردن حساب شما <b> به طور پیش فرض باعث نمی شود پیام های ارسالی شما را حذف کنیم </b>. اگر می خواهید پیام های شما را فراموش کنیم ، لطفاً کادر زیر را علامت بزنید.
|
||||
\nغیرفعّالسازی حسابتان به صورت پیشگزیده <b>منجر به قراموشی پیامهایی که فرستادهاید نخواهد شد</b>. اگر میخواهید پیامهایتان را فراموش کنیم، تیک کادر زیر را بزنید.
|
||||
\n
|
||||
\nقابلیت مشاهده پیام در المنت مانند ایمیل است. حذف کردن پیامهای شما به این معنی است که پیامهایی که ارسال کردهاید به هیچ کاربر جدید یا ثبت نشدهای نمایش داده نمی شود، اما کاربران ثبتنام شده که از قبل به این پیام ها دسترسی داشتند همچنان به نسخه خود دسترسی خواهند داشت.</string>
|
||||
\nپدیداری پیام در ماتریکس، مانند رایانامه است. فراموشی پیامهایتان به معنی همرسانی نشدن آنها با کاربران جدید یا ثبتنشده است. کاربران ثبتشدهای که پیشتر به این پیامها دسترسی داشتهاند، همچنان به رونوشتشان دسترسی خواهند داشت.</string>
|
||||
<string name="dialog_user_consent_content">برای ادامهٔ استفاده از کارساز خانگی %1$s باید شرایط و ضوابط را خوانده و بپذیرید.</string>
|
||||
<string name="markdown_has_been_disabled">Markdown غیرفعال شده است.</string>
|
||||
<string name="markdown_has_been_enabled">Markdown فعال شده است.</string>
|
||||
|
@ -2441,7 +2441,7 @@
|
|||
<string name="permissions_denied_add_contact">دسترسی به مخاطبانتان را مجاز کنید.</string>
|
||||
<string name="permissions_denied_qr_code">برای پویش یک رمز QR نیاز است دسترسی به دوربین را مجاز کنید.</string>
|
||||
<string name="start_chatting">آغاز به گپ</string>
|
||||
<string name="settings_export_trail">خروجی گرفتن</string>
|
||||
<string name="settings_export_trail">برونریزی بازرسی</string>
|
||||
<string name="create_room_disable_federation_description">اگر اتاق فقط برای تعامل با افراد داخل سرور خانه شما میباشد، این قابلیت را فعال کنید. این تنظیم را بعدا نمیتوانید تغییر دهید.</string>
|
||||
<string name="identity_server_consent_dialog_content">آیا میخواهید جهت کشف مخاطبینی که می شناسید، داده های مخاطب خود را (شماره تلفن و ایمیل) به سرور هویتسنجی(%1$s) ارسال کنید؟
|
||||
\n
|
||||
|
@ -2528,9 +2528,9 @@
|
|||
<string name="command_snow">فرستادن پیام داده شده با بارش برف</string>
|
||||
<string name="default_message_emote_confetti">فرستادن کاغذ رنگی 🎉</string>
|
||||
<string name="default_message_emote_snow">فرستادن برف ❄️</string>
|
||||
<string name="authentication_error">احراز هویت انجام نشد</string>
|
||||
<string name="template_re_authentication_default_confirm_text">المنت برای انجام این عمل نیاز دارد که گذواژهی خود را وارد کنید.</string>
|
||||
<string name="re_authentication_activity_title">احراز هویت مجدد مورد نیاز است</string>
|
||||
<string name="authentication_error">تأیید هویت شکست خورد</string>
|
||||
<string name="template_re_authentication_default_confirm_text">${app_name} برای انجام این کار، نیاز به ورود گذواژهتان دارد.</string>
|
||||
<string name="re_authentication_activity_title">نیاز به تأیید هویت دوباره است</string>
|
||||
<string name="call_transfer_users_tab_title">کاربران</string>
|
||||
<string name="call_transfer_failure">هنگام انتقال تماس خطایی روی داد</string>
|
||||
<string name="call_transfer_title">انتقال</string>
|
||||
|
@ -2560,43 +2560,43 @@
|
|||
<string name="call_resume_action">از سرگیری</string>
|
||||
<string name="error_unauthorized">غیر مجاز، اطلاعات هویتسنجی موجود نمیباشد</string>
|
||||
<string name="action_return">بازگشت</string>
|
||||
<string name="dev_tools_event_content_hint">محتوای رخداد</string>
|
||||
<string name="dev_tools_success_state_event">رخداد وضعیتی ارسال شد!</string>
|
||||
<string name="dev_tools_success_event">رخداد ارسال شد!</string>
|
||||
<string name="dev_tools_error_malformed_event">رخداد معیوب</string>
|
||||
<string name="dev_tools_error_no_message_type">نوع پیام فراموش شدهاست</string>
|
||||
<string name="dev_tools_event_content_hint">محتوای رویداد</string>
|
||||
<string name="dev_tools_success_state_event">رویداد وضعیت فرستاده شد!</string>
|
||||
<string name="dev_tools_success_event">رویداد فرستاده شد!</string>
|
||||
<string name="dev_tools_error_malformed_event">رویداد بدشکل</string>
|
||||
<string name="dev_tools_error_no_message_type">بدون گونهٔ پیام</string>
|
||||
<string name="dev_tools_error_no_content">بدون محتوا</string>
|
||||
<string name="dev_tools_form_hint_event_content">محتوای رخداد</string>
|
||||
<string name="dev_tools_form_hint_event_content">محتوای رویداد</string>
|
||||
<string name="dev_tools_form_hint_state_key">کلید وضعیت</string>
|
||||
<string name="dev_tools_form_hint_type">نوع</string>
|
||||
<string name="dev_tools_send_custom_state_event">ارسال رخداد وضعیت سفارشی</string>
|
||||
<string name="dev_tools_form_hint_type">گونه</string>
|
||||
<string name="dev_tools_send_custom_state_event">فرستادن رویداد وضعیت سفارشی</string>
|
||||
<string name="dev_tools_edit_content">ویرایش محتوا</string>
|
||||
<string name="dev_tools_state_event">رخدادهای وضعیتی</string>
|
||||
<string name="dev_tools_send_state_event">ارسال رخداد وضعیتی</string>
|
||||
<string name="dev_tools_send_custom_event">ارسال رخداد سفارشی</string>
|
||||
<string name="dev_tools_state_event">رویدادهای وضعیت</string>
|
||||
<string name="dev_tools_send_state_event">فرستادن رویداد وضعیت</string>
|
||||
<string name="dev_tools_send_custom_event">فرستادن رویداد سفارشی</string>
|
||||
<string name="dev_tools_explore_room_state">کاوش وضعیت اتاق</string>
|
||||
<string name="dev_tools_menu_name">ابزارهای توسعه</string>
|
||||
<string name="a11y_view_read_receipts">مشاهده رسیدهای خواندهشده</string>
|
||||
<string name="a11y_rule_notify_off">خبر نده</string>
|
||||
<string name="a11y_rule_notify_silent">بدون صدا خبر بده</string>
|
||||
<string name="a11y_rule_notify_noisy">با صدا خبر بده</string>
|
||||
<string name="a11y_error_message_not_sent">پیام به دلیل رخداد خطا ارسال نشد</string>
|
||||
<string name="a11y_view_read_receipts">دیدن رسیدهای خواندن</string>
|
||||
<string name="a11y_rule_notify_off">بدون آگاهی</string>
|
||||
<string name="a11y_rule_notify_silent">آگاهی بدون صدا</string>
|
||||
<string name="a11y_rule_notify_noisy">آگاهی با صدا</string>
|
||||
<string name="a11y_error_message_not_sent">پیام به دلیل خطا فرستاده نشد</string>
|
||||
<string name="a11y_checked">تیکخورده</string>
|
||||
<string name="a11y_close_emoji_picker">بستن پنجره ی شکلکها</string>
|
||||
<string name="a11y_open_emoji_picker">بازکردن پنجرهی شکلکها</string>
|
||||
<string name="a11y_close_emoji_picker">بستن اموجیبردار</string>
|
||||
<string name="a11y_open_emoji_picker">گشودن اموجیبردار</string>
|
||||
<string name="a11y_trust_level_trusted">سطح اعتماد کامل</string>
|
||||
<string name="a11y_trust_level_warning">سطح اعتماد هشداری</string>
|
||||
<string name="a11y_trust_level_default">سطح اعتماد پیشفرض</string>
|
||||
<string name="a11y_selected">انتخابشده</string>
|
||||
<string name="a11y_image">عکس</string>
|
||||
<string name="a11y_video">ویدئو</string>
|
||||
<string name="a11y_unsent_draft">این اتاق حاوی پیشنویسهایی است که هنوز ارسال نشدهاند</string>
|
||||
<string name="a11y_error_some_message_not_sent">بعضی از پیامها هنوز ارسال نشدهاند</string>
|
||||
<string name="a11y_delete_avatar">حذف نمایه</string>
|
||||
<string name="a11y_change_avatar">تغییر نمایه</string>
|
||||
<string name="a11y_import_key_from_file">واردکردن کلید از فایل</string>
|
||||
<string name="a11y_open_widget">ابزارکهای باز</string>
|
||||
<string name="a11y_screenshot">اسکرینشات</string>
|
||||
<string name="a11y_trust_level_warning">سطح اعتماد هشدار</string>
|
||||
<string name="a11y_trust_level_default">سطح اعتماد پیشگزیده</string>
|
||||
<string name="a11y_selected">گزیده</string>
|
||||
<string name="a11y_image">تصویر</string>
|
||||
<string name="a11y_video">ویدیو</string>
|
||||
<string name="a11y_unsent_draft">این اتاق پیشنویسهایی نفرستاده دارد</string>
|
||||
<string name="a11y_error_some_message_not_sent">برخی پیامها ارسال نشدهاند</string>
|
||||
<string name="a11y_delete_avatar">حذف چهرک</string>
|
||||
<string name="a11y_change_avatar">تغییر چهرک</string>
|
||||
<string name="a11y_import_key_from_file">درونریزی کلید از پرونده</string>
|
||||
<string name="a11y_open_widget">گشودن ابزارکها</string>
|
||||
<string name="a11y_screenshot">نماگرفت</string>
|
||||
<string name="call_transfer_consult_first">ابتدا مشاوره بگیرید</string>
|
||||
<plurals name="entries">
|
||||
<item quantity="one">%d ورودی</item>
|
||||
|
@ -2661,26 +2661,26 @@
|
|||
<string name="notice_room_server_acl_set_allowed">تطبیق سرور %s اجازه داده شدهاست.</string>
|
||||
<string name="notice_room_server_acl_set_banned">تطبیق سرور %s ممنوع شدهاست.</string>
|
||||
<string name="notice_room_server_acl_set_title_by_you">شما ACL های سرور را برای این اتاق تنظیم کردید.</string>
|
||||
<string name="event_status_delete_all_failed_dialog_message">آیا مطمئن هستید که می خواهید همه پیام های ارسال نشده در این اتاق را حذف کنید؟</string>
|
||||
<string name="event_status_delete_all_failed_dialog_title">حذف پیامهای ارسال نشده</string>
|
||||
<string name="event_status_failed_messages_warning">پیام ارسال نشد</string>
|
||||
<string name="event_status_cancel_sending_dialog_message">آیا می خواهید ارسال پیام را لغو کنید؟</string>
|
||||
<string name="event_status_a11y_delete_all">حذف تمامی پیامهای ناموفق</string>
|
||||
<string name="event_status_a11y_failed">ناموفق</string>
|
||||
<string name="event_status_a11y_sent">ارسال شد</string>
|
||||
<string name="event_status_a11y_sending">در حال ارسال</string>
|
||||
<string name="event_status_delete_all_failed_dialog_message">مطمئنید که میخواهید تمام پیامهای فرستاده نشده در این اتاق را حذف کنید؟</string>
|
||||
<string name="event_status_delete_all_failed_dialog_title">حذف پیامهای فرستاده نشده</string>
|
||||
<string name="event_status_failed_messages_warning">فرستادن پیامها شکست خورد</string>
|
||||
<string name="event_status_cancel_sending_dialog_message">میخواهید فرستادن پیام را لغو کنید؟</string>
|
||||
<string name="event_status_a11y_delete_all">حذف تمام پیامهای شکستخورده</string>
|
||||
<string name="event_status_a11y_failed">شکست خورد</string>
|
||||
<string name="event_status_a11y_sent">فرستاده شد</string>
|
||||
<string name="event_status_a11y_sending">فرستادن</string>
|
||||
<string name="settings_room_directory_show_all_rooms_summary">نمایش همهی اتاقهای داخل فهرست.</string>
|
||||
<string name="settings_room_directory_show_all_rooms">نمایشها همهی اتاقها</string>
|
||||
<string name="settings_category_room_directory">فهرست اتاقها</string>
|
||||
<string name="event_status_sent_message">پَیام فِرِستاده شُد</string>
|
||||
<string name="labs_use_restricted_join_rule_desc">هشدار به پشتیبانی سرور و نسخه اتاق آزمایشی نیاز دارد</string>
|
||||
<string name="labs_use_restricted_join_rule">فضای کاری آزمایشی - اتاق محدود.</string>
|
||||
<string name="you_are_invited">شما دعوت شدهاید</string>
|
||||
<string name="spaces_beta_welcome_to_spaces_desc">اسپیس ها یک شیوه جدید برای دسته بندی اتاق ها و افراد هستند.</string>
|
||||
<string name="spaces_beta_welcome_to_spaces">به فضایهای کاری خوشآمدید!</string>
|
||||
<string name="space_add_existing_rooms">اتاقها و فضای کاری موجود را اضافه کنید</string>
|
||||
<string name="space_leave_prompt_msg">آیا مطمئن هستید که می خواهید فضای کاری را ترک کنید؟</string>
|
||||
<string name="leave_space">خروج از فضای کاری</string>
|
||||
<string name="event_status_sent_message">پیام فرستاده شد</string>
|
||||
<string name="labs_use_restricted_join_rule_desc">هشدار: نیاز به پشتیبانی کارساز و نگارش اتاق آزمایشی</string>
|
||||
<string name="labs_use_restricted_join_rule">فضای آزمایشی - اتاق محدود.</string>
|
||||
<string name="you_are_invited">دعوت شدهاید</string>
|
||||
<string name="spaces_beta_welcome_to_spaces_desc">فضاها شیوهای جدید برای گروهبندی اتاقها و افراد است.</string>
|
||||
<string name="spaces_beta_welcome_to_spaces">به فضاها خوش آمدید!</string>
|
||||
<string name="space_add_existing_rooms">افزودن فضا و اتاقهای موجود</string>
|
||||
<string name="space_leave_prompt_msg">مطمئنید که میخواهید فضا را ترک کنید؟</string>
|
||||
<string name="leave_space">ترک فضا</string>
|
||||
<string name="space_add_child_title">افزودن اتاق</string>
|
||||
<string name="space_explore_activity_title">کاوش در اتاقها</string>
|
||||
<plurals name="space_people_you_know">
|
||||
|
@ -2692,10 +2692,10 @@
|
|||
<string name="room_alias_preview_not_found">این نام مستعار در حال حاضر قابل دسترسی نیست.
|
||||
\nبعداً دوباره امتحان کنید، یا از مدیر اتاق بخواهید که دسترسی شما به آن را بررسی کند.</string>
|
||||
<string name="join_anyway">در هر صورت بپیوند</string>
|
||||
<string name="join_space">پیوستن به فضای کاری</string>
|
||||
<string name="create_space">ساخت فضای کاری</string>
|
||||
<string name="join_space">پیوستن به فضا</string>
|
||||
<string name="create_space">ایجاد فضا</string>
|
||||
<string name="skip_for_now">فعلا رد شوید</string>
|
||||
<string name="share_space_link_message">به فضای کاری %1$s من بپیوند.%2$s</string>
|
||||
<string name="share_space_link_message">به فضایم %1$s بپیوند.%2$s</string>
|
||||
<string name="invite_just_to_this_room_desc">آنها بخشی از %s نخواهند شد</string>
|
||||
<string name="invite_just_to_this_room">فقط به این اتاق</string>
|
||||
<string name="invite_to_space_with_name_desc">آنها قادر به کاوش در %s خواهند بود</string>
|
||||
|
@ -2705,9 +2705,9 @@
|
|||
<string name="invite_by_email">دعوت با ایمیل</string>
|
||||
<string name="invite_people_to_your_space_desc">در حال حاضر فقط شما هستید. %s با دیگران حتی بهتر خواهد بود.</string>
|
||||
<string name="invite_people_menu">دعوت افراد</string>
|
||||
<string name="invite_people_to_your_space">افراد را به فضای کاری خود دعوت کنید</string>
|
||||
<string name="invite_people_to_your_space">دعوت افراد به فضایتان</string>
|
||||
<string name="create_space_topic_hint">توضیحات</string>
|
||||
<string name="create_spaces_loading_message">در حال ساخت فضای کاری …</string>
|
||||
<string name="create_spaces_loading_message">ساختن فضا…</string>
|
||||
<string name="create_spaces_default_public_random_room_name">تصادفی</string>
|
||||
<string name="create_spaces_default_public_room_name">عمومی</string>
|
||||
<string name="create_spaces_room_private_header_desc">بیایید برای هر یک از آنها یک اتاق درست کنیم. بعداً می توانید موارد دیگر را اضافه کنید، از جمله موارد موجود.</string>
|
||||
|
@ -2717,30 +2717,30 @@
|
|||
<string name="create_space_error_empty_field_space_name">برای ادامه یک اسم برای آن بگذارید.</string>
|
||||
<string name="create_spaces_details_private_header">توضیحاتی را برای کمک به افراد در شناسایی آن اضافه کنید. همواره می توانید این توضیحات را تغییر دهید.</string>
|
||||
<string name="create_spaces_details_public_header">توضیحاتی را برای مشخص کردن آن اضافه کنید. همواره می توانید این توضیحات را تغییر دهید.</string>
|
||||
<string name="activity_create_space_title">ساخت فضای کاری</string>
|
||||
<string name="space_type_private_desc">فقط با دعوت، مناسب برای خودتان و یا تیمها</string>
|
||||
<string name="activity_create_space_title">ایجاد یک فضا</string>
|
||||
<string name="space_type_private_desc">فقط با دعوت. بهترین برای خودتان یا گروهها</string>
|
||||
<string name="space_type_private">خصوصی</string>
|
||||
<string name="space_type_public_desc">برای همه آزاد است، بهترین حالت برای انجمنها میباشد</string>
|
||||
<string name="space_type_public_desc">آزاد برای همه. بهترین برای اجتماعها</string>
|
||||
<string name="space_type_public">عمومی</string>
|
||||
<string name="create_spaces_private_teammates">یک فضای کاری خصوصی برای شما و همکارانتان</string>
|
||||
<string name="create_spaces_me_and_teammates">من و همکارانم</string>
|
||||
<string name="create_spaces_organise_rooms">یک فضای کاری خصوصی برای نظم بخشیدن به اتاقهای شما</string>
|
||||
<string name="create_spaces_just_me">فقط خودم</string>
|
||||
<string name="create_spaces_make_sure_access">اطمینان حاصل کنید که افراد مناسب به %s دسترسی دارند. شما میتوانید این را بعدا تغییر دهید.</string>
|
||||
<string name="command_description_create_space">ساخت فضای کاری</string>
|
||||
<string name="room_settings_room_access_restricted_description">هر کسی در فضای کاری این اتاق وجود داشته باشد، میتواند اتاق را پیدا کرده و به آن بپیوندد. فقط مدیران این اتاق می توانند آن را به یک فضای کاری اضافه کنند.</string>
|
||||
<string name="room_settings_room_access_restricted_title">فضاهای کاری</string>
|
||||
<string name="spaces_header">فضاهای کاری</string>
|
||||
<string name="create_spaces_private_teammates">فضایی خصوصی برای شما و همگروهیهایتان</string>
|
||||
<string name="create_spaces_me_and_teammates">من و همگروهیهایم</string>
|
||||
<string name="create_spaces_organise_rooms">یک فضای خصوصی برای نظم بخشی به اتاقهایتان</string>
|
||||
<string name="create_spaces_just_me">فقط من</string>
|
||||
<string name="create_spaces_make_sure_access">مطمئن شوید که افراد درست به %s دسترسی دارند. میتوانید بعدها این را تغییر دهید.</string>
|
||||
<string name="command_description_create_space">ساخت یک فضا</string>
|
||||
<string name="room_settings_room_access_restricted_description">هر کسی در فضای این اتاق، میتواند اتاق را یافته و به آن بپیوندد. فقط مدیران این اتاق می توانند آن را به فضایی بیفزایند.</string>
|
||||
<string name="room_settings_room_access_restricted_title">فضاها</string>
|
||||
<string name="spaces_header">فضاها</string>
|
||||
<string name="create_spaces_who_are_you_working_with">با چه کسانی کار میکنی؟</string>
|
||||
<string name="create_spaces_join_info_help">برای پیوستن به یک فضای کاری موجود، نیاز به دعوت دارید.</string>
|
||||
<string name="create_spaces_you_can_change_later">میتوانید این را بعدا تغییر دهید</string>
|
||||
<string name="create_spaces_choose_type_label">می خواهید چه نوع فضای کاریای ایجاد کنید؟</string>
|
||||
<string name="create_spaces_type_header">فضاهای کاری راهی جدید برای ایجاد دستههایی از اتاقها و افراد است</string>
|
||||
<string name="your_private_space">فضای کاری خصوصی شما</string>
|
||||
<string name="your_public_space">فضای کاری عمومی شما</string>
|
||||
<string name="add_space">افزودن فضای کاری</string>
|
||||
<string name="command_description_leave_room">ترک اتاق با شناسه داده شده (یا اگر اتاق خالی باشد اتاق فعلی)</string>
|
||||
<string name="command_description_join_space">پیوستن به فضای کاری با شناسه</string>
|
||||
<string name="create_spaces_join_info_help">برای پیوستن به یک فضای موجود، نیاز به دعوت دارید.</string>
|
||||
<string name="create_spaces_you_can_change_later">میتوانید بعداً این را تغییر دهید</string>
|
||||
<string name="create_spaces_choose_type_label">میخواهید چه نوع فضایی ایجاد کنید؟</string>
|
||||
<string name="create_spaces_type_header">فضاها راهی جدید برای گروهبندی اتاقها و افراد است</string>
|
||||
<string name="your_private_space">فضای خصوصیتان</string>
|
||||
<string name="your_public_space">فضا عمومیتان</string>
|
||||
<string name="add_space">افزودن فضا</string>
|
||||
<string name="command_description_leave_room">ترک اتاق با شناسهٔ دادهشده (در صورت null بودن، اتاق کنونی)</string>
|
||||
<string name="command_description_join_space">پیوستن به فضا با شناسهٔ دادهشده</string>
|
||||
<string name="a11y_unchecked">علامت زده نشده</string>
|
||||
<string name="search_hint_room_name">جستجو با نام اتاق</string>
|
||||
<string name="room_settings_room_access_public_description">هرکسی می تواند اتاق را پیدا کرده و به آن بپیوندد</string>
|
||||
|
@ -2752,14 +2752,14 @@
|
|||
<string name="room_settings_guest_access_title">مجوز پیوستن مهمان به اتاق</string>
|
||||
<string name="spaces_invited_header">دعوتها</string>
|
||||
<string name="suggested_header">اتاقهای پیشنهادی</string>
|
||||
<string name="space_manage_rooms_and_spaces">مدیریت اتاقها و فضاهای کاری</string>
|
||||
<string name="space_manage_rooms_and_spaces">مدیریت اتاقها و فضاها</string>
|
||||
<string name="space_mark_as_not_suggested">نشانهگذاری به عنوان غیر پیشنهادی</string>
|
||||
<string name="space_mark_as_suggested">نشانهگذاری به عنوان پیشنهادی</string>
|
||||
<string name="space_suggested">پیشنهادی</string>
|
||||
<string name="make_this_space_public">عمومی کردن این فضای کاری</string>
|
||||
<string name="make_this_space_public">عمومی کردن این فضا</string>
|
||||
<string name="space_settings_manage_rooms">مدیریت اتاقها</string>
|
||||
<string name="looking_for_someone_not_in_space">به دنبال کسی هستید که در %s نباشد؟</string>
|
||||
<string name="user_invites_you">%s شما را دعوت کرده است</string>
|
||||
<string name="looking_for_someone_not_in_space">دنبال کسی هستید که در %s نیست؟</string>
|
||||
<string name="user_invites_you">%s دعوتتان میکند</string>
|
||||
<string name="a11y_public_room">این اتاق عمومی است</string>
|
||||
<string name="send_images_and_video_with_original_size">رسانه را با اندازه اصلی ارسال کن</string>
|
||||
<plurals name="send_videos_with_original_size">
|
||||
|
@ -2772,29 +2772,72 @@
|
|||
<string name="use_as_default_and_do_not_ask_again">به عنوان پیش فرض استفاده کن و دیگر سوال نپرس</string>
|
||||
<string name="option_always_ask">همواره بپرس</string>
|
||||
<string name="invite_to_space">دعوت به %s</string>
|
||||
<string name="call_transfer_unknown_person">شخص ناشناس</string>
|
||||
<string name="call_transfer_unknown_person">فرد ناشناس</string>
|
||||
<string name="call_transfer_transfer_to_title">انتقال به %1$s</string>
|
||||
<string name="a11y_beta">این قابلیت در وضعیت بتا است</string>
|
||||
<string name="give_feedback">به ما بازخورد دهید</string>
|
||||
<string name="feedback_failed">ارسال بازخورد با شکست مواجه شد(%s)</string>
|
||||
<string name="feedback_sent">سپاس! بازخورد شما با موفقیت ارسال شد</string>
|
||||
<string name="send_feedback_space_info">شما در حال استفاده از نسخه بتای اسپیس هستید. بازخورد شما به آگاه کردن نسخه های بعدی کمک خواهد کرد. سکو و نام کاربری شما ثبت خواهد شد تا به ما در استفاده هرچه بیشتر از بازخورد شما کمک کند.</string>
|
||||
<string name="a11y_beta">این ویژگی در حالت آزمایشی است</string>
|
||||
<string name="give_feedback">دادن بازخورد</string>
|
||||
<string name="feedback_failed">فرستادن بازخورد شکست خورد (%s)</string>
|
||||
<string name="feedback_sent">سپاس! بازخوردتان با موفّقیت فرستاده شد</string>
|
||||
<string name="send_feedback_space_info">دارید از نگارشی آزمایشی از فضاها استفاده میکنید. بازخوردتان در شکلدهی به نگارشهای آتی کمک خواهد کرد. برای کمک به استفادهٔ هرچه بیشتر از بازخوردتان، بنسازه و نام کاربریتان یادداشت خواهد شد.</string>
|
||||
<string name="feedback">بازخورد</string>
|
||||
<string name="error_jitsi_join_conf">متاسفم، یک خطا زمانی که در تلاش برای اضافه شدن به کنفرانس بود ، اتفاق افتاد</string>
|
||||
<string name="directory_add_a_new_server_error_already_added">این کارساز در حال حاضر در فهرست موجود است</string>
|
||||
<string name="directory_add_a_new_server_error">نمیتوانم این کارساز یا فهرست اتاقش را بیابم</string>
|
||||
<string name="directory_add_a_new_server_prompt">نام کارساز جدیدی که میخواهید در آن به جستجو بپردازید را وارد کنید</string>
|
||||
<string name="directory_add_a_new_server_prompt">نام کارسازی جدید که میخواهید کشف کنید را وارد کنید.</string>
|
||||
<string name="directory_add_a_new_server">اضافه کردن یک کارساز(سرور) جدید</string>
|
||||
<string name="directory_your_server">کارساز شما</string>
|
||||
<string name="unnamed_room">اتاق نامگذاری نشده</string>
|
||||
<string name="this_space_has_no_rooms_admin">بعضی اتاق ها ممکن است پنهان باشند زیرا خصوصی هستند و شما به یک دعوتنامه نیاز دارید.</string>
|
||||
<string name="this_space_has_no_rooms_not_admin">بعضی اتاق ها ممکن است پنهان باشند زیرا خصوصی هستند و شما به یک دعوتنامه نیاز دارید.
|
||||
\nشما اجازه اضافه کردن اتاق را ندارید.</string>
|
||||
<string name="this_space_has_no_rooms">این اسپیس هیچ اتاقی ندارد</string>
|
||||
<string name="spaces_no_server_support_description">لطفا برای آگاهی بیشتر با مدیر کارساز خود تماس بگیرید</string>
|
||||
<string name="spaces_no_server_support_title">به نظر می رسد کارساز خانگی شما هنوز از اسپیس ها پشتیبانی نمی کند</string>
|
||||
<string name="space_add_rooms">افزودن اتاق ها</string>
|
||||
<string name="space_leave_prompt_msg_as_admin">شما مدیر این اسپیس هستید،مطمئن شوید قبل از خروج، حق مدیریت را به عضوی دیگر منتقل کرده اید.</string>
|
||||
<string name="space_leave_prompt_msg_private">این اسپیس عمومی نیست. شما نمیتوانید بدون یک دعوتنامه دوباره ملحق شوید.</string>
|
||||
<string name="space_leave_prompt_msg_only_you">شما تنها فرد اینجا هستید. اگر خارج شوید، هیچ کس از جمله شما قادر نخواهد بود در آینده ملحق شود.</string>
|
||||
<string name="unnamed_room">اتاق بینام</string>
|
||||
<string name="this_space_has_no_rooms_admin">ممکن است برخی اتاقها پنهان باشند، زیرا خصوصی بوده و نیاز به دعوت دارید.</string>
|
||||
<string name="this_space_has_no_rooms_not_admin">ممکن است برخی اتاقها پنهان باشند، زیرا خصوصی بوده و نیاز به دعوت دارید.
|
||||
\nاجازهٔ افزودن اتاقها را ندارید.</string>
|
||||
<string name="this_space_has_no_rooms">این فضا هیچ اتاقی ندارد</string>
|
||||
<string name="spaces_no_server_support_description">لطفاً برای اطّلاعات بیشتر، با مدیر کارسازتان تماس بگیرید</string>
|
||||
<string name="spaces_no_server_support_title">به نظر کارساز خانگیتان هنوز از فضاها پشتیبانی نمیکند</string>
|
||||
<string name="space_add_rooms">افزودن اتاقها</string>
|
||||
<string name="space_leave_prompt_msg_as_admin">شما مدیر این فضایید. پیش از ترک، مطمئن شوید حق مدیریت را به عضو دیگری منتقل کردهاید.</string>
|
||||
<string name="space_leave_prompt_msg_private">این فضا عمومی نیست. بدون دعوت نخواهید توانست دوباره بپیوندید.</string>
|
||||
<string name="space_leave_prompt_msg_only_you">شما تنها فرد حاضرید. اگر خارج شوید، هیچکس از جمله خوتان قادر به پیوستن در آینده نخواهد بود.</string>
|
||||
<string name="continue_anyway">به هر حال ادامه بده</string>
|
||||
<string name="you_may_contact_me">اگر پرسش دیگری دارید، میتوانید با من در تماس باشید</string>
|
||||
<string name="send_feedback_space_title">بازخورد فضاها</string>
|
||||
<string name="missing_permissions_title">اجازههای نداشته</string>
|
||||
<string name="denied_permission_camera">برای این کار، لطفاً اجازهٔ دوربین را از تنظیمات سامانه اعطا کنید.</string>
|
||||
<string name="denied_permission_generic">برخی اجازهها برای این کار داده نشده. لطفاً اجازهها را از تنظیمات سامانه اعطا کنید.</string>
|
||||
<string name="error_failed_to_join_room">متأسفانه هنگام تلاش برای پیوستن، خطایی رخ داد: %s</string>
|
||||
<string name="room_upgrade_to_recommended_version">ارتقا به نگارش اتاق پیشنهادی</string>
|
||||
<string name="room_using_unstable_room_version">این اتاق در حال اجرای نگارش %s اتاقهاست که این کارساز خانگی به عنوان ناپایدار علامت زده است.</string>
|
||||
<string name="upgrade_room_no_power_to_manage">برای ارتقای اتاق نیاز به اجازه دارید</string>
|
||||
<string name="upgrade_room_update_parent_space">بهروز رسانی خودکار والد فضا</string>
|
||||
<string name="upgrade_room_auto_invite">دعوت خودکار کاربران</string>
|
||||
<string name="upgrade_public_room_from_to">این اتاق را از %s به %s ارتقا خواهید داد.</string>
|
||||
<string name="upgrade_room_warning">ارتقای اتاق، عملی پیشرفته است و معمولاً هنگامی که اتاقی به خاطر اشکالها، کمبود ویژگیها یا آسیبپذیریهای امنیتی ناپایدار است، پیشنهاد میشود.
|
||||
\nاین عمل معمولاً فقط روی چگونگی فراوری اتاق روی کارساز اثر میگذارد.</string>
|
||||
<string name="upgrade_private_room">ارتقای اتاق خصوصی</string>
|
||||
<string name="upgrade_public_room">ارتقای اتاق عمومی</string>
|
||||
<string name="upgrade">ارتقا</string>
|
||||
<string name="it_may_take_some_time">لطفاً شکیبا باشید. ممکن است کمی زمان ببرد.</string>
|
||||
<string name="joining_replacement_room">پیوستن به اتاق جایگزینی</string>
|
||||
<string name="teammate_spaces_might_not_join">در حال حاضر ممکن است افراد نتوانند به هیچ اتاق خصوصیای که میسازید بپیوندند.
|
||||
\n
|
||||
\nاین مورد را به عنوان بخشی از حالت آزمایشی بهیود خواهیم داد، فقط میخواستیم بدانید.</string>
|
||||
<string name="teammate_spaces_arent_quite_ready">فضاهای همگروهی هنوز کاملاً آماده نیستند، ولی میتوانید بیازماییدشان</string>
|
||||
<string name="settings_server_room_version_unstable">ناپایدار</string>
|
||||
<string name="settings_server_room_version_stable">پایدار</string>
|
||||
<string name="settings_server_default_room_version">نگارش پیشگزیده</string>
|
||||
<string name="settings_server_room_versions">نگارشهای اتاق 👓</string>
|
||||
<string name="verification_scan_self_emoji_subtitle">جایگزینی تأیید با مقایسهٔ اموجیها</string>
|
||||
<string name="verification_scan_with_this_device">پویش با این افزاره</string>
|
||||
<string name="verification_scan_self_notice">با افزارهٔ دیگرتان پوییده یا جابهجا کرده و با این افزاره بپویید</string>
|
||||
<string name="create_space_alias_hint">نشانی فضا</string>
|
||||
<string name="space_settings_alias_subtitle">دیدن و مدیریت نشانیهای این فضا.</string>
|
||||
<string name="space_settings_alias_title">نشانیهای فضا</string>
|
||||
<string name="hs_client_url">نشانی API کارساز خانگی</string>
|
||||
<string name="private_space">فضای خصوصی</string>
|
||||
<string name="public_space">فضای عمومی</string>
|
||||
<string name="command_description_upgrade_room">اتاقی را به نگارشی جدید ارتقا میدهد</string>
|
||||
<string name="a11y_public_space">این فضا عمومی است</string>
|
||||
<string name="spaces_feeling_experimental_subspace">حس آزمایش دارید؟
|
||||
\nمیتوانید فضاهای موجود را به فضایی بیفزایید.</string>
|
||||
<string name="labs_space_show_orphan_in_home">فضای آزمایشی - فقط نمایش یتسمها در خانه</string>
|
||||
<string name="call_transfer_consulting_with">مشاوره با %1$s</string>
|
||||
</resources>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue