Merge branch 'master' into av/desktop-windows-build

This commit is contained in:
Avently committed 2023-09-22 02:20:13 +08:00
commit 363d1b186f
217 files changed
+24025 -14564

No files matched your search

@@ -44,6 +44,7 @@ import java.net.URI
@Composable
actual fun PlatformTextField(
composeState: MutableState<ComposeState>,
sendMsgEnabled: Boolean,
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
@@ -60,6 +61,7 @@ actual fun PlatformTextField(
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
var showKeyboard by remember { mutableStateOf(false) }
var freeFocus by remember { mutableStateOf(false) }
LaunchedEffect(cs.contextItem) {
if (cs.contextItem is ComposeContextItem.QuotedItem) {
delay(100)
@@ -70,6 +72,11 @@ actual fun PlatformTextField(
showKeyboard = true
}
}
LaunchedEffect(sendMsgEnabled) {
if (!sendMsgEnabled) {
freeFocus = true
}
}
AndroidView(modifier = Modifier, factory = {
val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) {
@@ -142,6 +149,11 @@ actual fun PlatformTextField(
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
showKeyboard = false
}
if (freeFocus) {
it.clearFocus()
hideKeyboard(it)
freeFocus = false
}
showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress
}
if (composeState.value.preview is ComposePreview.VoicePreview) {
@@ -27,7 +27,7 @@ actual class RecorderNative: RecorderInterface {
}
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
VideoPlayer.stopAll()
VideoPlayerHolder.stopAll()
AudioPlayer.stop()
val rec: MediaRecorder
recorder = initRecorder().also { rec = it }
@@ -140,7 +140,7 @@ actual object AudioPlayer: AudioPlayerInterface {
return null
}
VideoPlayer.stopAll()
VideoPlayerHolder.stopAll()
RecorderInterface.stopRecording?.invoke()
val current = currentlyPlaying.value
if (current == null || current.first != fileSource.filePath) {
@@ -1,10 +1,13 @@
package chat.simplex.common.platform
import android.media.MediaMetadataRetriever
import android.media.session.PlaybackState
import android.net.Uri
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import chat.simplex.common.helpers.toUri
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import com.google.android.exoplayer2.*
@@ -17,49 +20,15 @@ import kotlinx.coroutines.*
import java.io.File
import java.net.URI
actual class VideoPlayer private constructor(
private val uri: URI,
private val gallery: Boolean,
actual class VideoPlayer actual constructor(
override val uri: URI,
override val gallery: Boolean,
private val defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayerInterface {
actual companion object {
private val players: MutableMap<Pair<URI, Boolean>, VideoPlayer> = mutableMapOf()
private val previewsAndDurations: MutableMap<URI, VideoPlayerInterface.PreviewAndDuration> = mutableMapOf()
actual fun getOrCreate(
uri: URI,
gallery: Boolean,
defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayer =
players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) }
actual fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean =
player(fileName, gallery)?.enableSound(enable) == true
private fun player(fileName: String?, gallery: Boolean): VideoPlayer? {
fileName ?: return null
return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery }
}
actual fun release(uri: URI, gallery: Boolean, remove: Boolean) =
player(uri.path, gallery)?.release(remove).run { }
actual fun stopAll() {
players.values.forEach { it.stop() }
}
actual fun releaseAll() {
players.values.forEach { it.release(false) }
players.clear()
previewsAndDurations.clear()
}
}
private val currentVolume: Float
override val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled)
override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
@@ -114,7 +83,7 @@ actual class VideoPlayer private constructor(
RecorderInterface.stopRecording?.invoke()
}
AudioPlayer.stop()
stopAll()
VideoPlayerHolder.stopAll()
if (listener.value == null) {
runCatching {
val dataSourceFactory = DefaultDataSource.Factory(androidAppContext, DefaultHttpDataSource.Factory())
@@ -224,14 +193,14 @@ actual class VideoPlayer private constructor(
override fun release(remove: Boolean) {
player.release()
if (remove) {
players.remove(uri to gallery)
VideoPlayerHolder.players.remove(uri to gallery)
}
}
private fun setPreviewAndDuration() {
// It freezes main thread, doing it in IO thread
CoroutineScope(Dispatchers.IO).launch {
val previewAndDuration = previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri) }
val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri) }
withContext(Dispatchers.Main) {
preview.value = previewAndDuration.preview ?: defaultPreview
duration.value = (previewAndDuration.duration ?: 0)
@@ -51,7 +51,7 @@ actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap:
}
@Composable
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) {
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) {
AndroidView(
factory = { ctx ->
StyledPlayerView(ctx).apply {
@@ -72,7 +72,7 @@ if(NOT APPLE)
else()
# Without direct linking it can't find hs_init in linking step
add_library( rts SHARED IMPORTED )
FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/deps/libHSrts_thr-*.${OS_LIB_EXT})
FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/deps/libHSrts*_thr-*.${OS_LIB_EXT})
set_target_properties( rts PROPERTIES IMPORTED_LOCATION ${RTSLIB})
target_link_libraries(app-lib rts simplex)
@@ -7,13 +7,12 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.ComposeState
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.platform.AudioPlayer
import chat.simplex.common.platform.chatController
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
@@ -606,10 +605,13 @@ data class Chat (
val userCanSend: Boolean
get() = when (chatInfo) {
is ChatInfo.Direct -> true
is ChatInfo.Group -> {
val m = chatInfo.groupInfo.membership
m.memberActive && m.memberRole >= GroupMemberRole.Member
}
is ChatInfo.Group -> chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Member
else -> false
}
val nextSendGrpInv: Boolean
get() = when (chatInfo) {
is ChatInfo.Direct -> chatInfo.contact.nextSendGrpInv
else -> false
}
@@ -799,13 +801,18 @@ data class Contact(
val userPreferences: ChatPreferences,
val mergedPreferences: ContactUserPreferences,
override val createdAt: Instant,
override val updatedAt: Instant
override val updatedAt: Instant,
val contactGroupMemberId: Long? = null,
val contactGrpInvSent: Boolean
): SomeChat, NamedChat {
override val chatType get() = ChatType.Direct
override val id get() = "@$contactId"
override val apiId get() = contactId
override val ready get() = activeConn.connStatus == ConnStatus.Ready
override val sendMsgEnabled get() = !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false)
override val sendMsgEnabled get() =
(ready && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false))
|| nextSendGrpInv
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
override val ntfsEnabled get() = chatSettings.enableNtfs
override val incognito get() = contactConnIncognito
override fun featureEnabled(feature: ChatFeature) = when (feature) {
@@ -856,7 +863,8 @@ data class Contact(
userPreferences = ChatPreferences.sampleData,
mergedPreferences = ContactUserPreferences.sampleData,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
updatedAt = Clock.System.now(),
contactGrpInvSent = false
)
}
}
@@ -881,6 +889,7 @@ class ContactSubStatus(
data class Connection(
val connId: Long,
val agentConnId: String,
val peerChatVRange: VersionRange,
val connStatus: ConnStatus,
val connLevel: Int,
val viaGroupLink: Boolean,
@@ -890,10 +899,17 @@ data class Connection(
) {
val id: ChatId get() = ":$connId"
companion object {
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null)
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null)
}
}
@Serializable
data class VersionRange(val minVersion: Int, val maxVersion: Int) {
fun isCompatibleRange(vRange: VersionRange): Boolean =
this.minVersion <= vRange.maxVersion && vRange.minVersion <= this.maxVersion
}
@Serializable
data class SecurityCode(val securityCode: String, val verifiedAt: Instant)
@@ -1224,6 +1240,7 @@ class MemberSubError (
@Serializable
class UserContactRequest (
val contactRequestId: Long,
val cReqChatVRange: VersionRange,
override val localDisplayName: String,
val profile: Profile,
override val createdAt: Instant,
@@ -1246,6 +1263,7 @@ class UserContactRequest (
companion object {
val sampleData = UserContactRequest(
contactRequestId = 1,
cReqChatVRange = VersionRange(1, 1),
localDisplayName = "alice",
profile = Profile.sampleData,
createdAt = Clock.System.now(),
@@ -1398,8 +1416,7 @@ data class ChatItem (
val encryptedFile: Boolean? = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null
val encryptLocalFile: Boolean
get() = file?.fileProtocol == FileProtocol.XFTP &&
content.msgContent !is MsgContent.MCVideo &&
get() = content.msgContent !is MsgContent.MCVideo &&
chatController.appPrefs.privacyEncryptLocalFiles.get()
val memberDisplayName: String? get() =
@@ -1465,6 +1482,7 @@ data class ChatItem (
is RcvGroupEvent.GroupDeleted -> showNtfDir
is RcvGroupEvent.GroupUpdated -> false
is RcvGroupEvent.InvitedViaGroupLink -> false
is RcvGroupEvent.MemberCreatedContact -> false
}
is CIContent.SndGroupEventContent -> showNtfDir
is CIContent.RcvConnEventContent -> false
@@ -2093,6 +2111,23 @@ data class CryptoFile(
val isAbsolutePath: Boolean
get() = File(filePath).isAbsolute
@Transient
private var tmpFile: File? = null
fun createTmpFileIfNeeded(): File {
if (tmpFile == null) {
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
tmpFile.deleteOnExit()
ChatModel.filesToDelete.add(tmpFile)
this.tmpFile = tmpFile
}
return tmpFile!!
}
fun deleteTmpFile() {
tmpFile?.delete()
}
companion object {
fun plain(f: String): CryptoFile = CryptoFile(f, null)
}
@@ -2464,6 +2499,7 @@ sealed class RcvGroupEvent() {
@Serializable @SerialName("groupDeleted") class GroupDeleted(): RcvGroupEvent()
@Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): RcvGroupEvent()
@Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent()
@Serializable @SerialName("memberCreatedContact") class MemberCreatedContact(): RcvGroupEvent()
val text: String get() = when (this) {
is MemberAdded -> String.format(generalGetString(MR.strings.rcv_group_event_member_added), profile.profileViewName)
@@ -2476,6 +2512,7 @@ sealed class RcvGroupEvent() {
is GroupDeleted -> generalGetString(MR.strings.rcv_group_event_group_deleted)
is GroupUpdated -> generalGetString(MR.strings.rcv_group_event_updated_group_profile)
is InvitedViaGroupLink -> generalGetString(MR.strings.rcv_group_event_invited_via_your_group_link)
is MemberCreatedContact -> generalGetString(MR.strings.rcv_group_event_member_created_contact)
}
}
@@ -26,6 +26,12 @@ import java.util.Date
typealias ChatCtrl = Long
// currentChatVersion in core
const val CURRENT_CHAT_VERSION: Int = 2
// version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core)
val CREATE_MEMBER_CONTACT_VRANGE = VersionRange(minVersion = 2, maxVersion = CURRENT_CHAT_VERSION)
enum class CallOnLockScreen {
DISABLE,
SHOW,
@@ -784,16 +790,18 @@ object ChatController {
return null
}
suspend fun apiGetContactCode(contactId: Long): Pair<Contact, String> {
suspend fun apiGetContactCode(contactId: Long): Pair<Contact, String>? {
val r = sendCmd(CC.APIGetContactCode(contactId))
if (r is CR.ContactCode) return r.contact to r.connectionCode
throw Exception("failed to get contact code: ${r.responseType} ${r.details}")
Log.e(TAG,"failed to get contact code: ${r.responseType} ${r.details}")
return null
}
suspend fun apiGetGroupMemberCode(groupId: Long, groupMemberId: Long): Pair<GroupMember, String> {
suspend fun apiGetGroupMemberCode(groupId: Long, groupMemberId: Long): Pair<GroupMember, String>? {
val r = sendCmd(CC.APIGetGroupMemberCode(groupId, groupMemberId))
if (r is CR.GroupMemberCode) return r.member to r.connectionCode
throw Exception("failed to get group member code: ${r.responseType} ${r.details}")
Log.e(TAG,"failed to get group member code: ${r.responseType} ${r.details}")
return null
}
suspend fun apiVerifyContact(contactId: Long, connectionCode: String?): Pair<Boolean, String>? {
@@ -1272,6 +1280,30 @@ object ChatController {
}
}
suspend fun apiCreateMemberContact(groupId: Long, groupMemberId: Long): Contact? {
return when (val r = sendCmd(CC.APICreateMemberContact(groupId, groupMemberId))) {
is CR.NewMemberContact -> r.contact
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiCreateMemberContact", generalGetString(MR.strings.error_creating_member_contact), r)
}
null
}
}
}
suspend fun apiSendMemberContactInvitation(contactId: Long, mc: MsgContent): Contact? {
return when (val r = sendCmd(CC.APISendMemberContactInvitation(contactId, mc))) {
is CR.NewMemberContactSentInv -> r.contact
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiSendMemberContactInvitation", generalGetString(MR.strings.error_sending_message_contact_invitation), r)
}
null
}
}
}
suspend fun allowFeatureToContact(contact: Contact, feature: ChatFeature, param: Int? = null) {
val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param)
val toContact = apiSetContactPrefs(contact.contactId, prefs)
@@ -1527,6 +1559,10 @@ object ChatController {
if (active(r.user)) {
chatModel.updateGroup(r.toGroup)
}
is CR.NewMemberContactReceivedInv ->
if (active(r.user)) {
chatModel.updateContact(r.contact)
}
is CR.RcvFileStart ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileComplete ->
@@ -1822,6 +1858,8 @@ sealed class CC {
class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC()
class APIDeleteGroupLink(val groupId: Long): CC()
class APIGetGroupLink(val groupId: Long): CC()
class APICreateMemberContact(val groupId: Long, val groupMemberId: Long): CC()
class APISendMemberContactInvitation(val contactId: Long, val mc: MsgContent): CC()
class APIGetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol): CC()
class APISetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol, val servers: List<ServerCfg>): CC()
class APITestProtoServer(val userId: Long, val server: String): CC()
@@ -1927,6 +1965,8 @@ sealed class CC {
is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}"
is APIDeleteGroupLink -> "/_delete link #$groupId"
is APIGetGroupLink -> "/_get link #$groupId"
is APICreateMemberContact -> "/_create member contact #$groupId $groupMemberId"
is APISendMemberContactInvitation -> "/_invite member contact @$contactId ${mc.cmdString}"
is APIGetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()}"
is APISetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()} ${protoServersStr(servers)}"
is APITestProtoServer -> "/_server test $userId $server"
@@ -2021,6 +2061,8 @@ sealed class CC {
is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole"
is APIDeleteGroupLink -> "apiDeleteGroupLink"
is APIGetGroupLink -> "apiGetGroupLink"
is APICreateMemberContact -> "apiCreateMemberContact"
is APISendMemberContactInvitation -> "apiSendMemberContactInvitation"
is APIGetUserProtoServers -> "apiGetUserProtoServers"
is APISetUserProtoServers -> "apiSetUserProtoServers"
is APITestProtoServer -> "testProtoServer"
@@ -2350,7 +2392,7 @@ data class NetCfg(
sessionMode = TransportSessionMode.User,
tcpConnectTimeout = 15_000_000,
tcpTimeout = 10_000_000,
tcpTimeoutPerKb = 20_000,
tcpTimeoutPerKb = 30_000,
tcpKeepAlive = KeepAliveOpts.defaults,
smpPingInterval = 1200_000_000,
smpPingCount = 3
@@ -2364,7 +2406,7 @@ data class NetCfg(
sessionMode = TransportSessionMode.User,
tcpConnectTimeout = 30_000_000,
tcpTimeout = 20_000_000,
tcpTimeoutPerKb = 40_000,
tcpTimeoutPerKb = 60_000,
tcpKeepAlive = KeepAliveOpts.defaults,
smpPingInterval = 1200_000_000,
smpPingCount = 3
@@ -3311,6 +3353,9 @@ sealed class CR {
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR()
@Serializable @SerialName("groupLink") class GroupLink(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR()
@Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR()
@Serializable @SerialName("newMemberContact") class NewMemberContact(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("newMemberContactSentInv") class NewMemberContactSentInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("newMemberContactReceivedInv") class NewMemberContactReceivedInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR()
// receiving file events
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: UserRef, val chatItem: AChatItem): CR()
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR()
@@ -3438,6 +3483,9 @@ sealed class CR {
is GroupLinkCreated -> "groupLinkCreated"
is GroupLink -> "groupLink"
is GroupLinkDeleted -> "groupLinkDeleted"
is NewMemberContact -> "newMemberContact"
is NewMemberContactSentInv -> "newMemberContactSentInv"
is NewMemberContactReceivedInv -> "newMemberContactReceivedInv"
is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled"
is RcvFileAccepted -> "rcvFileAccepted"
is RcvFileStart -> "rcvFileStart"
@@ -3563,6 +3611,9 @@ sealed class CR {
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo))
is NewMemberContact -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
is NewMemberContactReceivedInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
is RcvFileAcceptedSndCancelled -> withUser(user, noDetails())
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
is RcvFileStart -> withUser(user, json.encodeToString(chatItem))
@@ -3820,6 +3871,7 @@ sealed class ChatErrorType {
is AgentCommandError -> "agentCommandError"
is InvalidFileDescription -> "invalidFileDescription"
is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited"
is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible"
is InternalError -> "internalError"
is CEException -> "exception $message"
}
@@ -3894,6 +3946,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType()
@Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType()
@Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType()
@Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType()
@Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType()
@Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType()
}
@@ -3922,6 +3975,7 @@ sealed class StoreError {
is GroupMemberNameNotFound -> "groupMemberNameNotFound"
is GroupMemberNotFound -> "groupMemberNotFound"
is GroupMemberNotFoundByMemberId -> "groupMemberNotFoundByMemberId"
is MemberContactGroupMemberNotFound -> "memberContactGroupMemberNotFound"
is GroupWithoutUser -> "groupWithoutUser"
is DuplicateGroupMember -> "duplicateGroupMember"
is GroupAlreadyJoined -> "groupAlreadyJoined"
@@ -3979,6 +4033,7 @@ sealed class StoreError {
@Serializable @SerialName("groupMemberNameNotFound") class GroupMemberNameNotFound(val groupId: Long, val groupMemberName: String): StoreError()
@Serializable @SerialName("groupMemberNotFound") class GroupMemberNotFound(val groupMemberId: Long): StoreError()
@Serializable @SerialName("groupMemberNotFoundByMemberId") class GroupMemberNotFoundByMemberId(val memberId: String): StoreError()
@Serializable @SerialName("memberContactGroupMemberNotFound") class MemberContactGroupMemberNotFound(val contactId: Long): StoreError()
@Serializable @SerialName("groupWithoutUser") object GroupWithoutUser: StoreError()
@Serializable @SerialName("duplicateGroupMember") object DuplicateGroupMember: StoreError()
@Serializable @SerialName("groupAlreadyJoined") object GroupAlreadyJoined: StoreError()
@@ -8,6 +8,7 @@ import chat.simplex.common.views.chat.ComposeState
@Composable
expect fun PlatformTextField(
composeState: MutableState<ComposeState>,
sendMsgEnabled: Boolean,
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
@@ -7,6 +7,8 @@ import java.net.URI
interface VideoPlayerInterface {
data class PreviewAndDuration(val preview: ImageBitmap?, val duration: Long?, val timestamp: Long)
val uri: URI
val gallery: Boolean
val soundEnabled: MutableState<Boolean>
val brokenVideo: MutableState<Boolean>
val videoPlaying: MutableState<Boolean>
@@ -20,18 +22,45 @@ interface VideoPlayerInterface {
fun release(remove: Boolean)
}
expect class VideoPlayer: VideoPlayerInterface {
companion object {
fun getOrCreate(
uri: URI,
gallery: Boolean,
defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayer
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean
fun release(uri: URI, gallery: Boolean, remove: Boolean)
fun stopAll()
fun releaseAll()
expect class VideoPlayer(
uri: URI,
gallery: Boolean,
defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayerInterface
object VideoPlayerHolder {
val players: MutableMap<Pair<URI, Boolean>, VideoPlayer> = mutableMapOf()
val previewsAndDurations: MutableMap<URI, VideoPlayerInterface.PreviewAndDuration> = mutableMapOf()
fun getOrCreate(
uri: URI,
gallery: Boolean,
defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayer =
players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) }
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean =
player(fileName, gallery)?.enableSound(enable) == true
private fun player(fileName: String?, gallery: Boolean): VideoPlayer? {
fileName ?: return null
return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery }
}
fun release(uri: URI, gallery: Boolean, remove: Boolean) =
player(uri.path, gallery)?.release(remove).run { }
fun stopAll() {
players.values.forEach { it.stop() }
}
fun releaseAll() {
players.values.forEach { it.release(false) }
players.clear()
previewsAndDurations.clear()
}
}
@@ -85,6 +85,8 @@ fun TerminalLayout(
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = false,
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
sendMsgEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
userIsObserver = false,
@@ -291,21 +291,23 @@ fun ChatInfoLayout(
SectionDividerSpaced()
}
SectionView {
if (connectionCode != null) {
VerifyCodeButton(contact.verified, verifyClicked)
if (contact.ready) {
SectionView {
if (connectionCode != null) {
VerifyCodeButton(contact.verified, verifyClicked)
}
ContactPreferencesButton(openPreferences)
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
if (cStats != null && cStats.ratchetSyncAllowed) {
SynchronizeConnectionButton(syncContactConnection)
}
// } else if (developerTools) {
// SynchronizeConnectionButtonForce(syncContactConnectionForce)
// }
}
ContactPreferencesButton(openPreferences)
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
if (cStats != null && cStats.ratchetSyncAllowed) {
SynchronizeConnectionButton(syncContactConnection)
}
// } else if (developerTools) {
// SynchronizeConnectionButtonForce(syncContactConnectionForce)
// }
SectionDividerSpaced()
}
SectionDividerSpaced()
if (contact.contactLink != null) {
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
@@ -316,36 +318,40 @@ fun ChatInfoLayout(
SectionDividerSpaced()
}
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
SectionItemView({
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.network_status),
contactNetworkStatus.statusExplanation
)}) {
NetworkStatusRow(contactNetworkStatus)
}
if (cStats != null) {
SwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
switchAddress = switchContactAddress
)
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
AbortSwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
abortSwitchAddress = abortSwitchContactAddress
if (contact.ready) {
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
SectionItemView({
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.network_status),
contactNetworkStatus.statusExplanation
)
}) {
NetworkStatusRow(contactNetworkStatus)
}
val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer }
if (rcvServers.isNotEmpty()) {
SimplexServers(stringResource(MR.strings.receiving_via), rcvServers)
}
val sndServers = cStats.sndQueuesInfo.map { it.sndServer }
if (sndServers.isNotEmpty()) {
SimplexServers(stringResource(MR.strings.sending_via), sndServers)
if (cStats != null) {
SwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
switchAddress = switchContactAddress
)
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
AbortSwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
abortSwitchAddress = abortSwitchContactAddress
)
}
val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer }
if (rcvServers.isNotEmpty()) {
SimplexServers(stringResource(MR.strings.receiving_via), rcvServers)
}
val sndServers = cStats.sndQueuesInfo.map { it.sndServer }
if (sndServers.isNotEmpty()) {
SimplexServers(stringResource(MR.strings.sending_via), sndServers)
}
}
}
SectionDividerSpaced()
}
SectionDividerSpaced()
SectionView {
ClearChatButton(clearChat)
DeleteContactButton(deleteContact)
@@ -114,7 +114,18 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
unreadCount,
composeState,
composeView = {
if (chat.chatInfo.sendMsgEnabled) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (chat.chatInfo is ChatInfo.Direct && !chat.chatInfo.contact.ready && !chat.chatInfo.contact.nextSendGrpInv) {
Text(
generalGetString(MR.strings.contact_connection_pending),
Modifier.padding(top = 4.dp),
fontSize = 14.sp,
color = MaterialTheme.colors.secondary
)
}
ComposeView(
chatModel, chat, composeState, attachmentOption,
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
@@ -145,7 +156,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
var preloadedLink: Pair<String, GroupMemberRole>? = null
if (chat.chatInfo is ChatInfo.Direct) {
preloadedContactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
preloadedCode = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId).second
preloadedCode = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)?.second
} else if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
preloadedLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
@@ -158,7 +169,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
preloadedContactInfo = contactInfo
code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId).second
code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)?.second
preloadedCode = code
}
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
@@ -183,12 +194,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val (_, code) = if (member.memberActive) {
try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
member to null
}
val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
member to memCode?.second
} else {
member to null
}
@@ -280,6 +287,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
chatModel.controller.allowFeatureToContact(contact, feature, param)
}
},
openDirectChat = { contactId ->
withApi {
openDirectChat(contactId, chatModel)
}
},
updateContactStats = { contact ->
withApi {
val r = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
@@ -409,6 +421,7 @@ fun ChatLayout(
startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
@@ -485,7 +498,7 @@ fun ChatLayout(
ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
)
@@ -534,15 +547,22 @@ fun ChatInfoToolbar(
IconButton({
showMenu.value = false
startCall(CallMediaType.Audio)
}) {
Icon(painterResource(MR.images.ic_call_500), stringResource(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.primary)
},
enabled = chat.chatInfo.contact.ready) {
Icon(
painterResource(MR.images.ic_call_500),
stringResource(MR.strings.icon_descr_more_button),
tint = if (chat.chatInfo.contact.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
}
}
menuItems.add {
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
showMenu.value = false
startCall(CallMediaType.Video)
})
if (chat.chatInfo.contact.ready) {
menuItems.add {
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
showMenu.value = false
startCall(CallMediaType.Video)
})
}
}
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) {
barButtons.add {
@@ -554,20 +574,22 @@ fun ChatInfoToolbar(
}
}
}
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
menuItems.add {
ItemAction(
if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
onClick = {
showMenu.value = false
// Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu
scope.launch {
delay(200)
changeNtfsState(!ntfsEnabled.value, ntfsEnabled)
if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready) || chat.chatInfo is ChatInfo.Group) {
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
menuItems.add {
ItemAction(
if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
onClick = {
showMenu.value = false
// Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu
scope.launch {
delay(200)
changeNtfsState(!ntfsEnabled.value, ntfsEnabled)
}
}
}
)
)
}
}
barButtons.add {
@@ -661,6 +683,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
@@ -714,7 +737,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
DisposableEffectOnGone(
whenGone = {
VideoPlayer.releaseAll()
VideoPlayerHolder.releaseAll()
}
)
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
@@ -808,7 +831,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
) {
MemberImage(member)
}
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames)
}
}
} else {
@@ -817,7 +840,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
.then(swipeableModifier)
) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames)
}
}
}
@@ -827,7 +850,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
.then(swipeableModifier)
) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
}
}
} else { // direct message
@@ -838,7 +861,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
).then(swipeableModifier)
) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
}
}
@@ -1263,6 +1286,7 @@ fun PreviewChatLayout() {
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
@@ -1330,6 +1354,7 @@ fun PreviewGroupChatLayout() {
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
@@ -0,0 +1,39 @@
package chat.simplex.common.views.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
fun ComposeContextInvitingContactMemberView() {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
Row(
Modifier
.height(60.dp)
.fillMaxWidth()
.padding(top = 8.dp)
.background(sentColor),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(MR.images.ic_chat),
stringResource(MR.strings.button_send_direct_message),
modifier = Modifier
.padding(start = 12.dp, end = 8.dp)
.height(20.dp)
.width(20.dp),
tint = MaterialTheme.colors.secondary
)
Text(generalGetString(MR.strings.compose_send_direct_message_to_connect))
}
}
@@ -335,8 +335,6 @@ fun ComposeView(
return null
}
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
val cInfo = chat.chatInfo
val cs = composeState.value
@@ -358,6 +356,7 @@ fun ComposeView(
MsgContent.MCText(msgText)
}
}
else -> MsgContent.MCText(msgText)
}
}
@@ -374,6 +373,14 @@ fun ComposeView(
}
}
suspend fun sendMemberContactInvitation() {
val mc = checkLinkPreview()
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.chatInfo.apiId, mc)
if (contact != null) {
chatModel.updateContact(contact)
}
}
suspend fun updateMessage(ei: ChatItem, cInfo: ChatInfo, live: Boolean): ChatItem? {
val oldMsgContent = ei.content.msgContent
if (oldMsgContent != null) {
@@ -397,7 +404,10 @@ fun ComposeView(
}
clearCurrentDraft()
if (cs.contextItem is ComposeContextItem.EditingItem) {
if (chat.nextSendGrpInv) {
sendMemberContactInvitation()
sent = null
} else if (cs.contextItem is ComposeContextItem.EditingItem) {
val ei = cs.contextItem.chatItem
sent = updateMessage(ei, cInfo, live)
} else if (liveMessage != null && liveMessage.sent) {
@@ -655,9 +665,14 @@ fun ComposeView(
}
val userCanSend = rememberUpdatedState(chat.userCanSend)
val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled)
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv)
Column {
if (nextSendGrpInv.value) {
ComposeContextInvitingContactMemberView()
}
if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) {
contextItemView()
when {
@@ -690,15 +705,21 @@ fun ComposeView(
} else {
showChooseAttachment
}
val attachmentEnabled =
!composeState.value.attachmentDisabled
&& sendMsgEnabled.value
&& userCanSend.value
&& !isGroupAndProhibitedFiles
&& !nextSendGrpInv.value
IconButton(
attachmentClicked,
Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp),
enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value
enabled = attachmentEnabled
) {
Icon(
painterResource(MR.images.ic_attach_file_filled_500),
contentDescription = stringResource(MR.strings.attach),
tint = if (!composeState.value.attachmentDisabled && userCanSend.value && !isGroupAndProhibitedFiles) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
tint = if (attachmentEnabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
@@ -774,6 +795,8 @@ fun ComposeView(
recState,
chat.chatInfo is ChatInfo.Direct,
liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown,
sendMsgEnabled = sendMsgEnabled.value,
nextSendGrpInv = nextSendGrpInv.value,
needToAllowVoiceToContact,
allowedVoiceByPrefs,
allowVoiceToContact = ::allowVoiceToContact,
@@ -37,6 +37,8 @@ fun SendMsgView(
recState: MutableState<RecordingState>,
isDirectChat: Boolean,
liveMessageAlertShown: SharedPreference<Boolean>,
sendMsgEnabled: Boolean,
nextSendGrpInv: Boolean,
needToAllowVoiceToContact: Boolean,
allowedVoiceByPrefs: Boolean,
userIsObserver: Boolean,
@@ -74,16 +76,16 @@ fun SendMsgView(
false
}
}
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
val showVoiceButton = !nextSendGrpInv && cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
PlatformTextField(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage) {
PlatformTextField(composeState, sendMsgEnabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage) {
if (!cs.inProgress) {
sendMessage(null)
}
}
// Disable clicks on text field
if (cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
if (!sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
Box(
Modifier
.matchParentSize()
@@ -110,7 +112,7 @@ fun SendMsgView(
}
when {
progressByTimeout -> ProgressIndicator()
showVoiceButton -> {
showVoiceButton && sendMsgEnabled -> {
Row(verticalAlignment = Alignment.CenterVertically) {
val stopRecOnNextClick = remember { mutableStateOf(false) }
when {
@@ -150,7 +152,7 @@ fun SendMsgView(
else -> {
val cs = composeState.value
val icon = if (cs.editing || cs.liveMessage != null) painterResource(MR.images.ic_check_filled) else painterResource(MR.images.ic_arrow_upward)
val disabled = !cs.sendEnabled() ||
val disabled = !sendMsgEnabled || !cs.sendEnabled() ||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled
val showDropdown = rememberSaveable { mutableStateOf(false) }
@@ -159,7 +161,7 @@ fun SendMsgView(
fun MenuItems(): List<@Composable () -> Unit> {
val menuItems = mutableListOf<@Composable () -> Unit>()
if (cs.liveMessage == null && !cs.editing) {
if (cs.liveMessage == null && !cs.editing && !nextSendGrpInv || sendMsgEnabled) {
if (
cs.preview !is ComposePreview.VoicePreview &&
cs.contextItem is ComposeContextItem.NoContextItem &&
@@ -599,6 +601,8 @@ fun PreviewSendMsgView() {
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
@@ -630,6 +634,8 @@ fun PreviewSendMsgViewEditing() {
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
@@ -661,6 +667,8 @@ fun PreviewSendMsgViewInProgress() {
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
@@ -76,12 +76,8 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val (_, code) = if (member.memberActive) {
try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
member to null
}
val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
member to memCode?.second
} else {
member to null
}
@@ -35,6 +35,7 @@ import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.*
import chat.simplex.common.views.chatlist.openChat
import chat.simplex.res.MR
import kotlinx.datetime.Clock
@@ -52,6 +53,8 @@ fun GroupMemberInfoView(
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val connStats = remember { mutableStateOf(connectionStats) }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
var progressIndicator by remember { mutableStateOf(false) }
if (chat != null) {
val newRole = remember { mutableStateOf(member.memberRole) }
GroupMemberInfoLayout(
@@ -76,6 +79,20 @@ fun GroupMemberInfoView(
}
}
},
createMemberContact = {
withApi {
progressIndicator = true
val memberContact = chatModel.controller.apiCreateMemberContact(groupInfo.apiId, member.groupMemberId)
if (memberContact != null) {
val memberChat = Chat(ChatInfo.Direct(memberContact), chatItems = arrayListOf())
chatModel.addChat(memberChat)
openChat(memberChat, chatModel)
closeAll()
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
}
progressIndicator = false
}
},
connectViaAddress = { connReqUri ->
connectViaMemberAddressAlert(connReqUri)
},
@@ -170,6 +187,10 @@ fun GroupMemberInfoView(
}
}
)
if (progressIndicator) {
ProgressIndicator()
}
}
}
@@ -201,6 +222,7 @@ fun GroupMemberInfoLayout(
connectionCode: String?,
getContactChat: (Long) -> Chat?,
openDirectChat: (Long) -> Unit,
createMemberContact: () -> Unit,
connectViaAddress: (String) -> Unit,
removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit,
@@ -237,9 +259,13 @@ fun GroupMemberInfoLayout(
if (member.memberActive) {
SectionView {
if (contactId != null) {
if (knownDirectChat(contactId) != null || groupInfo.fullGroupPreferences.directMessages.on) {
if (contactId != null && knownDirectChat(contactId) != null) {
OpenChatButton(onClick = { openDirectChat(contactId) })
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
if (contactId != null) {
OpenChatButton(onClick = { openDirectChat(contactId) })
} else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) {
OpenChatButton(onClick = { createMemberContact() })
}
}
if (connectionCode != null) {
@@ -498,6 +524,7 @@ fun PreviewGroupMemberInfoLayout() {
connectionCode = "123",
getContactChat = { Chat.sampleData },
openDirectChat = {},
createMemberContact = {},
connectViaAddress = {},
removeMember = {},
onRoleSelected = {},
@@ -71,7 +71,7 @@ fun CIFileView(
when (file.fileStatus) {
is CIFileStatus.RcvInvitation -> {
if (fileSizeValid()) {
val encrypted = file.fileProtocol == FileProtocol.XFTP && chatController.appPrefs.privacyEncryptLocalFiles.get()
val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()
receiveFile(file.fileId, encrypted)
} else {
AlertManager.shared.showAlertMsg(
@@ -0,0 +1,70 @@
package chat.simplex.common.views.chat.item
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.model.*
import chat.simplex.res.MR
@Composable
fun CIMemberCreatedContactView(
chatItem: ChatItem,
openDirectChat: (Long) -> Unit
) {
fun eventText(): AnnotatedString {
val memberDisplayName = chatItem.memberDisplayName
return if (memberDisplayName != null) {
buildAnnotatedString {
withStyle(chatEventStyle) { append(memberDisplayName) }
append(" ")
withStyle(chatEventStyle) { append(chatItem.content.text) }
}
} else {
buildAnnotatedString {
withStyle(chatEventStyle) { append(chatItem.content.text) }
}
}
}
Row(
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
if (chatItem.chatDir is CIDirection.GroupRcv && chatItem.chatDir.groupMember.memberContactId != null) {
val openChatStyle = SpanStyle(color = MaterialTheme.colors.primary, fontSize = 12.sp)
val annotatedText = buildAnnotatedString {
append(eventText())
append(" ")
withAnnotation(tag = "Open", annotation = "Open") {
withStyle(openChatStyle) { append(generalGetString(MR.strings.rcv_group_event_open_chat) + " ") }
}
withStyle(chatEventStyle) { append(chatItem.timestampText) }
}
fun open(offset: Int): Boolean = annotatedText.getStringAnnotations(tag = "Open", start = offset, end = offset).isNotEmpty()
ClickableText(
annotatedText,
onClick = {
if (open(it)) {
openDirectChat(chatItem.chatDir.groupMember.memberContactId)
}
},
shouldConsumeEvent = ::open
)
} else {
val annotatedText = buildAnnotatedString {
append(eventText())
append(" ")
withStyle(chatEventStyle) { append(chatItem.timestampText) }
}
Text(annotatedText)
}
}
}
@@ -50,7 +50,7 @@ fun CIVideoView(
})
} else {
Box {
ImageView(preview, showMenu, onClick = {
VideoPreviewImageView(preview, onClick = {
if (file != null) {
when (file.fileStatus) {
CIFileStatus.RcvInvitation ->
@@ -75,7 +75,10 @@ fun CIVideoView(
else -> {}
}
}
})
},
onLongClick = {
showMenu.value = true
})
if (file != null) {
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
}
@@ -90,7 +93,7 @@ fun CIVideoView(
@Composable
private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true) }
val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, false, defaultPreview, defaultDuration, true) }
val videoPlaying = remember(uri.path) { player.videoPlaying }
val progress = remember(uri.path) { player.progress }
val duration = remember(uri.path) { player.duration }
@@ -111,6 +114,7 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
stop()
}
}
val onLongClick = { showMenu.value = true }
Box {
val windowWidth = LocalWindowWidth()
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
@@ -118,12 +122,12 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
player,
width,
onClick = onClick,
onLongClick = { showMenu.value = true },
onLongClick = onLongClick,
stop
)
if (showPreview.value) {
ImageView(preview, showMenu, onClick)
PlayButton(brokenVideo, onLongClick = { showMenu.value = true }, play)
VideoPreviewImageView(preview, onClick, onLongClick)
PlayButton(brokenVideo, onLongClick = onLongClick, if (appPlatform.isAndroid) play else onClick)
}
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
}
@@ -201,7 +205,7 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat
}
@Composable
private fun ImageView(preview: ImageBitmap, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick: () -> Unit) {
val windowWidth = LocalWindowWidth()
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
Image(
@@ -210,10 +214,10 @@ private fun ImageView(preview: ImageBitmap, showMenu: MutableState<Boolean>, onC
modifier = Modifier
.width(width)
.combinedClickable(
onLongClick = { showMenu.value = true },
onLongClick = onLongClick,
onClick = onClick
)
.onRightClick { showMenu.value = true },
.onRightClick(onLongClick),
contentScale = ContentScale.FillWidth,
)
}
@@ -54,6 +54,7 @@ fun ChatItemView(
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
@@ -348,6 +349,7 @@ fun ChatItemView(
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) {
is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText())
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
else -> EventItemView()
}
is CIContent.SndGroupEventContent -> EventItemView()
@@ -572,6 +574,7 @@ fun PreviewChatItemView() {
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
@@ -601,6 +604,7 @@ fun PreviewChatItemViewDeletedContent() {
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
@@ -46,9 +46,11 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
val scope = rememberCoroutineScope()
val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
DisposableEffectOnGone(
whenGone = { playersToRelease.forEach { VideoPlayer.release(it, true, true) } }
whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } }
)
HorizontalPager(pageCount = remember { provider.totalMediaSize }.value, state = pagerState) { index ->
@Composable
fun Content(index: Int) {
Column(
Modifier
.fillMaxSize()
@@ -127,7 +129,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
FullScreenImageView(modifier, data, imageBitmap)
} else if (media is ProviderMedia.Video) {
val preview = remember(media.uri.path) { base64ToBitmap(media.preview) }
VideoView(modifier, media.uri, preview, index == settledCurrentPage)
VideoView(modifier, media.uri, preview, index == settledCurrentPage, close)
DisposableEffect(Unit) {
onDispose { playersToRelease.add(media.uri) }
}
@@ -135,14 +137,19 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
}
}
}
if (appPlatform.isAndroid) {
HorizontalPager(pageCount = remember { provider.totalMediaSize }.value, state = pagerState) { index -> Content(index) }
} else {
Content(pagerState.currentPage)
}
}
@Composable
expect fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap)
@Composable
private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean) {
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true) }
private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean, close: () -> Unit) {
val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, 0L, true) }
val isCurrentPage = rememberUpdatedState(currentPage)
val play = {
player.play(true)
@@ -154,13 +161,16 @@ private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap,
player.enableSound(true)
snapshotFlow { isCurrentPage.value }
.distinctUntilChanged()
.collect { if (it) play() else stop() }
.collect {
// Do not autoplay on desktop because it needs workaround
if (it && appPlatform.isAndroid) play() else if (!it) stop()
}
}
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
FullScreenVideoView(player, modifier)
FullScreenVideoView(player, modifier, close)
}
}
@Composable
expect fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier)
expect fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit)
@@ -103,11 +103,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
}
fun directChatAction(chatInfo: ChatInfo, chatModel: ChatModel) {
if (chatInfo.ready) {
withBGApi { openChat(chatInfo, chatModel) }
} else {
pendingContactAlertDialog(chatInfo, chatModel)
}
withBGApi { openChat(chatInfo, chatModel) }
}
fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) {
@@ -118,15 +114,28 @@ fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) {
}
}
suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId)
suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) {
val chat = chatModel.controller.apiGetChat(ChatType.Direct, contactId)
if (chat != null) {
chatModel.chatItems.clear()
chatModel.chatItems.addAll(chat.chatItems)
chatModel.chatId.value = chatInfo.id
chatModel.chatId.value = "@$contactId"
}
}
suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId)
if (chat != null) {
openChat(chat, chatModel)
}
}
suspend fun openChat(chat: Chat, chatModel: ChatModel) {
chatModel.chatItems.clear()
chatModel.chatItems.addAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id
}
suspend fun apiLoadPrevMessages(chatInfo: ChatInfo, chatModel: ChatModel, beforeChatItemId: Long, search: String) {
val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT)
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return
@@ -66,6 +66,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
if (chatModel.chatId.value != null) {
ModalManager.end.closeModalsExceptFirst()
}
AudioPlayer.stop()
VideoPlayerHolder.stopAll()
}
}
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
@@ -76,6 +78,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
scaffoldState = scaffoldState,
drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) },
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
drawerGesturesEnabled = appPlatform.isAndroid,
floatingActionButton = {
if (searchInList.isEmpty()) {
FloatingActionButton(
@@ -172,7 +172,9 @@ fun ChatPreviewView(
} else {
when (cInfo) {
is ChatInfo.Direct ->
if (!cInfo.ready) {
if (cInfo.contact.nextSendGrpInv) {
Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary)
} else if (!cInfo.ready) {
Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary)
}
is ChatInfo.Group ->
@@ -334,12 +334,6 @@ private val versionDescriptions: List<VersionDescription> = listOf(
)
)
),
// Also in v5.1
// preference to disable calls per contact
// configurable SOCKS proxy port
// access welcome message via a group profile
// improve calls on lock screen
// better formatting of times and dates
VersionDescription(
version = "v5.1",
post = "https://simplex.chat/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html",
@@ -370,7 +364,7 @@ private val versionDescriptions: List<VersionDescription> = listOf(
descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate,
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat"
)
),
)
),
VersionDescription(
version = "v5.2",
@@ -401,8 +395,42 @@ private val versionDescriptions: List<VersionDescription> = listOf(
titleId = MR.strings.v5_2_more_things,
descrId = MR.strings.v5_2_more_things_descr
)
),
)
)
),
VersionDescription(
version = "v5.3",
post = "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html",
features = listOf(
FeatureDescription(
icon = MR.images.ic_desktop,
titleId = MR.strings.v5_3_new_desktop_app,
descrId = MR.strings.v5_3_new_desktop_app_descr,
link = "https://simplex.chat/downloads/"
),
FeatureDescription(
icon = MR.images.ic_lock,
titleId = MR.strings.v5_3_encrypt_local_files,
descrId = MR.strings.v5_3_encrypt_local_files_descr
),
FeatureDescription(
icon = MR.images.ic_search,
titleId = MR.strings.v5_3_discover_join_groups,
descrId = MR.strings.v5_3_discover_join_groups_descr,
link = "simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"
),
FeatureDescription(
icon = MR.images.ic_theater_comedy,
titleId = MR.strings.v5_3_simpler_incognito_mode,
descrId = MR.strings.v5_3_simpler_incognito_mode_descr
),
FeatureDescription(
icon = MR.images.ic_translate,
titleId = MR.strings.v5_3_new_interface_languages,
descrId = MR.strings.v5_3_new_interface_languages_descr,
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat"
)
)
),
)
private val lastVersion = versionDescriptions.last().version
@@ -164,9 +164,10 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
)
}
SectionItemView {
// can't be higher than 130ms to avoid overflow on 32bit systems
TimeoutSettingRow(
stringResource(MR.strings.network_option_protocol_timeout_per_kb), networkTCPTimeoutPerKb,
listOf(10_000, 20_000, 40_000, 75_000, 100_000), secondsLabel
listOf(15_000, 30_000, 60_000, 90_000, 120_000), secondsLabel
)
}
SectionItemView {
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="accept_contact_button">اقبل</string>
<string name="about_simplex_chat">عن ٍSimpleX </string>
<string name="about_simplex_chat">عن SimpleX Chat</string>
<string name="a_plus_b">a + b</string>
<string name="accept">اقبل</string>
<string name="chat_item_ttl_week">اسبوع 1</string>
@@ -10,7 +10,7 @@
<string name="chat_item_ttl_day">يوم 1</string>
<string name="accept_feature">اقبل</string>
<string name="about_simplex">عن SimpleX</string>
<string name="above_then_preposition_continuation">أعلاه ، ثم:</string>
<string name="above_then_preposition_continuation">أعلاه، ثم:</string>
<string name="accept_call_on_lock_screen">اقبل</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي.</string>
<string name="alert_message_no_group">هذه المجموعة لم تعد موجودة.</string>
@@ -114,7 +114,7 @@
<string name="v4_5_transport_isolation_descr">عن طريق ملف تعريف الدردشة (افتراضي) أو عن طريق الاتصال (تجريبي).</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b> يمكن تعطيله عبر الإعدادات</b> - سيستمر عرض الإشعارات أثناء تشغيل التطبيق.]]></string>
<string name="settings_audio_video_calls">مكالمات الصوت والفيديو</string>
<string name="impossible_to_recover_passphrase"><![CDATA[<b> الرجاء ملاحظة </b>: لن تتمكن من استعادة عبارة المرور أو تغييرها في حالة فقدها.]]></string>
<string name="impossible_to_recover_passphrase"><![CDATA[<b>يُرجى الملاحظة</b>: لن تتمكن من استعادة عبارة المرور أو تغييرها في حالة فقدها.]]></string>
<string name="both_you_and_your_contacts_can_delete">يمكنك أنت وجهة اتصالك حذف الرسائل المرسلة بشكل لا رجعة فيه.</string>
<string name="v4_2_auto_accept_contact_requests">قبول طلبات الاتصال تلقائيًا</string>
<string name="la_auth_failed">فشلت المصادقة</string>
@@ -584,7 +584,7 @@
<string name="large_file">الملف كبير!</string>
<string name="learn_more">معرفة المزيد</string>
<string name="v4_3_irreversible_message_deletion">حذف رسالة لا رجعة فيه</string>
<string name="v4_4_live_messages">رسائل مباشرة</string>
<string name="v4_4_live_messages">رسائل حيّة</string>
<string name="smp_servers_invalid_address">عنوان الخادم غير صالح!</string>
<string name="invalid_migration_confirmation">تأكيد الترحيل غير صالح</string>
<string name="group_member_status_invited">مدعو</string>
@@ -794,7 +794,8 @@
<string name="videos_limit_desc">يمكن إرسال 10 فيديوهات فقط في نفس الوقت</string>
<string name="add_contact">رابط دعوة لمرة واحدة</string>
<string name="network_use_onion_hosts_no">لا</string>
<string name="network_use_onion_hosts_required_desc">سوف تكون مضيفات البصل مطلوبة للاتصال.</string>
<string name="network_use_onion_hosts_required_desc">سوف تكون مضيفات البصل مطلوبة للاتصال.
\nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">سيتم استخدام مضيفات البصل عند توفرها.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">لن يتم استخدام مضيفات البصل.</string>
<string name="self_destruct_new_display_name">اسم عرض جديد:</string>
@@ -881,7 +882,7 @@
<string name="restore_database_alert_title">استعادة النسخة الاحتياطية لقاعدة البيانات؟</string>
<string name="network_options_save">حفظ</string>
<string name="users_delete_with_connections">اتصالات الملف الشخصي والخادم</string>
<string name="prohibit_message_reactions">منع ردود فعل الرسائل.</string>
<string name="prohibit_message_reactions">منع ردود فعل الرسالة.</string>
<string name="prohibit_sending_voice">منع إرسال الرسائل الصوتية.</string>
<string name="prohibit_message_reactions_group">منع ردود فعل الرسائل.</string>
<string name="whats_new_read_more">قراءة المزيد</string>
@@ -1084,7 +1085,7 @@
<string name="notifications_mode_periodic">يبدأ بشكل دوري</string>
<string name="stop_file__confirm">إيقاف</string>
<string name="stop_file__action">إيقاف الملف</string>
<string name="stop_snd_file__title">التوقف عن استلام الملف؟</string>
<string name="stop_snd_file__title">التوقف عن إرسال الملف؟</string>
<string name="icon_descr_address">عنوان SimpleX</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[اضبط <i>استخدم مضيفي .onion</i> إلى \"لا\" إذا كان وكيل SOCKS لا يدعمها.]]></string>
<string name="share_with_contacts">مشاركة مع جهات الاتصال</string>
@@ -1374,4 +1375,28 @@
<string name="privacy_show_last_messages">إظهار الرسائل الأخيرة</string>
<string name="rcv_group_event_n_members_connected">%s، %s و %d أعضاء آخرين متصلون</string>
<string name="rcv_group_event_3_members_connected">%s، %s و %s متصل</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">سيتم تشفير قاعدة البيانات وتخزين عبارة المرور في الإعدادات.</string>
<string name="you_can_change_it_later">يُخزين عبارة المرور العشوائية في الإعدادات كنص عادي.
\nيمكنك تغييره لاحقا.</string>
<string name="database_encryption_will_be_updated_in_settings">سيتم تحديث عبارة مرور تشفير قاعدة البيانات وتخزينها في الإعدادات.</string>
<string name="remove_passphrase_from_settings">هل تريد إزالة عبارة المرور من الإعدادات؟</string>
<string name="use_random_passphrase">استخدم عبارة مرور عشوائية</string>
<string name="save_passphrase_in_settings">حفظ عبارة المرور في الإعدادات</string>
<string name="setup_database_passphrase">إعداد كلمة المرور لقاعدة البيانات</string>
<string name="set_database_passphrase">تعيين عبارة مرور قاعدة البيانات</string>
<string name="open_database_folder">افتح مجلد قاعدة البيانات</string>
<string name="passphrase_will_be_saved_in_settings">سيتم تخزين عبارة المرور في الإعدادات كنص عادي بعد تغييرها أو إعادة تشغيل التطبيق.</string>
<string name="settings_is_storing_in_clear_text">يُخزين عبارة المرور في الإعدادات كنص عادي.</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>يُرجى الملاحظة</b>: يتم توصيل مرحلات الرسائل والملفات عبر وكيل SOCKS. تستخدم المكالمات وإرسال معاينات الارتباط الاتصال المباشر.]]></string>
<string name="encrypt_local_files">تشفير الملفات المحلية</string>
<string name="v5_3_encrypt_local_files">تشفير الملفات والوسائط المخزنة</string>
<string name="v5_3_new_desktop_app">تطبيق سطح المكتب الجديد!</string>
<string name="v5_3_new_interface_languages">6 لغات واجهة جديدة</string>
<string name="v5_3_encrypt_local_files_descr">يقوم التطبيق بتشفير الملفات المحلية الجديدة (باستثناء مقاطع الفيديو).</string>
<string name="v5_3_discover_join_groups">اكتشاف والانضمام إلى المجموعات</string>
<string name="v5_3_new_interface_languages_descr">العربية والبلغارية والفنلندية والعبرية والتايلاندية والأوكرانية - شكرًا للمستخدمين و Weblate.</string>
<string name="v5_3_new_desktop_app_descr">إنشاء ملف تعريف جديد في تطبيق سطح المكتب. 💻</string>
<string name="v5_3_discover_join_groups_descr">- الاتصال بخدمة الدليل (تجريبي)!
\n- إيصالات التسليم (ما يصل إلى 20 عضوا).
\n- أسرع وأكثر استقرارًا.</string>
</resources>
@@ -272,6 +272,7 @@
<string name="this_text_is_available_in_settings">This text is available in settings</string>
<string name="your_chats">Chats</string>
<string name="contact_connection_pending">connecting…</string>
<string name="member_contact_send_direct_message">send direct message</string>
<string name="group_preview_you_are_invited">you are invited to group</string>
<string name="group_preview_join_as">join as %s</string>
<string name="group_connection_pending">connecting…</string>
@@ -304,6 +305,7 @@
<string name="observer_cant_send_message_desc">Please contact group admin.</string>
<string name="files_and_media_prohibited">Files and media prohibited!</string>
<string name="only_owners_can_enable_files_and_media">Only group owners can enable files and media.</string>
<string name="compose_send_direct_message_to_connect">Send direct message to connect</string>
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Image</string>
@@ -1114,6 +1116,7 @@
<string name="rcv_group_event_group_deleted">deleted group</string>
<string name="rcv_group_event_updated_group_profile">updated group profile</string>
<string name="rcv_group_event_invited_via_your_group_link">invited via your group link</string>
<string name="rcv_group_event_member_created_contact">connected directly</string>
<string name="snd_group_event_changed_member_role">you changed role of %s to %s</string>
<string name="snd_group_event_changed_role_for_yourself">you changed role for yourself to %s</string>
<string name="snd_group_event_member_deleted">you removed %1$s</string>
@@ -1124,6 +1127,8 @@
<string name="rcv_group_event_3_members_connected">%s, %s and %s connected</string>
<string name="rcv_group_event_n_members_connected">%s, %s and %d other members connected</string>
<string name="rcv_group_event_open_chat">Open</string>
<!-- Conn event chat items -->
<string name="rcv_conn_event_switch_queue_phase_completed">changed address for you</string>
<string name="rcv_conn_event_switch_queue_phase_changing">changing address…</string>
@@ -1201,6 +1206,8 @@
<string name="error_creating_link_for_group">Error creating group link</string>
<string name="error_updating_link_for_group">Error updating group link</string>
<string name="error_deleting_link_for_group">Error deleting group link</string>
<string name="error_creating_member_contact">Error creating member contact</string>
<string name="error_sending_message_contact_invitation">Sending message contact invitation</string>
<string name="only_group_owners_can_change_prefs">Only group owners can change group preferences.</string>
<string name="address_section_title">Address</string>
<string name="share_address">Share address</string>
@@ -1552,6 +1559,16 @@
<string name="v5_2_disappear_one_message_descr">Even when disabled in the conversation.</string>
<string name="v5_2_more_things">A few more things</string>
<string name="v5_2_more_things_descr">- more stable message delivery.\n- a bit better groups.\n- and more!</string>
<string name="v5_3_new_desktop_app">New desktop app!</string>
<string name="v5_3_new_desktop_app_descr">Create new profile in desktop app. 💻</string>
<string name="v5_3_encrypt_local_files">Encrypt stored files &amp; media</string>
<string name="v5_3_encrypt_local_files_descr">App encrypts new local files (except videos).</string>
<string name="v5_3_discover_join_groups">Discover and join groups</string>
<string name="v5_3_discover_join_groups_descr">- connect to directory service (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable.</string>
<string name="v5_3_simpler_incognito_mode">Simplified incognito mode</string>
<string name="v5_3_simpler_incognito_mode_descr">Toggle incognito when connecting.</string>
<string name="v5_3_new_interface_languages">6 new interface languages</string>
<string name="v5_3_new_interface_languages_descr">Arabic, Bulgarian, Finnish, Hebrew, Thai and Ukrainian - thanks to the users and Weblate.</string>
<!-- CustomTimePicker -->
<string name="custom_time_unit_seconds">seconds</string>
@@ -19,7 +19,7 @@
<string name="add_address_to_your_profile">Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти.</string>
<string name="color_secondary_variant">Допълнителен вторичен</string>
<string name="users_add">Добави профил</string>
<string name="one_time_link_short">1-кратен линк</string>
<string name="one_time_link_short">Еднократен линк</string>
<string name="chat_item_ttl_week">1 седмица</string>
<string name="send_disappearing_message_5_minutes">5 минути</string>
<string name="integrity_msg_skipped">%1$d пропуснато(и) съобщение(я)</string>
@@ -62,7 +62,7 @@
<string name="cannot_access_keychain">Не може да се осъществи достъп до Keystore, за да се запази паролата на базата данни</string>
<string name="cannot_receive_file">Файлът не може да бъде получен</string>
<string name="alert_title_cant_invite_contacts">Не може да поканят контактите!</string>
<string name="settings_notification_preview_title">Визуализация на известието</string>
<string name="settings_notification_preview_title">Визуализация на известията</string>
<string name="notification_preview_mode_message">Текст на съобщението</string>
<string name="notification_preview_new_message">ново съобщение</string>
<string name="group_welcome_preview">Визуализация</string>
@@ -177,7 +177,7 @@
<string name="app_passcode_replaced_with_self_destruct">Кода за достъп до приложение се заменя с код за самоунищожение.</string>
<string name="auto_accept_images">Автоматично приемане на изображения</string>
<string name="authentication_cancelled">Идентификацията е отменена</string>
<string name="send_link_previews">Изпрати визуализация на линка</string>
<string name="send_link_previews">Изпрати визуализация на линковете</string>
<string name="settings_section_title_calls">ОБАЖДАНИЯ</string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore ще се използва за сигурно съхраняване на паролата, след като рестартирате приложението или промените паролата - това ще позволи получаването на известия.</string>
<string name="change_database_passphrase_question">Промяна на паролата на базата данни\?</string>
@@ -327,14 +327,14 @@
<string name="share_text_database_id">ID в базата данни: %d</string>
<string name="receipts_section_contacts">Контакти</string>
<string name="settings_section_title_themes">ТЕМИ</string>
<string name="set_password_to_export_desc">Базата данни е криптирана с произволна парола. Моля, променете я преди експортиране.</string>
<string name="set_password_to_export_desc">Базата данни е криптирана с автоматично генерирана парола. Моля, променете я преди експортиране.</string>
<string name="database_passphrase">Парола за базата данни</string>
<string name="delete_database">Изтрий базата данни</string>
<string name="delete_chat_profile_question">Изтриване на чат профила\?</string>
<string name="delete_files_and_media_question">Изтрий файлове и медия\?</string>
<string name="current_passphrase">Текуща парола…</string>
<string name="database_encrypted">Базата данни е криптирана!</string>
<string name="encrypted_with_random_passphrase">Базата данни е криптирана с произволна парола, можете да я промените.</string>
<string name="encrypted_with_random_passphrase">Базата данни е криптирана с автоматично генерирана парола, можете да я промените.</string>
<string name="database_encryption_will_be_updated">Паролата за крптиране на базата данни ще бъде актуализирана и съхранена в Keystore.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">Базата данни ще бъде криптирана и паролата ще бъде съхранена в Keystore.</string>
<string name="database_passphrase_will_be_updated">Паролата за криптиране на базата данни ще бъде актуализирана.</string>
@@ -752,7 +752,7 @@
<string name="user_unhide">Покажи</string>
<string name="you_can_hide_or_mute_user_profile">Можете да скриете или заглушите потребителски профил - задръжте върху него за менюто.</string>
<string name="incognito">Инкогнито</string>
<string name="incognito_info_protects">Режимът инкогнито защитава вашата поверителност, като използва нов произволен профил за всеки контакт.</string>
<string name="incognito_info_protects">Режимът инкогнито защитава вашата поверителност, като използва нов автоматично генериран профил за всеки контакт.</string>
<string name="incognito_info_allows">Позволява да имате много анонимни връзки без споделени данни между тях в един чат профил .</string>
<string name="v4_5_italian_interface">Италиански интерфейс</string>
<string name="description_via_contact_address_link_incognito">инкогнито чрез линк с адрес за контакт</string>
@@ -808,7 +808,8 @@
<string name="network_use_onion_hosts_no_desc_in_alert">Няма се използват Onion хостове.</string>
<string name="network_use_onion_hosts_required">Задължително</string>
<string name="network_use_onion_hosts_no">Не</string>
<string name="network_use_onion_hosts_required_desc">За свързване ще са необходими Onion хостове.</string>
<string name="network_use_onion_hosts_required_desc">За свързване ще са необходими Onion хостове.
\nМоля, обърнете внимание: няма да можете да се свържете със сървърите без .onion адрес.</string>
<string name="network_use_onion_hosts_prefer_desc">Ще се използват Onion хостове, когато са налични.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Ще се използват Onion хостове, когато са налични.</string>
<string name="network_use_onion_hosts_no_desc">Няма се използват Onion хостове.</string>
@@ -928,7 +929,7 @@
<string name="button_remove_member">Острани член</string>
<string name="member_role_will_be_changed_with_notification">Ролята ще бъде променена на \"%s\". Всички в групата ще бъдат уведомени.</string>
<string name="users_delete_data_only">Само данни за локален профил</string>
<string name="users_delete_with_connections">Profile and server connections</string>
<string name="users_delete_with_connections">Профилни и сървърни връзки</string>
<string name="user_mute">Без звук</string>
<string name="make_profile_private">Направи профила поверителен!</string>
<string name="muted_when_inactive">Без звук при неактивност!</string>
@@ -1032,7 +1033,7 @@
<string name="save_auto_accept_settings">Запази настройките за автоматично приемане</string>
<string name="save_settings_question">Запази настройките\?</string>
<string name="save_profile_password">Запази паролата на профила</string>
<string name="stop_chat_question">Спри чат\?</string>
<string name="stop_chat_question">Спри чата\?</string>
<string name="set_password_to_export">Задай парола за експортиране</string>
<string name="stop_chat_confirmation">Спри</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Спрете чата, за да експортирате, импортирате или изтриете чат базата данни. Няма да можете да получавате и изпращате съобщения, докато чатът е спрян.</string>
@@ -1057,7 +1058,7 @@
<string name="smp_servers">SMP сървъри</string>
<string name="share_address_with_contacts_question">Сподели адреса с контактите\?</string>
<string name="share_link">Сподели линк</string>
<string name="share_with_contacts">Сподели с контакти</string>
<string name="share_with_contacts">Сподели с контактите</string>
<string name="stop_sharing">Спри споделянето</string>
<string name="stop_sharing_address">Спри споделянето на адреса\?</string>
<string name="settings_section_title_settings">НАСТРОЙКИ</string>
@@ -1137,7 +1138,7 @@
<string name="search_verb">Търсене</string>
<string name="sent_message">Изпратено съобщение</string>
<string name="share_verb">Сподели</string>
<string name="auth_stop_chat">Спри чат</string>
<string name="auth_stop_chat">Спри чата</string>
<string name="reveal_verb">Покажи</string>
<string name="stop_file__action">Спри файл</string>
<string name="icon_descr_settings">Настройки</string>
@@ -1146,7 +1147,7 @@
<string name="show_dev_options">Покажи:</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="save_and_notify_contact">Запази и уведоми контакта</string>
<string name="save_and_notify_group_members">Запази и уведоми контактите</string>
<string name="save_and_notify_group_members">Запази и уведоми членовете на групата</string>
<string name="save_preferences_question">Запази настройките\?</string>
<string name="icon_descr_speaker_on">Високоговорителят е включен</string>
<string name="icon_descr_speaker_off">Високоговорителят е изключен</string>
@@ -1287,14 +1288,14 @@
<string name="wrong_passphrase_title">Грешна парола!</string>
<string name="voice_messages">Гласови съобщения</string>
<string name="your_preferences">Вашите настройки</string>
<string name="whats_new">Какво е ново</string>
<string name="whats_new">Какво е новото</string>
<string name="v4_3_irreversible_message_deletion_desc">Вашите контакти могат да позволят пълното изтриване на съобщението.</string>
<string name="update_database_passphrase">Актуализирай паролата на базата данни</string>
<string name="snd_group_event_changed_role_for_yourself">променихте ролята си на %s</string>
<string name="snd_group_event_user_left">вие напуснахте</string>
<string name="snd_group_event_changed_member_role">променихте ролята на %s на %s</string>
<string name="snd_group_event_member_deleted">премахнахте %1$s</string>
<string name="incognito_random_profile">Вашият случаен профил</string>
<string name="incognito_random_profile">Вашият автоматично генериран профил</string>
<string name="user_unmute">Уведомявай</string>
<string name="you_can_share_your_address">Можете да споделите адреса си като линк или QR код - всеки може да се свърже с вас.</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">променихте адреса за %s</string>
@@ -1354,7 +1355,7 @@
<string name="connect_via_link_incognito">Свързване инкогнито</string>
<string name="turn_off_system_restriction_button">Отвори настройките на приложението</string>
<string name="turn_off_battery_optimization_button">Позволи</string>
<string name="connect__a_new_random_profile_will_be_shared">Нов произволен профил ще бъде споделен.</string>
<string name="connect__a_new_random_profile_will_be_shared">Нов автоматично генериран профил ще бъде споделен.</string>
<string name="disable_notifications_button">Деактивирай известията</string>
<string name="system_restricted_background_in_call_title">Без фонови разговори</string>
<string name="connect_via_member_address_alert_title">Свързване директно\?</string>
@@ -1372,5 +1373,31 @@
<string name="rcv_group_event_n_members_connected">%s, %s и %d други членове са свързани</string>
<string name="rcv_group_event_3_members_connected">%s, %s и %s са свързани</string>
<string name="privacy_message_draft">Чернова на съобщение</string>
<string name="privacy_show_last_messages">Показване на последните съобщения</string>
<string name="privacy_show_last_messages">Показване на последните съобщения в листа с чатовете</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Базата данни ще бъде криптирана и паролата ще бъде съхранена в настройките.</string>
<string name="you_can_change_it_later">Автоматично генерирана парола се съхранява в настройките като обикновен текст.
\nМожете да я промените по-късно.</string>
<string name="database_encryption_will_be_updated_in_settings">Паролата за криптиране на базата данни ще бъде актуализирана и съхранена в настройките.</string>
<string name="remove_passphrase_from_settings">Премахване на паролата от настройките\?</string>
<string name="use_random_passphrase">Използвай автоматично генерирана парола</string>
<string name="save_passphrase_in_settings">Запази паролата в настройките</string>
<string name="setup_database_passphrase">Задай парола за базата данни</string>
<string name="set_database_passphrase">Задай парола за базата данни</string>
<string name="open_database_folder">Отвори папката за база данни</string>
<string name="passphrase_will_be_saved_in_settings">Паролата ще бъде съхранена в настройките като обикновен текст, след като я промените или рестартирате приложението.</string>
<string name="settings_is_storing_in_clear_text">Паролата се съхранява в настройките като обикновен текст.</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Моля, обърнете внимание</b>: релетата за съобщения и файлове са свързани чрез SOCKS прокси. Обажданията и изпращането на визуализации на линкове използват директна връзка.]]></string>
<string name="encrypt_local_files">Криптиране на локални файлове</string>
<string name="v5_3_encrypt_local_files">Криптиране на съхранените файлове и медия</string>
<string name="v5_3_new_desktop_app">Ново настолно приложение!</string>
<string name="v5_3_new_interface_languages">6 нови езика на интерфейса</string>
<string name="v5_3_encrypt_local_files_descr">Приложението криптира нови локални файлове (с изключение на видеоклипове).</string>
<string name="v5_3_discover_join_groups">Открийте и се присъединете към групи</string>
<string name="v5_3_simpler_incognito_mode">Опростен режим инкогнито</string>
<string name="v5_3_new_interface_languages_descr">Арабски, български, финландски, иврит, тайландски и украински - благодарение на потребителите и Weblate.</string>
<string name="v5_3_new_desktop_app_descr">Създайте нов профил в настолното приложение. 💻</string>
<string name="v5_3_simpler_incognito_mode_descr">Избор на инкогнито при свързване.</string>
<string name="v5_3_discover_join_groups_descr">- свържете се с директория за услуги (БЕТА)!
\n- потвърждениe за доставка (до 20 члена).
\n- по-бързо и по-стабилно.</string>
</resources>
@@ -1072,14 +1072,14 @@
<string name="enable_lock">Povolit zámek</string>
<string name="lock_after">Zamknout po</string>
<string name="lock_mode">Režim zámku</string>
<string name="authentication_cancelled">Ověření zrušeno</string>
<string name="authentication_cancelled">Autentizace zrušena</string>
<string name="confirm_passcode">Potvrdit heslo</string>
<string name="incorrect_passcode">Nesprávné heslo</string>
<string name="new_passcode">Nové heslo</string>
<string name="submit_passcode">Odeslat</string>
<string name="la_mode_system">Systém</string>
<string name="change_lock_mode">Změnit zamykání</string>
<string name="la_mode_passcode">Heslo</string>
<string name="la_mode_passcode">Přístupový kód</string>
<string name="passcode_changed">Heslo změněno!</string>
<string name="passcode_not_changed">Heslo nezměněno!</string>
<string name="passcode_set">Heslo nastaveno!</string>
@@ -1376,4 +1376,18 @@
<string name="privacy_show_last_messages">Zobrazit poslední zprávy</string>
<string name="send_receipts_disabled_alert_msg">Tato skupina má více než %1$d členů, doručenky nejsou odeslány.</string>
<string name="in_developing_desc">Tato funkce zatím není podporována. Vyzkoušejte další vydání.</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Databáze bude zašifrována a heslo bude uloženo v klíčence.</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Všimněte si prosím</b>: zprávy a relé souborů jsou spojeny prostřednictvím proxy SOCKS. Volání a odesílání náhledů odkazů pomocí přímého připojení.]]></string>
<string name="encrypt_local_files">Šifrovat místní soubory</string>
<string name="you_can_change_it_later">Náhodné heslo je uloženo v nastavení jako prostý text.
\nMůžete jej změnit později.</string>
<string name="database_encryption_will_be_updated_in_settings">Heslo pro šifrování databáze bude aktualizováno a uloženo v klíčence.</string>
<string name="remove_passphrase_from_settings">Odebrat heslo z nastavení\?</string>
<string name="use_random_passphrase">Použít náhodné heslo</string>
<string name="save_passphrase_in_settings">Uložit heslo v nastavení</string>
<string name="setup_database_passphrase">Nastavení hesla databáze</string>
<string name="set_database_passphrase">Nastavit heslo databáze</string>
<string name="open_database_folder">Otevřete složku databáze</string>
<string name="passphrase_will_be_saved_in_settings">Heslo bude uloženo v nastavení jako prostý text až jej změníte nebo po restartu aplikace.</string>
<string name="settings_is_storing_in_clear_text">Heslo je uloženo v nastavení jako prostý text.</string>
</resources>
@@ -405,7 +405,8 @@
<string name="network_use_onion_hosts_required">Erforderlich</string>
<string name="network_use_onion_hosts_prefer_desc">Onion-Hosts werden verwendet, wenn sie verfügbar sind.</string>
<string name="network_use_onion_hosts_no_desc">Onion-Hosts werden nicht verwendet.</string>
<string name="network_use_onion_hosts_required_desc">Für die Verbindung werden Onion-Hosts benötigt.</string>
<string name="network_use_onion_hosts_required_desc">Für die Verbindung werden Onion-Hosts benötigt.
\nBitte beachten Sie: Ohne .onion-Adresse können Sie keine Verbindung mit den Servern herstellen.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion-Hosts werden verwendet, wenn sie verfügbar sind.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion-Hosts werden nicht verwendet.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Für die Verbindung werden Onion-Hosts benötigt.</string>
@@ -1457,4 +1458,18 @@
<string name="rcv_group_event_3_members_connected">%s, %s und %s wurden verbunden</string>
<string name="privacy_message_draft">Nachrichtenentwurf</string>
<string name="privacy_show_last_messages">Letzte Nachrichten anzeigen</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Die Datenbank wird verschlüsselt und das Passwort in den Einstellungen gespeichert.</string>
<string name="you_can_change_it_later">Das zufällige Passwort wird in Klartext in den Einstellungen gespeichert.
\nSie können es später ändern.</string>
<string name="database_encryption_will_be_updated_in_settings">Das Passwort für die Datenbankverschlüsselung wird aktualisiert und in den Einstellungen gespeichert.</string>
<string name="remove_passphrase_from_settings">Passwort aus den Einstellungen entfernen\?</string>
<string name="use_random_passphrase">Zufälliges Passwort verwenden</string>
<string name="save_passphrase_in_settings">Passwort in den Einstellungen sichern</string>
<string name="setup_database_passphrase">Datenbank-Passwort einrichten</string>
<string name="set_database_passphrase">Datenbank-Passwort festlegen</string>
<string name="open_database_folder">Datenbank-Ordner öffnen</string>
<string name="passphrase_will_be_saved_in_settings">Das Passwort wird in Klartext in den Einstellungen gespeichert, nachdem Sie es geändert oder die App neu gestartet haben.</string>
<string name="settings_is_storing_in_clear_text">Das Passwort wurde in Klartext in den Einstellungen gespeichert.</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Bitte beachten Sie</b>: Die Nachrichten- und Dateirelais sind per SOCKS Proxy verbunden. Anrufe und gesendete Link-Vorschaubilder nutzen eine direkte Verbindung.]]></string>
<string name="encrypt_local_files">Lokale Dateien verschlüsseln</string>
</resources>
@@ -1306,7 +1306,7 @@
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">El envío de confirmaciones de entrega se activará para todos los contactos en todos los perfiles visibles.</string>
<string name="receipts_section_contacts">Contactos</string>
<string name="delivery_receipts_title">¡Confirmación de entrega!</string>
<string name="receipts_contacts_disable_keep_overrides">Desactivar (conservar anulaciones)</string>
<string name="receipts_contacts_disable_keep_overrides">Desactivar (conservando anulaciones)</string>
<string name="v5_2_favourites_filter">Encontrar chats mas rápido</string>
<string name="v5_2_disappear_one_message">Escribir un mensaje temporal</string>
<string name="receipts_contacts_override_disabled">El envío de confirmaciones está desactivado para %d contactos</string>
@@ -1351,7 +1351,7 @@
<string name="recipient_colon_delivery_status">%s: %s</string>
<string name="send_receipts_disabled">desactivado</string>
<string name="receipts_groups_disable_for_all">Desactivado para todos los grupos</string>
<string name="receipts_groups_disable_keep_overrides">Desactivar (conservar anulaciones de grupo)</string>
<string name="receipts_groups_disable_keep_overrides">Desactivar (conservando anulaciones de grupo)</string>
<string name="receipts_groups_title_disable">¿Desactivar confirmaciones para grupos\?</string>
<string name="receipts_groups_enable_keep_overrides">Activar (conservar anulaciones de grupo)</string>
<string name="send_receipts_disabled_alert_title">Confirmaciones desactivadas</string>
@@ -1377,4 +1377,16 @@
<string name="rcv_group_event_3_members_connected">%s, %s y %s conectados</string>
<string name="rcv_group_event_2_members_connected">%s y %s conectados</string>
<string name="privacy_message_draft">Borrador de mensaje</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">La base de datos será cifrada y la contraseña se guardará en Configuración</string>
<string name="you_can_change_it_later">La contraseña aleatoria se almacenará en Configuración como texto plano.
\nPuedes cambiarlo más tarde.</string>
<string name="database_encryption_will_be_updated_in_settings">La contraseña para el cifrado de la base de datos se actualizará y almacenará en Configuración</string>
<string name="remove_passphrase_from_settings">Eliminar contraseña de configuración\?</string>
<string name="use_random_passphrase">Usar contraseña aleatoria</string>
<string name="save_passphrase_in_settings">Guardar contraseña en configuración</string>
<string name="setup_database_passphrase">Configuración contraseña base de datos</string>
<string name="set_database_passphrase">Escribe una contraseña para la base de datos</string>
<string name="open_database_folder">Abrir carpeta base de datos</string>
<string name="passphrase_will_be_saved_in_settings">La contraseña se almacenará en configuración como texto plano después de cambiarla o reiniciar la aplicación.</string>
<string name="settings_is_storing_in_clear_text">La contraseña está almacenada en configuración como texto plano.</string>
</resources>
@@ -872,7 +872,8 @@
<string name="network_use_onion_hosts_prefer_desc">Onion-isäntiä käytetään, kun niitä on saatavilla.</string>
<string name="network_use_onion_hosts_no_desc">Onion-isäntiä ei käytetä.</string>
<string name="network_use_onion_hosts_required">Pakollinen</string>
<string name="network_use_onion_hosts_required_desc">Yhteyden muodostamiseen tarvitaan Onion-isäntiä.</string>
<string name="network_use_onion_hosts_required_desc">Yhteyden muodostamiseen tarvitaan Onion-isäntiä.
\nHuomioi: et voi muodostaa yhteyttä palvelimiin ilman .onion-osoitetta.</string>
<string name="callstate_received_answer">vastaus saatu…</string>
<string name="icon_descr_call_rejected">Hylätty puhelu</string>
<string name="v4_6_chinese_spanish_interface_descr">Kiitos käyttäjille – osallistu Weblaten kautta!</string>
@@ -1374,4 +1375,18 @@
<string name="v5_2_message_delivery_receipts_descr">Toinen kuittaus, joka uupui! ✅</string>
<string name="error_synchronizing_connection">Virhe yhteyden synkronoinnissa</string>
<string name="no_history">Ei historiaa</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Tietokanta salataan ja tunnuslause tallennetaan asetuksiin.</string>
<string name="you_can_change_it_later">Satunnainen tunnuslause on tallennettu asetuksiin selkokielisenä.
\nVoit muuttaa sen myöhemmin.</string>
<string name="database_encryption_will_be_updated_in_settings">Tietokannan salaustunnuslause päivitetään ja tallennetaan asetuksiin.</string>
<string name="remove_passphrase_from_settings">Poista tunnuslause asetuksista\?</string>
<string name="use_random_passphrase">Käytä satunnaista tunnuslausetta</string>
<string name="save_passphrase_in_settings">Tallenna tunnuslause asetuksiin</string>
<string name="setup_database_passphrase">Aseta tietokannan tunnuslause</string>
<string name="set_database_passphrase">Aseta tietokannan tunnuslause</string>
<string name="open_database_folder">Avaa tietokantakansio</string>
<string name="passphrase_will_be_saved_in_settings">Tunnuslause tallennetaan asetuksiin selkokielisenä sen jälkeen, kun olet vaihtanut sen tai käynnistänyt sovelluksen uudelleen.</string>
<string name="settings_is_storing_in_clear_text">Tunnuslause on tallennettu asetuksiin selkokielisenä.</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b> Huomioi </b>: Viesti- ja tiedostovälittimet yhdistetään SOCKS-proxyn kautta. Puhelut ja linkin esikatselut käyttävät suoraa yhteyttä.]]></string>
<string name="encrypt_local_files">Salaa paikalliset tiedostot</string>
</resources>
@@ -253,7 +253,7 @@
<string name="icon_descr_server_status_pending">En attente</string>
<string name="accept_connection_request__question">Accepter la demande de connexion \?</string>
<string name="clear_verb">Effacer</string>
<string name="clear_chat_button">Effacer la conversation</string>
<string name="clear_chat_button">Effacer le chat</string>
<string name="connect_via_link">Se connecter via un lien</string>
<string name="clear_verification">Retirer la vérification</string>
<string name="one_time_link">Lien d\'invitation unique</string>
@@ -421,7 +421,8 @@
<string name="update_onion_hosts_settings_question">Mettre à jour le paramètre des hôtes .onion \?</string>
<string name="network_use_onion_hosts_prefer">Quand disponible</string>
<string name="network_use_onion_hosts_no_desc">Les hôtes .onion ne seront pas utilisés.</string>
<string name="network_use_onion_hosts_required_desc">Les hôtes .onion seront nécessaires pour la connexion.</string>
<string name="network_use_onion_hosts_required_desc">Les hôtes .onion seront nécessaires pour la connexion.
\nAttention : vous ne pourrez pas vous connecter aux serveurs sans adresse .onion.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Les hôtes .onion ne seront pas utilisés.</string>
<string name="delete_address__question">Supprimer l\'adresse \?</string>
<string name="all_your_contacts_will_remain_connected">Tous vos contacts resteront connectés.</string>
@@ -1376,4 +1377,30 @@
<string name="rcv_group_event_2_members_connected">%s et %s sont connecté.es</string>
<string name="rcv_group_event_n_members_connected">%s, %s et %d autres membres sont connectés</string>
<string name="rcv_group_event_3_members_connected">%s, %s et %s sont connecté.es</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">La base de données sera chiffrée et la phrase de passe sera stockée dans les paramètres.</string>
<string name="you_can_change_it_later">La phrase secrète aléatoire est stockée en clair dans les paramètres.
\nVous pouvez la modifier ultérieurement.</string>
<string name="database_encryption_will_be_updated_in_settings">La phrase de chiffrement de la base de données sera mise à jour et stockée dans les paramètres.</string>
<string name="remove_passphrase_from_settings">Supprimer la phrase secrète des paramètres \?</string>
<string name="use_random_passphrase">Utiliser une phrase secrète aléatoire</string>
<string name="save_passphrase_in_settings">Enregistrer la phrase secrète dans les paramètres</string>
<string name="setup_database_passphrase">Configurer la phrase secrète de la base de données</string>
<string name="set_database_passphrase">Définir la phrase secrète de la base de données</string>
<string name="open_database_folder">Ouvrir le dossier de la base de données</string>
<string name="passphrase_will_be_saved_in_settings">La phrase secrète sera stockée en clair dans les paramètres après que vous la modifiez ou que vous redémarrez l\'application.</string>
<string name="settings_is_storing_in_clear_text">La phrase secrète est stockée en clair dans les paramètres.</string>
<string name="v5_3_encrypt_local_files">Chiffrement des fichiers et des médias stockés</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Remarque</b> : Les relais de messages et de fichiers sont connectés par le biais d\'un proxy SOCKS. Les appels et l\'envoi d\'aperçus de liens utilisent une connexion directe.]]></string>
<string name="encrypt_local_files">Chiffrer les fichiers locaux</string>
<string name="v5_3_new_desktop_app">Nouvelle application de bureau !</string>
<string name="v5_3_new_interface_languages">6 nouvelles langues d\'interface</string>
<string name="v5_3_encrypt_local_files_descr">L\'application chiffre les nouveaux fichiers locaux (sauf les vidéos).</string>
<string name="v5_3_discover_join_groups">Découvrir et rejoindre des groupes</string>
<string name="v5_3_simpler_incognito_mode">Mode incognito simplifié</string>
<string name="v5_3_new_interface_languages_descr">Arabe, bulgare, finnois, hébreu, thaï et ukrainien - grâce aux utilisateurs et à Weblate.</string>
<string name="v5_3_new_desktop_app_descr">Créer un nouveau profil sur l\'application de bureau. 💻</string>
<string name="v5_3_simpler_incognito_mode_descr">Basculer en mode incognito lors de la connexion.</string>
<string name="v5_3_discover_join_groups_descr">- connexion au service d\'annuaire (BETA) !
\n- accusés de réception (jusqu\'à 20 membres).
\n- plus rapide et plus stable.</string>
</resources>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M422.5-182v-99.5h-280q-22.969 0-40.234-17.266Q85-316.031 85-339v-439q0-22.969 17.266-40.234Q119.531-835.5 142.5-835.5h675q22.969 0 40.234 17.266Q875-800.969 875-778v439q0 22.969-17.266 40.234Q840.469-281.5 817.5-281.5h-280v99.5h57q11.675 0 20.088 8.463Q623-165.074 623-153.325q0 12.325-8.412 20.575-8.413 8.25-20.088 8.25H366q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T366-182h56.5Zm-280-157h675v-439h-675v439Zm0 0v-439 439Z"/></svg>

After

Width:  |  Height:  |  Size: 554 B

@@ -612,7 +612,8 @@
<string name="network_and_servers">Rete e server</string>
<string name="network_settings_title">Impostazioni di rete</string>
<string name="network_use_onion_hosts_no">No</string>
<string name="network_use_onion_hosts_required_desc">Gli host Onion saranno necessari per la connessione.</string>
<string name="network_use_onion_hosts_required_desc">Gli host Onion saranno necessari per la connessione.
\nNota bene: non potrai connetterti ai server senza indirizzo .onion .</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Gli host Onion saranno necessari per la connessione.</string>
<string name="network_use_onion_hosts_prefer_desc">Gli host Onion verranno usati quando disponibili.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Gli host Onion verranno usati quando disponibili.</string>
@@ -1376,4 +1377,30 @@
<string name="rcv_group_event_3_members_connected">%s, %s e %s sono connessi/e</string>
<string name="privacy_message_draft">Bozza</string>
<string name="privacy_show_last_messages">Mostra gli ultimi messaggi</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Il database verrà crittografato e la password conservata nelle impostazioni.</string>
<string name="you_can_change_it_later">La password casuale viene conservata nelle impostazioni come testo normale.
\nPuoi cambiarla dopo.</string>
<string name="database_encryption_will_be_updated_in_settings">La password di crittografia del database verrà aggiornata e conservata nelle impostazioni.</string>
<string name="remove_passphrase_from_settings">Rimuovere la password dalle impostazioni\?</string>
<string name="use_random_passphrase">Usa password casuale</string>
<string name="save_passphrase_in_settings">Salva password nelle impostazioni</string>
<string name="setup_database_passphrase">Configura password del database</string>
<string name="set_database_passphrase">Imposta password del database</string>
<string name="open_database_folder">Apri cartella del database</string>
<string name="passphrase_will_be_saved_in_settings">La password verrà conservata nelle impostazioni come testo normale dopo averla cambiata o il riavvio dell\'app.</string>
<string name="settings_is_storing_in_clear_text">La password viene conservata nelle impostazioni come testo normale.</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Nota bene</b>: i relay di messaggi e file sono connessi via proxy SOCKS. Le chiamate e l\'invio di anteprime dei link usano una connessione diretta.]]></string>
<string name="encrypt_local_files">Cripta i file locali</string>
<string name="v5_3_encrypt_local_files">Crittografia di file e media memorizzati</string>
<string name="v5_3_new_desktop_app">Nuova app desktop!</string>
<string name="v5_3_new_interface_languages">6 nuove lingue dell\'interfaccia</string>
<string name="v5_3_encrypt_local_files_descr">L\'app cripta i nuovi file locali (eccetto i video).</string>
<string name="v5_3_discover_join_groups">Scopri ed unisciti ai gruppi</string>
<string name="v5_3_simpler_incognito_mode">Modalità incognito semplificata</string>
<string name="v5_3_new_interface_languages_descr">Arabo, bulgaro, finlandese, ebraico, tailandese e ucraino - grazie agli utenti e a Weblate.</string>
<string name="v5_3_new_desktop_app_descr">Crea un nuovo profilo nell\'app desktop. 💻</string>
<string name="v5_3_simpler_incognito_mode_descr">Attiva/disattiva l\'incognito quando ti colleghi.</string>
<string name="v5_3_discover_join_groups_descr">- connessione al servizio directory (BETA)!
\n- ricevute di consegna (fino a 20 membri).
\n- più veloce e più stabile.</string>
</resources>
@@ -530,7 +530,8 @@
<string name="how_to_use_simplex_chat">使い方</string>
<string name="markdown_help">マークダウン (書式編集) ガイド</string>
<string name="smp_servers_enter_manually">サーバを手動で入力</string>
<string name="network_use_onion_hosts_required_desc">接続にオニオンのホストが必要となります。</string>
<string name="network_use_onion_hosts_required_desc">接続にオニオンのホストが必要となります。
\n注意: .onion アドレスがないとサーバーに接続できません。</string>
<string name="create_address">アドレスを作成</string>
<string name="delete_address__question">アドレスを削除しますか?</string>
<string name="display_name__field">表示の名前:</string>
@@ -1376,4 +1377,18 @@
<string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string>
<string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string>
<string name="delivery_receipts_are_disabled">Delivery receipts are disabled!</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">データベースは暗号化され、パスフレーズは設定に保存されます。</string>
<string name="you_can_change_it_later">ランダムなパスフレーズは設定に平文として保存されます。
\n後で変更できます。</string>
<string name="database_encryption_will_be_updated_in_settings">データベースの暗号化パスフレーズが更新され、設定に保存されます。</string>
<string name="remove_passphrase_from_settings">設定からパスフレーズを削除しますか?</string>
<string name="use_random_passphrase">ランダムなパスフレーズを使用する</string>
<string name="save_passphrase_in_settings">パスフレーズを設定に保存します</string>
<string name="setup_database_passphrase">データベースのパスフレーズを設定する</string>
<string name="set_database_passphrase">データベースのパスフレーズを設定する</string>
<string name="open_database_folder">データベースフォルダを開く</string>
<string name="passphrase_will_be_saved_in_settings">パスフレーズを変更するかアプリを再起動すると、平文として設定に保存されます。</string>
<string name="settings_is_storing_in_clear_text">パスフレーズは平文として設定に保存されます。</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>注意</b>: メッセージとファイルのリレーは SOCKS プロキシ経由で接続されます。 通話とリンク プレビューの送信には直接接続が使用されます。]]></string>
<string name="encrypt_local_files">ローカルファイルを暗号化する</string>
</resources>
@@ -59,14 +59,14 @@
<string name="v4_2_auto_accept_contact_requests">Contact verzoeken automatisch accepteren</string>
<string name="bold_text">vetgedrukt</string>
<string name="attach">Bijvoegen</string>
<string name="allow_irreversible_message_deletion_only_if">Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contactpersoon dit toestaat.</string>
<string name="allow_irreversible_message_deletion_only_if">Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat.</string>
<string name="allow_to_send_disappearing">Sta toe om verdwijnende berichten te verzenden.</string>
<string name="allow_your_contacts_to_send_voice_messages">Sta toe dat uw contacten spraak berichten verzenden.</string>
<string name="all_your_contacts_will_remain_connected">Al uw contacten blijven verbonden.</string>
<string name="allow_voice_messages_question">Spraak berichten toestaan\?</string>
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Goed voor de batterij</b>. Achtergrondservice controleert berichten elke 10 minuten. Mogelijk mist u oproepen of dringende berichten.]]></string>
<string name="integrity_msg_bad_hash">Onjuiste bericht hash</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scan QR-code</b>: om verbinding te maken met uw contactpersoon die u de QR-code laat zien.]]></string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scan QR-code</b>: om verbinding te maken met uw contact die u de QR-code laat zien.]]></string>
<string name="integrity_msg_bad_id">Onjuiste bericht-ID</string>
<string name="call_already_ended">Oproep al beëindigd!</string>
<string name="chat_item_ttl_month">1 maand</string>
@@ -75,8 +75,8 @@
<string name="above_then_preposition_continuation">hier boven, dan:</string>
<string name="users_delete_all_chats_deleted">Alle gesprekken en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt!</string>
<string name="clear_chat_warning">Alle berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd.</string>
<string name="allow_disappearing_messages_only_if">Sta verdwijnende berichten alleen toe als uw contactpersoon dit toestaat.</string>
<string name="allow_voice_messages_only_if">Sta spraak berichten alleen toe als uw contactpersoon ze toestaat.</string>
<string name="allow_disappearing_messages_only_if">Sta verdwijnende berichten alleen toe als uw contact dit toestaat.</string>
<string name="allow_voice_messages_only_if">Sta spraak berichten alleen toe als uw contact ze toestaat.</string>
<string name="allow_your_contacts_irreversibly_delete">Laat uw contacten verzonden berichten onomkeerbaar verwijderen.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Sta toe dat uw contacten verdwijnende berichten verzenden.</string>
<string name="chat_preferences_always">altijd</string>
@@ -99,9 +99,9 @@
<string name="turning_off_service_and_periodic">Batterijoptimalisatie is actief, waardoor achtergrondservice en periodieke verzoeken om nieuwe berichten worden uitgeschakeld. Je kunt ze weer inschakelen via instellingen.</string>
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Het beste voor de batterij</b>. U ontvangt alleen meldingen wanneer de app wordt uitgevoerd (GEEN achtergrondservice).]]></string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Het kan worden uitgeschakeld via instellingen</b>, meldingen worden nog steeds weergegeven terwijl de app actief is.]]></string>
<string name="both_you_and_your_contacts_can_delete">Zowel jij als je contactpersoon kunnen verzonden berichten onherroepelijk verwijderen.</string>
<string name="both_you_and_your_contact_can_send_disappearing">Zowel jij als je contactpersoon kunnen verdwijnende berichten sturen.</string>
<string name="both_you_and_your_contact_can_send_voice">Zowel jij als je contactpersoon kunnen spraak berichten verzenden.</string>
<string name="both_you_and_your_contacts_can_delete">Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen.</string>
<string name="both_you_and_your_contact_can_send_disappearing">Zowel jij als je contact kunnen verdwijnende berichten sturen.</string>
<string name="both_you_and_your_contact_can_send_voice">Zowel jij als je contact kunnen spraak berichten verzenden.</string>
<string name="impossible_to_recover_passphrase"><![CDATA[<b>Let op</b>: u kunt het wachtwoord NIET herstellen of wijzigen als u het kwijt raakt.]]></string>
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Gebruikt meer batterij</b>! Achtergrondservice wordt altijd uitgevoerd - meldingen worden weergegeven zodra berichten beschikbaar zijn.]]></string>
<string name="icon_descr_cancel_link_preview">link voorbeeld annuleren</string>
@@ -374,7 +374,7 @@
<string name="delete_group_for_self_cannot_undo_warning">De groep wordt voor u verwijderd, dit kan niet ongedaan worden gemaakt!</string>
<string name="hide_notification">Verbergen</string>
<string name="server_error">fout</string>
<string name="file_will_be_received_when_contact_is_online">Het bestand wordt ontvangen wanneer uw contact persoon online is, even geduld a.u.b. of controleer later!</string>
<string name="file_will_be_received_when_contact_is_online">Het bestand wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of controleer later!</string>
<string name="error_saving_file">Fout bij opslaan van bestand</string>
<string name="file_not_found">Bestand niet gevonden</string>
<string name="file_saved">Bestand opgeslagen</string>
@@ -420,7 +420,7 @@
<string name="mark_read">Markeer gelezen</string>
<string name="mark_unread">Markeer als ongelezen</string>
<string name="mute_chat">Dempen</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Als u elkaar niet persoonlijk kunt ontmoeten, kunt u <b> de QR-code scannen in het video gesprek </b>, of uw contactpersoon kan een uitnodiging link delen.]]></string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Als u elkaar niet persoonlijk kunt ontmoeten, kunt u <b> de QR-code scannen in het video gesprek </b>, of uw contact kan een uitnodiging link delen.]]></string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Als je elkaar niet persoonlijk kunt ontmoeten, <b>toon je de QR-code in het video gesprek</b> of deel je de link.]]></string>
<string name="invalid_QR_code">Ongeldige QR-code</string>
<string name="icon_descr_more_button">Meer</string>
@@ -479,7 +479,7 @@
<string name="group_preview_join_as">lid worden als %s</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Het kan gebeuren wanneer:
\n1. De berichten zijn na 2 dagen verlopen bij de verzendende client of na 30 dagen op de server.
\n2. Decodering van het bericht is mislukt, omdat u of uw contactpersoon een oude databaseback-up heeft gebruikt.
\n2. Decodering van het bericht is mislukt, omdat u of uw contact een oude databaseback-up heeft gebruikt.
\n3. De verbinding is verbroken.</string>
<string name="joining_group">Deel nemen aan groep</string>
<string name="leave_group_button">Verlaten</string>
@@ -527,7 +527,7 @@
<string name="rcv_group_event_invited_via_your_group_link">uitgenodigd via je groep link</string>
<string name="incognito">Incognito</string>
<string name="icon_descr_call_missed">Gemiste oproep</string>
<string name="description_via_contact_address_link_incognito">incognito via contact adres link</string>
<string name="description_via_contact_address_link_incognito">incognito via contactadres link</string>
<string name="description_via_group_link_incognito">incognito via groep link</string>
<string name="description_via_one_time_link_incognito">incognito via eenmalige link</string>
<string name="invalid_chat">ongeldige gesprek</string>
@@ -537,7 +537,7 @@
<string name="live">LIVE</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Zorg ervoor dat SMP server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn.</string>
<string name="marked_deleted_description">gemarkeerd als verwijderd</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Controleer of u de juiste link heeft gebruikt of vraag uw contactpersoon om u een andere te sturen.</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Controleer of u de juiste link heeft gebruikt of vraag uw contact om u een andere te sturen.</string>
<string name="image_descr_profile_image">profielfoto</string>
<string name="privacy_redefined">Privacy opnieuw gedefinieerd</string>
<string name="privacy_and_security">Privacy en beveiliging</string>
@@ -563,8 +563,8 @@
<string name="chat_preferences_off">uit</string>
<string name="chat_preferences_on">aan</string>
<string name="only_you_can_send_disappearing">Alleen jij kunt verdwijnende berichten verzenden.</string>
<string name="only_your_contact_can_send_disappearing">Alleen uw contactpersoon kan verdwijnende berichten verzenden.</string>
<string name="only_you_can_delete_messages">Alleen jij kunt berichten onomkeerbaar verwijderen (je contactpersoon kan ze markeren voor verwijdering).</string>
<string name="only_your_contact_can_send_disappearing">Alleen uw contact kan verdwijnende berichten verzenden.</string>
<string name="only_you_can_delete_messages">Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering).</string>
<string name="feature_offered_item_with_param">voorgesteld %s: %2s</string>
<string name="old_database_archive">Oud database archief</string>
<string name="enter_correct_current_passphrase">Voer het juiste huidige wachtwoord in.</string>
@@ -584,7 +584,7 @@
<string name="periodic_notifications_disabled">Periodieke meldingen zijn uitgeschakeld!</string>
<string name="icon_descr_server_status_pending">In behandeling</string>
<string name="only_group_owners_can_enable_voice">Alleen groep eigenaren kunnen spraak berichten inschakelen.</string>
<string name="ask_your_contact_to_enable_voice">Vraag uw contactpersoon om het verzenden van spraak berichten in te schakelen.</string>
<string name="ask_your_contact_to_enable_voice">Vraag uw contact om het verzenden van spraak berichten in te schakelen.</string>
<string name="ok">OK</string>
<string name="network_use_onion_hosts_required_desc">Onion hosts zijn vereist voor verbinding.</string>
<string name="network_use_onion_hosts_prefer_desc">Onion hosts worden gebruikt indien beschikbaar.</string>
@@ -592,9 +592,9 @@
<string name="network_use_onion_hosts_no_desc">Onion hosts worden niet gebruikt.</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol en code. Iedereen kan de servers draaien.</string>
<string name="people_can_connect_only_via_links_you_share">Mensen kunnen alleen verbinding met u maken via de links die u deelt.</string>
<string name="only_your_contact_can_delete">Alleen uw contactpersoon kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</string>
<string name="only_your_contact_can_delete">Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</string>
<string name="only_you_can_send_voice">Alleen jij kunt spraak berichten verzenden.</string>
<string name="only_your_contact_can_send_voice">Alleen uw contactpersoon kan spraak berichten verzenden.</string>
<string name="only_your_contact_can_send_voice">Alleen uw contact kan spraak berichten verzenden.</string>
<string name="prohibit_message_deletion">Verbied het onomkeerbaar verwijderen van berichten.</string>
<string name="feature_offered_item">voorgesteld %s</string>
<string name="store_passphrase_securely_without_recover">Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de gesprekken.</string>
@@ -641,7 +641,7 @@
<string name="simplex_service_notification_title">SimpleX Chat service</string>
<string name="simplex_service_notification_text">Berichten ontvangen…</string>
<string name="notification_preview_mode_message_desc">Toon contact en bericht</string>
<string name="notification_preview_mode_contact_desc">Toon alleen contactpersoon</string>
<string name="notification_preview_mode_contact_desc">Toon alleen contact</string>
<string name="ntf_channel_messages">SimpleX Chat berichten</string>
<string name="auth_simplex_lock_turned_on">SimpleX Vergrendelen ingeschakeld</string>
<string name="auth_stop_chat">Stop chat</string>
@@ -674,28 +674,28 @@
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scannen of plakken vanaf klembord)</string>
<string name="to_connect_via_link_title">Om verbinding te maken via een link</string>
<string name="reject_contact_button">Afwijzen</string>
<string name="set_contact_name">Naam contactpersoon instellen</string>
<string name="set_contact_name">Naam contact instellen</string>
<string name="connection_you_accepted_will_be_cancelled">De door u geaccepteerde verbinding wordt geannuleerd!</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Het contact met wie je deze link hebt gedeeld kan GEEN verbinding maken!</string>
<string name="you_accepted_connection">Je hebt de verbinding geaccepteerd</string>
<string name="you_invited_a_contact">Je hebt je contactpersoon uitgenodigd</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Uw contactpersoon moet online zijn om de verbinding te voltooien.
\nU kunt deze verbinding verbreken en het contact verwijderen (en later proberen met een nieuwe link).</string>
<string name="you_invited_a_contact">Je hebt je contact uitgenodigd</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Uw contact moet online zijn om de verbinding te voltooien.
\nU kunt deze verbinding verbreken en het contact verwijderen en later proberen met een nieuwe link.</string>
<string name="image_descr_qr_code">QR code</string>
<string name="icon_descr_settings">Instellingen</string>
<string name="contact_wants_to_connect_with_you">wil met je in contact komen!</string>
<string name="icon_descr_address">SimpleX Adres</string>
<string name="show_QR_code">Toon QR-code</string>
<string name="image_descr_simplex_logo">SimpleX-Logo</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Je chat profiel wordt verzonden naar uw contactpersoon</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Je chat profiel wordt verzonden naar uw contact</string>
<string name="you_will_be_connected_when_group_host_device_is_online">Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">U wordt verbonden wanneer uw verbindingsverzoek wordt geaccepteerd, even geduld a.u.b. of controleer later!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Je wordt verbonden wanneer het apparaat van je contact online is, even geduld a.u.b. of controleer het later!</string>
<string name="scan_code_from_contacts_app">Scan de beveiligingscode van de app van uw contactpersoon.</string>
<string name="scan_code_from_contacts_app">Scan de beveiligingscode van de app van uw contact.</string>
<string name="security_code">Beveiligingscode</string>
<string name="is_not_verified">%s is niet geverifieerd</string>
<string name="is_verified">%s is geverifieerd</string>
<string name="to_verify_compare">Vergelijk (of scan) de code op uw apparaten om end-to-end codering met uw contactpersoon te verifiëren.</string>
<string name="to_verify_compare">Vergelijk (of scan) de code op uw apparaten om end-to-end codering met uw contact te verifiëren.</string>
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[U kunt ook verbinding maken door op de link te klikken. Als het in de browser wordt geopend, klikt u op de knop <b> Openen in mobiele app </b>.]]></string>
<string name="your_chat_profiles">Uw chat profielen</string>
<string name="your_simplex_contact_address">Uw SimpleX adres</string>
@@ -907,8 +907,8 @@
<string name="chat_preferences_you_allow">Jij staat toe</string>
<string name="you_are_invited_to_group">Je bent uitgenodigd voor de groep</string>
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[U kunt <font color="#0088ff">verbinding maken met SimpleX Chat ontwikkelaars om vragen te stellen en updates te ontvangen</font>.]]></string>
<string name="connection_error_auth_desc">Tenzij uw contactpersoon de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft.
\nOm verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</string>
<string name="connection_error_auth_desc">Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft.
\nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</string>
<string name="update_onion_hosts_settings_question">.onion hosts-instelling updaten\?</string>
<string name="use_simplex_chat_servers__question">SimpleX Chat servers gebruiken\?</string>
<string name="voice_messages_are_prohibited">Spraak berichten zijn verboden in deze groep.</string>
@@ -916,7 +916,7 @@
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten.</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">je hebt het adres gewijzigd voor %s</string>
<string name="snd_group_event_member_deleted">je hebt %1$s verwijderd</string>
<string name="contact_sent_large_file">Je contactpersoon heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%1$s).</string>
<string name="contact_sent_large_file">Je contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%1$s).</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Uw huidige chatdatabase wordt VERWIJDERD en VERVANGEN door de geïmporteerde.
\nDeze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</string>
<string name="invite_prohibited_description">Je probeert een contact met wie je een incognito profiel hebt gedeeld uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt</string>
@@ -937,7 +937,7 @@
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Er wordt geprobeerd verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %1$s).</string>
<string name="unknown_message_format">onbekend berichtformaat</string>
<string name="simplex_link_mode_browser">Via browser</string>
<string name="description_via_contact_address_link">via contact adres link</string>
<string name="description_via_contact_address_link">via contactadres link</string>
<string name="description_via_group_link">via groep link</string>
<string name="description_via_one_time_link">via een eenmalige link</string>
<string name="simplex_link_connection">via %1$s</string>
@@ -1014,8 +1014,8 @@
<string name="confirm_database_upgrades">Bevestig database upgrades</string>
<string name="mtr_error_no_down_migration">database versie is nieuwer dan de app, maar geen down migratie voor: %s</string>
<string name="incompatible_database_version">Incompatibele database versie</string>
<string name="file_will_be_received_when_contact_completes_uploading">Het bestand wordt gedownload wanneer uw contactpersoon het uploaden heeft voltooid.</string>
<string name="image_will_be_received_when_contact_completes_uploading">De afbeelding wordt gedownload wanneer uw contactpersoon het uploaden heeft voltooid.</string>
<string name="file_will_be_received_when_contact_completes_uploading">Het bestand wordt gedownload wanneer uw contact het uploaden heeft voltooid.</string>
<string name="image_will_be_received_when_contact_completes_uploading">De afbeelding wordt gedownload wanneer uw contact het uploaden heeft voltooid.</string>
<string name="show_dev_options">Toon:</string>
<string name="developer_options">Database-ID\'s en Transport isolatie optie.</string>
<string name="hide_dev_options">Verbergen:</string>
@@ -1034,7 +1034,7 @@
<string name="icon_descr_waiting_for_video">Wachten op video</string>
<string name="waiting_for_video">Wachten op video</string>
<string name="video_descr">Video</string>
<string name="video_will_be_received_when_contact_completes_uploading">De video wordt gedownload wanneer uw contactpersoon het uploaden heeft voltooid.</string>
<string name="video_will_be_received_when_contact_completes_uploading">De video wordt gedownload wanneer uw contact het uploaden heeft voltooid.</string>
<string name="error_saving_xftp_servers">Fout bij opslaan van XFTP-servers</string>
<string name="error_loading_xftp_servers">Fout bij het laden van XFTP servers</string>
<string name="error_xftp_test_server_auth">Server vereist autorisatie om te uploaden, wachtwoord controleren</string>
@@ -1104,10 +1104,10 @@
<string name="stop_snd_file__title">Bestand verzenden stoppen\?</string>
<string name="only_your_contact_can_make_calls">Alleen je contact kan bellen.</string>
<string name="revoke_file__message">Het bestand wordt van de servers verwijderd.</string>
<string name="both_you_and_your_contact_can_make_calls">Zowel u als uw contact persoon kunnen bellen.</string>
<string name="both_you_and_your_contact_can_make_calls">Zowel u als uw contact kunnen bellen.</string>
<string name="only_you_can_make_calls">Alleen jij kunt bellen.</string>
<string name="stop_snd_file__message">Het verzenden van het bestand wordt gestopt.</string>
<string name="allow_calls_only_if">Sta oproepen alleen toe als uw contact persoon dit toestaat.</string>
<string name="allow_calls_only_if">Sta oproepen alleen toe als uw contact dit toestaat.</string>
<string name="allow_your_contacts_to_call">Sta toe dat uw contacten u bellen.</string>
<string name="audio_video_calls">Audio/video oproepen</string>
<string name="available_in_v51">"
@@ -1123,7 +1123,7 @@
<string name="auth_open_chat_profiles">Chat profielen openen</string>
<string name="learn_more_about_address">Over SimpleX adres</string>
<string name="learn_more">Kom meer te weten</string>
<string name="scan_qr_to_connect_to_contact">Om verbinding te maken, kan uw contact persoon de QR-code scannen of de link in de app gebruiken.</string>
<string name="scan_qr_to_connect_to_contact">Om verbinding te maken, kan uw contact de QR-code scannen of de link in de app gebruiken.</string>
<string name="customize_theme_title">Thema aanpassen</string>
<string name="group_welcome_preview">Voorbeeld</string>
<string name="you_can_share_this_address_with_your_contacts">U kunt dit adres delen met uw contacten om ze verbinding te laten maken met %s.</string>
@@ -1196,14 +1196,14 @@
<string name="only_you_can_add_message_reactions">Alleen jij kunt berichtreacties toevoegen.</string>
<string name="message_reactions_are_prohibited">Reacties op berichten zijn verboden in deze groep.</string>
<string name="prohibit_message_reactions_group">Berichten reacties verbieden.</string>
<string name="allow_message_reactions_only_if">Sta berichtreacties alleen toe als uw contactpersoon dit toestaat.</string>
<string name="allow_message_reactions_only_if">Sta berichtreacties alleen toe als uw contact dit toestaat.</string>
<string name="allow_your_contacts_adding_message_reactions">Sta uw contactpersonen toe om berichtreacties toe te voegen.</string>
<string name="allow_message_reactions">Sta berichtreacties toe.</string>
<string name="group_members_can_add_message_reactions">Groepsleden kunnen berichtreacties toevoegen.</string>
<string name="both_you_and_your_contact_can_add_message_reactions">Zowel u als uw contactpersoon kunnen berichtreacties toevoegen.</string>
<string name="both_you_and_your_contact_can_add_message_reactions">Zowel u als uw contact kunnen berichtreacties toevoegen.</string>
<string name="message_reactions">Reacties op berichten</string>
<string name="message_reactions_prohibited_in_this_chat">Reacties op berichten zijn verboden in deze chat.</string>
<string name="only_your_contact_can_add_message_reactions">Alleen uw contactpersoon kan berichtreacties toevoegen.</string>
<string name="only_your_contact_can_add_message_reactions">Alleen uw contact kan berichtreacties toevoegen.</string>
<string name="custom_time_unit_days">dagen</string>
<string name="custom_time_unit_hours">uren</string>
<string name="custom_time_unit_minutes">minuten</string>
@@ -1375,4 +1375,30 @@
<string name="privacy_show_last_messages">Laat laatste berichten zien</string>
<string name="rcv_group_event_3_members_connected">%s, %s en %s verbonden</string>
<string name="rcv_group_event_2_members_connected">%s en %s verbonden</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">De database wordt versleuteld en het wachtwoord wordt opgeslagen in de instellingen.</string>
<string name="you_can_change_it_later">Willekeurig wachtwoord wordt in de instellingen opgeslagen als platte tekst.
\nJe kunt het later wijzigen.</string>
<string name="database_encryption_will_be_updated_in_settings">Het wachtwoord voor database versleuteling wordt bijgewerkt en opgeslagen in de instellingen.</string>
<string name="remove_passphrase_from_settings">Wachtwoord uit instellingen verwijderen\?</string>
<string name="use_random_passphrase">Gebruik een willekeurig wachtwoord</string>
<string name="save_passphrase_in_settings">Bewaar het wachtwoord in de instellingen</string>
<string name="setup_database_passphrase">Database wachtwoord instellen</string>
<string name="set_database_passphrase">Database wachtwoord instellen</string>
<string name="open_database_folder">Database map openen</string>
<string name="passphrase_will_be_saved_in_settings">Het wachtwoord wordt als platte tekst in de instellingen opgeslagen nadat u deze hebt gewijzigd of de app opnieuw hebt opgestart.</string>
<string name="settings_is_storing_in_clear_text">Het wachtwoord wordt als leesbare tekst in de instellingen opgeslagen.</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Let op</b>: bericht en bestands relais zijn verbonden via SOCKS-proxy. Voor oproepen en het verzenden van link voorbeelden wordt gebruik gemaakt van een directe verbinding.]]></string>
<string name="encrypt_local_files">Versleutel lokale bestanden</string>
<string name="v5_3_encrypt_local_files">Versleutel opgeslagen bestanden en media</string>
<string name="v5_3_new_desktop_app">Nieuwe desktop app!</string>
<string name="v5_3_new_interface_languages">6 nieuwe interfacetalen</string>
<string name="v5_3_encrypt_local_files_descr">App versleutelt nieuwe lokale bestanden (behalve video\'s)</string>
<string name="v5_3_discover_join_groups">Ontdek en sluit je aan bij groepen</string>
<string name="v5_3_simpler_incognito_mode">Vereenvoudigde incognitomodus</string>
<string name="v5_3_new_interface_languages_descr">Arabisch, Bulgaars, Fins, Hebreeuws, Thais en Oekraïens - dankzij de gebruikers en Weblate.</string>
<string name="v5_3_new_desktop_app_descr">Maak een nieuw profiel in de desktop-app. 💻</string>
<string name="v5_3_simpler_incognito_mode_descr">Schakel incognito in tijdens het verbinden.</string>
<string name="v5_3_discover_join_groups_descr">- maak verbinding met de directoryservice (BETA)!
\n- ontvangst bevestiging (tot 20 leden).
\n- sneller en stabieler.</string>
</resources>
@@ -347,7 +347,8 @@
<string name="network_and_servers">Sieć i serwery</string>
<string name="network_settings_title">Ustawienia sieci</string>
<string name="network_use_onion_hosts_no">Nie</string>
<string name="network_use_onion_hosts_required_desc">Hosty onion będą wymagane do połączenia.</string>
<string name="network_use_onion_hosts_required_desc">Hosty onion będą wymagane do połączenia.
\nUwaga: nie będziesz mógł połączyć się z serwerami bez adresu .onion.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Hosty onion będą wymagane do połączenia.</string>
<string name="network_use_onion_hosts_prefer_desc">Hosty onion będą używane, gdy będą dostępne.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Hosty onion będą używane, gdy będą dostępne.</string>
@@ -1376,4 +1377,30 @@
<string name="rcv_group_event_2_members_connected">%s i %s połączeni</string>
<string name="rcv_group_event_n_members_connected">%s, %s i %d innych członków połączeni</string>
<string name="rcv_group_event_3_members_connected">%s, %s i %s połączeni</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Baza danych zostanie zaszyfrowana, a hasło zapisane w ustawieniach.</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Uwaga</b>: przekaźniki wiadomości i plików są połączone za pośrednictwem serwera proxy SOCKS. Połączenia i wysyłanie podglądów linków korzystają z połączenia bezpośredniego.]]></string>
<string name="encrypt_local_files">Zaszyfruj lokalne pliki</string>
<string name="you_can_change_it_later">Losowe hasło jest trzymane w ustawieniach w czystym tekście.
\nMożesz zmienić je później.</string>
<string name="database_encryption_will_be_updated_in_settings">Hasło szyfrowania bazy danych zostanie zaktualizowane i zapisane w ustawieniach.</string>
<string name="remove_passphrase_from_settings">Usunąć hasło z ustawień\?</string>
<string name="use_random_passphrase">Użyj losowego hasła</string>
<string name="save_passphrase_in_settings">Zapisz hasło w ustawieniach</string>
<string name="setup_database_passphrase">Ustaw hasło bazy danych</string>
<string name="set_database_passphrase">Ustaw hasło bazy danych</string>
<string name="open_database_folder">Otwórz folder bazy danych</string>
<string name="passphrase_will_be_saved_in_settings">Hasło będzie trzymane w ustawieniach jako czysty tekst po tym jak je zmienisz lub zrestartujesz aplikację.</string>
<string name="settings_is_storing_in_clear_text">Hasło jest trzymane w ustawieniach w czystym tekście.</string>
<string name="v5_3_encrypt_local_files">Szyfruj przechowywane pliki i media</string>
<string name="v5_3_new_desktop_app">Nowa aplikacja desktopowa!</string>
<string name="v5_3_new_interface_languages">6 nowych języków interfejsu</string>
<string name="v5_3_encrypt_local_files_descr">Aplikacja szyfruje nowe lokalne pliki (bez filmów).</string>
<string name="v5_3_discover_join_groups">Odkrywaj i dołączaj do grup</string>
<string name="v5_3_simpler_incognito_mode">Uproszczony tryb incognito</string>
<string name="v5_3_new_interface_languages_descr">Arabski, bułgarski, fiński, hebrajski, tajski i ukraiński - dzięki użytkownikom i Weblate.</string>
<string name="v5_3_new_desktop_app_descr">Utwórz nowy profil w aplikacji desktopowej. 💻</string>
<string name="v5_3_simpler_incognito_mode_descr">Przełącz incognito przy połączeniu.</string>
<string name="v5_3_discover_join_groups_descr">- połącz się z usługą katalogową (BETA)!
\n- potwierdzenia dostaw (do 20 członków).
\n- szybszy i stabilniejszy.</string>
</resources>
@@ -343,14 +343,14 @@
<string name="one_time_link">Одноразовая ссылка</string>
<!-- settings - SettingsView.kt -->
<string name="your_settings">Настройки</string>
<string name="your_simplex_contact_address">Ваш адрес SimpleX</string>
<string name="database_passphrase_and_export">Пароль и экспорт базы</string>
<string name="your_simplex_contact_address">Ваш SimpleX адрес</string>
<string name="database_passphrase_and_export">База данных</string>
<string name="about_simplex_chat">Информация о SimpleX Chat</string>
<string name="how_to_use_simplex_chat">Как использовать</string>
<string name="markdown_help">Форматирование сообщений</string>
<string name="markdown_in_messages">Форматирование сообщений</string>
<string name="chat_with_the_founder">Отправьте вопросы и идеи</string>
<string name="send_us_an_email">Отправить email</string>
<string name="chat_with_the_founder">Вопросы и предложения</string>
<string name="send_us_an_email">Написать нам письмо</string>
<string name="chat_lock">Блокировка SimpleX</string>
<string name="chat_console">Консоль</string>
<string name="smp_servers">SMP серверы</string>
@@ -403,7 +403,8 @@
<string name="network_use_onion_hosts_required">Обязательно</string>
<string name="network_use_onion_hosts_prefer_desc">Onion хосты используются, если возможно.</string>
<string name="network_use_onion_hosts_no_desc">Onion хосты не используются.</string>
<string name="network_use_onion_hosts_required_desc">Подключаться только к onion хостам.</string>
<string name="network_use_onion_hosts_required_desc">Подключаться только к onion хостам.
\nОбратите внимание: Вы не сможете соединиться с серверами, у которых нет .onion адреса.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion хосты используются, если возможно.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion хосты не используются.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Подключаться только к onion хостам.</string>
@@ -862,11 +863,11 @@
<string name="chat_preferences_always">всегда</string>
<string name="chat_preferences_on">да</string>
<string name="chat_preferences_off">нет</string>
<string name="chat_preferences">Предпочтения</string>
<string name="chat_preferences">Настройки чатов</string>
<string name="contact_preferences">Предпочтения контакта</string>
<string name="group_preferences">Предпочтения группы</string>
<string name="set_group_preferences">Предпочтения группы</string>
<string name="your_preferences">Ваши предпочтения</string>
<string name="your_preferences">Настройки чатов</string>
<string name="direct_messages">Прямые сообщения</string>
<string name="full_deletion">Удаление для всех</string>
<string name="voice_messages">Голосовые сообщения</string>
@@ -958,7 +959,7 @@
<string name="allow_disappearing_messages_only_if">Разрешить исчезающие сообщения, только если Ваш контакт разрешает их Вам.</string>
<string name="prohibit_sending_disappearing">Запретить посылать исчезающие сообщения.</string>
<string name="group_members_can_send_disappearing">Члены группы могут посылать исчезающие сообщения.</string>
<string name="whats_new">Новые функции</string>
<string name="whats_new">Что нового</string>
<string name="new_in_version">Новое в %s</string>
<string name="v4_2_security_assessment">Аудит безопасности</string>
<string name="v4_2_security_assessment_desc">Безопасность SimpleX Chat была проверена Trail of Bits.</string>
@@ -1000,7 +1001,7 @@
<string name="users_delete_data_only">Только локальные данные профиля</string>
<string name="messages_section_title">Сообщения</string>
<string name="smp_servers_per_user">Серверы для новых соединений Вашего текущего профиля чата</string>
<string name="your_chat_profiles">Ваши профили чата</string>
<string name="your_chat_profiles">Ваши профили</string>
<string name="users_delete_all_chats_deleted">Все чаты и сообщения будут удалены - это нельзя отменить!</string>
<string name="app_version_code">Сборка приложения: %s</string>
<string name="app_version_name">Версия приложения: v%s</string>
@@ -1049,7 +1050,7 @@
<string name="smp_save_servers_question">Сохранить серверы\?</string>
<string name="should_be_at_least_one_profile">Должен быть хотя бы один профиль пользователя.</string>
<string name="should_be_at_least_one_visible_profile">Должен быть хотя бы один открытый профиль пользователя.</string>
<string name="to_reveal_profile_enter_password">Чтобы показать Ваш скрытый профиль, введите пароль в поле поиска на странице Ваши профили чата.</string>
<string name="to_reveal_profile_enter_password">Чтобы показать Ваш скрытый профиль, введите пароль в поле поиска на странице Ваши профили.</string>
<string name="user_unmute">Уведомлять</string>
<string name="group_welcome_title">Приветственное сообщение</string>
<string name="confirm_password">Подтвердить пароль</string>
@@ -1082,10 +1083,10 @@
<string name="v4_6_audio_video_calls_descr">Поддержка bluetooth и другие улучшения.</string>
<string name="save_welcome_message_question">Сохранить приветственное сообщение\?</string>
<string name="v4_6_group_welcome_message_descr">Установить сообщение для новых членов группы!</string>
<string name="tap_to_activate_profile">Нажмите, чтобы сделать профиль активным.</string>
<string name="v4_6_chinese_spanish_interface_descr">Благодаря пользователям – добавьте переводы через Weblate!</string>
<string name="tap_to_activate_profile">Нажмите на профиль, чтобы переключиться на него.</string>
<string name="v4_6_chinese_spanish_interface_descr">Благодаря пользователям - добавьте переводы через Weblate!</string>
<string name="you_will_still_receive_calls_and_ntfs">Вы все равно получите звонки и уведомления в профилях без звука, когда они активные.</string>
<string name="you_can_hide_or_mute_user_profile">Вы можете скрыть профиль или выключить уведомления - подержите, чтобы увидеть меню.</string>
<string name="you_can_hide_or_mute_user_profile">Вы можете скрыть или отключить уведомления профиля - нажмите и удерживайте профиль, чтобы открыть меню.</string>
<string name="image_will_be_received_when_contact_completes_uploading">Изображение будет принято когда Ваш контакт его загрузит.</string>
<string name="file_will_be_received_when_contact_completes_uploading">Файл будет принят когда Ваш контакт загрузит его.</string>
<string name="database_upgrade">Обновление базы данных</string>
@@ -1362,7 +1363,7 @@
<string name="receipts_section_description">Установки для Вашего активного профиля</string>
<string name="receipts_contacts_override_disabled">Отправка отчётов о доставке выключена для %d контактов.</string>
<string name="sync_connection_force_desc">Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения!</string>
<string name="v5_2_message_delivery_receipts_descr">Вторая галочка - знать, что доставлено! ✅</string>
<string name="v5_2_message_delivery_receipts_descr">Вторая галочка, когда сообщение доставлено! ✅</string>
<string name="you_can_enable_delivery_receipts_later_alert">Вы можете включить их позже в настройках Конфиденциальности.</string>
<string name="error_aborting_address_change">Ошибка при прекращении изменения адреса</string>
<string name="abort_switch_receiving_address_confirm">Прекратить</string>
@@ -1396,7 +1397,7 @@
<string name="fix_connection_not_supported_by_contact">Починка не поддерживается контактом.</string>
<string name="fix_connection_not_supported_by_group_member">Починка не поддерживается членом группы.</string>
<string name="renegotiate_encryption">Пересогласовать шифрование</string>
<string name="v5_2_favourites_filter">Быстро найти чаты</string>
<string name="v5_2_favourites_filter">Быстрый поиск чатов</string>
<string name="v5_2_message_delivery_receipts">Отчеты о доставке сообщений!</string>
<string name="v5_2_more_things">Еще несколько изменений</string>
<string name="delivery_receipts_title">Отчёты о доставке!</string>
@@ -1458,4 +1459,30 @@
<string name="connect_use_current_profile">Использовать активный профиль</string>
<string name="connect_use_new_incognito_profile">Использовать новый Инкогнито профиль</string>
<string name="system_restricted_background_in_call_warn"><![CDATA[Чтобы совершать звонки в фоне, выберите <b>Расход батареи приложением</b> / <b>Без ограничений</b> в настройках приложения.]]></string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">База данных будет зашифрована, и пароль сохранен в настройках.</string>
<string name="v5_3_encrypt_local_files">Шифруйте сохраненные файлы и медиа</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Обратите внимание</b>: соединение с серверами файлов и сообщений устанавливаются через SOCKS прокси. Звонки и картинки ссылок используют прямое соединение.]]></string>
<string name="encrypt_local_files">Шифровать локальные файлы</string>
<string name="v5_3_new_desktop_app">Приложение для компьютера!</string>
<string name="v5_3_new_interface_languages">6 новых языков интерфейса</string>
<string name="v5_3_encrypt_local_files_descr">Приложение шифрует новые локальные файлы (кроме видео).</string>
<string name="you_can_change_it_later">Случайный пароль хранится в настройках как открытый текст.
\nВы можете изменить его позже.</string>
<string name="v5_3_discover_join_groups">Найдите и вступите в группы</string>
<string name="database_encryption_will_be_updated_in_settings">Пароль шифрования базы данных будет обновлён и сохранён в настройках.</string>
<string name="remove_passphrase_from_settings">Удалить пароль из настроек\?</string>
<string name="use_random_passphrase">Использовать случайный пароль</string>
<string name="save_passphrase_in_settings">Сохранить пароль в настройках</string>
<string name="v5_3_simpler_incognito_mode">Упрощенный режим Инкогнито</string>
<string name="setup_database_passphrase">Установить пароль базы данных</string>
<string name="set_database_passphrase">Установить пароль базы данных</string>
<string name="open_database_folder">Открыть директорию базы данных</string>
<string name="v5_3_new_interface_languages_descr">Арабский, болгарский, финский, иврит, тайский и украинский - благодаря пользователям и Weblate.</string>
<string name="v5_3_new_desktop_app_descr">Создайте новый профиль в приложении для компьютера. 💻</string>
<string name="passphrase_will_be_saved_in_settings">Пароль будет сохранён в настройках как простой текст после того, как вы его измените или перезапустите приложение.</string>
<string name="v5_3_simpler_incognito_mode_descr">Установите режим Инкогнито при соединении.</string>
<string name="v5_3_discover_join_groups_descr">- соединиться с каталогом групп (BETA)!
\n- отчеты о доставке (до 20 членов).
\n- быстрее и стабильнее.</string>
<string name="settings_is_storing_in_clear_text">Пароль хранится в настройках, как открытый текст.</string>
</resources>
@@ -67,7 +67,7 @@
<string name="connect_via_group_link">通过群组链接连接?</string>
<string name="connect_via_link_or_qr">通过群组链接/二维码连接</string>
<string name="always_use_relay">总是通过中继连接</string>
<string name="allow_your_contacts_irreversibly_delete">允许您的联系人不可撤回地删除已发送消息。</string>
<string name="allow_your_contacts_irreversibly_delete">允许您的联系人永久删除已发送消息。</string>
<string name="chat_preferences_contact_allows">联系人允许</string>
<string name="allow_voice_messages_only_if">仅有您的联系人许可后才允许语音消息。</string>
<string name="group_info_member_you">您: %1$s</string>
@@ -98,7 +98,7 @@
<string name="app_version_title">应用程序版本</string>
<string name="full_backup">应用程序数据备份</string>
<string name="settings_section_title_icon">应用程序图标</string>
<string name="app_version_name">应用程序版本:v%s</string>
<string name="app_version_name">应用版本:v%s</string>
<string name="notifications_mode_off_desc">应用程序仅在运行时可以接受通知,没有后台服务会被启动</string>
<string name="auth_unavailable">身份验证不可用</string>
<string name="auto_accept_images">自动接受图像</string>
@@ -106,7 +106,7 @@
<string name="icon_descr_audio_call">语音通话</string>
<string name="audio_call_no_encryption">语音通话(非端到端加密)</string>
<string name="v4_2_auto_accept_contact_requests">自动接受联系人请求</string>
<string name="integrity_msg_bad_hash">错误消息散列</string>
<string name="integrity_msg_bad_hash">消息散列值错误</string>
<string name="integrity_msg_bad_id">错误消息 ID</string>
<string name="settings_audio_video_calls">语音和视频通话</string>
<string name="turning_off_service_and_periodic">启用电池优化,关闭了后台服务和对新消息的定期请求。您可以在设置里重新启用它们。</string>
@@ -122,7 +122,7 @@
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b> 最长续航 </b>。您只会在应用程序运行时收到通知(无后台服务)。]]></string>
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> 较长续航 </b>。后台服务每 10 分钟检查一次消息。您可能会错过来电或者紧急信息。]]></string>
<string name="bold_text">加粗</string>
<string name="both_you_and_your_contacts_can_delete">您和您的联系人都可以不可逆转地删除已发送的消息。</string>
<string name="both_you_and_your_contacts_can_delete">您和您的联系人都可以永久删除已发送的消息。</string>
<string name="both_you_and_your_contact_can_send_disappearing">您和您的联系人都可以发送限时消息。</string>
<string name="both_you_and_your_contact_can_send_voice">您和您的联系人都可以发送语音消息。</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b> 可以在设置里禁用它 </b> - 应用程序运行时仍会显示通知。]]></string>
@@ -190,7 +190,7 @@
<string name="group_invitation_tap_to_join_incognito">点击以加入隐身聊天</string>
<string name="group_main_profile_sent">您的聊天资料将被发送给群组成员</string>
<string name="invite_prohibited_description">您正在尝试邀请与您共享隐身个人资料的联系人加入您使用主要个人资料的群组</string>
<string name="incognito_info_protects">隐身模式可以保护你的主要个人资料名称和图像的隐私——对于每个新的联系人,都会创建一个新的随机个人资料。</string>
<string name="incognito_info_protects">隐身模式通过为每个联系人使用新的随机配置文件来保护您的隐私。</string>
<string name="alert_title_cant_invite_contacts_descr">您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人</string>
<string name="description_via_one_time_link_incognito">通过一次性链接隐身</string>
<string name="only_group_owners_can_enable_voice">只有群主可以启用语音信息。</string>
@@ -285,7 +285,7 @@
<string name="incoming_video_call">视频通话来电</string>
<string name="no_call_on_lock_screen">禁用</string>
<string name="status_e2e_encrypted">端到端加密</string>
<string name="status_contact_has_e2e_encryption">联系人具有端到端加密</string>
<string name="status_contact_has_e2e_encryption">联系人已开启端到端加密</string>
<string name="allow_accepting_calls_from_lock_screen">通过设置启用在锁定屏幕上通话。</string>
<string name="icon_descr_call_connecting">连接通话中</string>
<string name="status_contact_has_no_e2e_encryption">联系人没有端到端加密</string>
@@ -531,7 +531,7 @@
<string name="enter_passphrase_notification_desc">要接收通知,请输入数据库密码</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[为了保护您的隐私,该应用程序没有推送通知,而是具有 <b>SimpleX 后台服务 </b>——它每天使用百分之几的电池。]]></string>
<string name="your_settings">您的设置</string>
<string name="turn_off_battery_optimization"><![CDATA[为了使用它,请 <b>禁用电池优化</b>为SimpleX在下一个对话框。否则通知将被禁用。]]></string>
<string name="turn_off_battery_optimization"><![CDATA[要使用它,请在下一个对话框中<b>允许 SimpleX 在后台运行</b>。 否则,通知将被禁用。]]></string>
<string name="settings_notification_preview_title">通知预览</string>
<string name="enter_passphrase_notification_title">需要密码</string>
<string name="periodic_notifications_disabled">定期通知被禁用!</string>
@@ -669,7 +669,8 @@
<string name="no_contacts_selected">未选择联系人</string>
<string name="one_time_link">一次性邀请链接</string>
<string name="feature_off">关闭</string>
<string name="network_use_onion_hosts_required_desc">连接需要 Onion 主机。</string>
<string name="network_use_onion_hosts_required_desc">连接需要 Onion 主机。
\n请注意:如果没有 .onion 地址,您将无法连接到服务器。</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion 主机将在可用时使用。</string>
<string name="chat_item_ttl_none">从不</string>
<string name="feature_offered_item">已提供 %s</string>
@@ -1107,7 +1108,7 @@
<string name="revoke_file__confirm">撤销</string>
<string name="audio_video_calls">音频/视频通话</string>
<string name="available_in_v51">"
\n在 v5.1 中可用"</string>
\n在 v5.1 版本中可用"</string>
<string name="v5_0_app_passcode">应用程序密码</string>
<string name="v5_0_polish_interface">波兰语界面</string>
<string name="v5_0_polish_interface_descr">感谢用户——通过 Weblate 做出贡献!</string>
@@ -1119,7 +1120,7 @@
<string name="only_you_can_make_calls">只有您可以拨打电话。</string>
<string name="only_your_contact_can_make_calls">只有您的联系人可以拨打电话。</string>
<string name="allow_your_contacts_to_call">允许您的联系人与您进行语音通话。</string>
<string name="allow_calls_only_if">仅当您的联系人许可时才允许呼叫。</string>
<string name="allow_calls_only_if">仅当您的联系人允许时才允许呼叫。</string>
<string name="calls_prohibited_with_this_contact">禁止音频/视频通话。</string>
<string name="send_disappearing_message_1_minute">1分钟</string>
<string name="one_time_link_short">一次性链接</string>
@@ -1292,10 +1293,114 @@
<string name="connect__a_new_random_profile_will_be_shared">一个新的随机个人档案将被分享。</string>
<string name="snd_conn_event_ratchet_sync_started">与 %s 协调加密中…</string>
<string name="in_developing_desc">该功能还没支持。请尝试下一个版本。</string>
<string name="turn_off_battery_optimization_button">确认</string>
<string name="turn_off_battery_optimization_button">允许</string>
<string name="connect_via_link_incognito">隐身连接</string>
<string name="connect_via_member_address_alert_title">确认发起私聊?</string>
<string name="delivery">发送</string>
<string name="recipient_colon_delivery_status">%s: %s</string>
<string name="in_developing_title">敬请期待!</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">数据库将被加密,密码将存储在设置中。</string>
<string name="you_can_enable_delivery_receipts_later">您可以稍后在“设置”中启用它</string>
<string name="receipts_groups_disable_for_all">对所有群组关闭</string>
<string name="no_info_on_delivery">无送货信息</string>
<string name="connect__your_profile_will_be_shared">您的个人资料 %1$s 将被共享。</string>
<string name="sending_delivery_receipts_will_be_enabled">将为所有联系人启用送达回执功能。</string>
<string name="turn_off_system_restriction_button">打开应用程序设置</string>
<string name="receipts_groups_enable_for_all">为所有组启用</string>
<string name="rcv_group_event_n_members_connected">%s、%s 和 %d 其他成员已连接</string>
<string name="receipts_contacts_override_enabled">已为 %d 联系人启用送达回执功能</string>
<string name="snd_conn_event_ratchet_sync_agreed">已同意 %s 的加密</string>
<string name="conn_event_ratchet_sync_allowed">允许重新协商加密</string>
<string name="delivery_receipts_title">送达回执!</string>
<string name="connect_use_new_incognito_profile">使用新的隐身个人资料</string>
<string name="v5_2_favourites_filter_descr">过滤未读和收藏的聊天记录。</string>
<string name="rcv_conn_event_verification_code_reset">已更改安全密码</string>
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">将为所有可见聊天配置文件中的所有联系人启用送达回执功能。</string>
<string name="system_restricted_background_in_call_warn"><![CDATA[要在后台拨打电话,请在应用设置中选择<b>应用电池使用情况</b> / <b>无限制</b>。]]></string>
<string name="sender_at_ts">%s 在 %s</string>
<string name="receipts_contacts_title_disable">禁用回执?</string>
<string name="sync_connection_force_question">重新协商加密?</string>
<string name="receipts_section_description_1">可以在联系人和群组设置中覆盖它们。</string>
<string name="receipts_contacts_disable_for_all">对所有联系人关闭</string>
<string name="you_can_change_it_later">随机密码以明文形式存储在设置中。
\n您可以稍后更改。</string>
<string name="receipts_groups_override_disabled">已禁用 %d 组的送达回执功能</string>
<string name="snd_conn_event_ratchet_sync_required">需要为 %s 重新协商加密</string>
<string name="system_restricted_background_desc">SimpleX 无法在后台运行。只有在应用程序运行时,您才会收到通知。</string>
<string name="receipts_contacts_enable_keep_overrides">启用(保留覆盖)</string>
<string name="database_encryption_will_be_updated_in_settings">即将更新数据库加密密码并将其存储在设置中。</string>
<string name="connect_use_current_profile">使用当前配置文件</string>
<string name="remove_passphrase_from_settings">从设置中删除密码?</string>
<string name="conn_event_ratchet_sync_agreed">同意加密</string>
<string name="receipts_contacts_title_enable">启用回执?</string>
<string name="system_restricted_background_in_call_desc">程序在后台运行 1 分钟后可能会关闭。</string>
<string name="privacy_message_draft">留言草稿</string>
<string name="v5_2_disappear_one_message_descr">即使在对话中禁用。</string>
<string name="use_random_passphrase">使用随机密码</string>
<string name="system_restricted_background_in_call_title">无后台通话</string>
<string name="you_can_enable_delivery_receipts_later_alert">您可以稍后通过应用程序隐私和安全设置启用它们。</string>
<string name="save_passphrase_in_settings">在设置中保存密码</string>
<string name="enable_receipts_all">启用</string>
<string name="send_receipts_disabled_alert_msg">该群组成员超过 %1$d ,未发送送达回执。</string>
<string name="fix_connection_question">修复连接?</string>
<string name="v5_2_message_delivery_receipts_descr">我们错过的第二个\"√\"!✅</string>
<string name="setup_database_passphrase">设定数据库密码</string>
<string name="receipts_groups_title_disable">为群组禁用回执吗?</string>
<string name="rcv_group_event_3_members_connected">%s、%s 和 %d 已连接</string>
<string name="fix_connection_not_supported_by_group_member">修复群组成员不支持的问题</string>
<string name="receipts_groups_override_enabled">已为 %d 组启用送达回执功能</string>
<string name="sync_connection_force_confirm">重新协商</string>
<string name="receipts_contacts_disable_keep_overrides">禁用(保留覆盖)</string>
<string name="set_database_passphrase">设置数据库密码</string>
<string name="receipts_contacts_override_disabled">已禁用 %d 联系人的送达回执功能</string>
<string name="receipts_groups_enable_keep_overrides">启用(保留组覆盖)</string>
<string name="system_restricted_background_warn"><![CDATA[要启用通知,请在应用设置中选择<b>应用电池使用情况</b> / <b>无限制</b>。]]></string>
<string name="send_receipts_disabled_alert_title">送达回执已禁用</string>
<string name="open_database_folder">打开数据库文件夹</string>
<string name="no_history">无历史记录</string>
<string name="fix_connection_confirm">修复</string>
<string name="fix_connection">修复连接</string>
<string name="rcv_group_event_2_members_connected">%s 和 %s 已连接</string>
<string name="send_receipts_disabled">关闭</string>
<string name="receipts_section_groups">小群组(最多 20 人)</string>
<string name="privacy_show_last_messages">显示最近的消息</string>
<string name="settings_section_title_delivery_receipts">将送达回执发送给</string>
<string name="error_enabling_delivery_receipts">启用已读回执时出错!</string>
<string name="passphrase_will_be_saved_in_settings">更改密码或重启应用后,密码将以明文形式保存在设置中。</string>
<string name="paste_the_link_you_received_to_connect_with_your_contact">粘贴您收到的链接以与您的联系人联系…</string>
<string name="send_receipts">送达回执</string>
<string name="no_selected_chat">没有选择聊天</string>
<string name="conn_event_ratchet_sync_ok">可以加密</string>
<string name="renegotiate_encryption">重新协商加密</string>
<string name="receipts_groups_disable_keep_overrides">禁用(保留组覆盖)</string>
<string name="receipts_groups_title_enable">为群组启用回执吗?</string>
<string name="fix_connection_not_supported_by_contact">修复联系人不支持的问题</string>
<string name="snd_conn_event_ratchet_sync_ok">对 %s 加密正常</string>
<string name="v5_2_fix_encryption_descr">修复还原备份后的加密问题。</string>
<string name="sync_connection_force_desc">加密正在运行,不需要新的加密协议。此操作可能会导致连接错误!</string>
<string name="disable_notifications_button">禁用通知</string>
<string name="in_reply_to">回复</string>
<string name="dont_enable_receipts">不启用</string>
<string name="connect_via_member_address_alert_desc">连接请求将发送给该组成员。</string>
<string name="settings_is_storing_in_clear_text">密码以明文形式存储在设置中。</string>
<string name="error_synchronizing_connection">同步连接时出错</string>
<string name="receipts_section_description">这些设置适用于您当前的配置文件</string>
<string name="snd_conn_event_ratchet_sync_allowed">允许为 %s 重新协商加密</string>
<string name="receipts_contacts_enable_for_all">为所有人启用</string>
<string name="conn_event_ratchet_sync_required">需要重新协商加密</string>
<string name="delivery_receipts_are_disabled">已关闭送达回执!</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>请注意</b>:消息和文件中继通过 SOCKS 代理连接。呼叫和发送链接预览使用直接连接。]]></string>
<string name="encrypt_local_files">加密本地文件</string>
<string name="v5_3_encrypt_local_files">为存储的文件和媒体加密</string>
<string name="v5_3_new_desktop_app">全新桌面应用!</string>
<string name="v5_3_new_interface_languages">6种全新的界面语言</string>
<string name="v5_3_encrypt_local_files_descr">应用程序为新的本地文件(视频除外)加密。</string>
<string name="v5_3_discover_join_groups">发现和加入群组</string>
<string name="v5_3_simpler_incognito_mode">简化的隐身模式</string>
<string name="v5_3_new_interface_languages_descr">阿拉伯语、保加利亚语、芬兰语、希伯莱语、泰国语和乌克兰语——得益于用户和Weblate。</string>
<string name="v5_3_new_desktop_app_descr">在桌面应用里创建新的账号。💻</string>
<string name="v5_3_simpler_incognito_mode_descr">在连接时切换隐身模式。</string>
<string name="v5_3_discover_join_groups_descr">- 连接到目录服务(BETA)!
\n- 发送回执(至多20名成员)。
\n- 更快,更稳定。</string>
</resources>
@@ -29,6 +29,10 @@ fun initApp() {
//testCrypto()
}
fun discoverVlcLibs(path: String) {
uk.co.caprica.vlcj.binding.LibC.INSTANCE.setenv("VLC_PLUGIN_PATH", path, 1)
}
private fun applyAppLocale() {
val lang = ChatController.appPrefs.appLanguage.get()
if (lang == null || lang == Locale.getDefault().language) return
@@ -21,6 +21,8 @@ actual val agentDatabaseFileName: String = "simplex_v1_agent.db"
actual val databaseExportDir: File = tmpDir
val vlcDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex-vlc").also { it.deleteOnExit() }
actual fun desktopOpenDatabaseDir() {
if (Desktop.isDesktopSupported()) {
try {
@@ -33,6 +33,7 @@ import kotlin.text.substring
@Composable
actual fun PlatformTextField(
composeState: MutableState<ComposeState>,
sendMsgEnabled: Boolean,
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
@@ -42,6 +43,7 @@ actual fun PlatformTextField(
) {
val cs = composeState.value
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboard = LocalSoftwareKeyboardController.current
val padding = PaddingValues(12.dp, 12.dp, 45.dp, 0.dp)
LaunchedEffect(cs.contextItem) {
@@ -51,6 +53,13 @@ actual fun PlatformTextField(
delay(50)
keyboard?.show()
}
LaunchedEffect(sendMsgEnabled) {
if (!sendMsgEnabled) {
focusManager.clearFocus()
delay(50)
keyboard?.hide()
}
}
val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) }
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) }
val textFieldValue = textFieldValueState.copy(text = cs.message)
@@ -113,7 +122,8 @@ actual fun PlatformTextField(
}
}
}
}
},
)
showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress
if (composeState.value.preview is ComposePreview.VoicePreview) {
@@ -1,9 +1,17 @@
package chat.simplex.common.platform
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import chat.simplex.common.model.*
import chat.simplex.common.views.usersettings.showInDevelopingAlert
import kotlinx.coroutines.CoroutineScope
import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import kotlinx.coroutines.*
import uk.co.caprica.vlcj.player.base.MediaPlayer
import uk.co.caprica.vlcj.player.base.State
import uk.co.caprica.vlcj.player.component.AudioPlayerComponent
import java.io.File
import kotlin.math.max
actual class RecorderNative: RecorderInterface {
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
@@ -18,36 +26,187 @@ actual class RecorderNative: RecorderInterface {
}
actual object AudioPlayer: AudioPlayerInterface {
override fun play(fileSource: CryptoFile, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean) {
showInDevelopingAlert()
val player by lazy { AudioPlayerComponent().mediaPlayer() }
// Filepath: String, onProgressUpdate
private val currentlyPlaying: MutableState<Pair<CryptoFile, (position: Int?, state: TrackState) -> Unit>?> = mutableStateOf(null)
private var progressJob: Job? = null
enum class TrackState {
PLAYING, PAUSED, REPLACED
}
// Returns real duration of the track
private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? {
val absoluteFilePath = getAppFilePath(fileSource.filePath)
if (!File(absoluteFilePath).exists()) {
Log.e(TAG, "No such file: ${fileSource.filePath}")
return null
}
VideoPlayerHolder.stopAll()
RecorderInterface.stopRecording?.invoke()
val current = currentlyPlaying.value
if (current == null || current.first != fileSource) {
stopListener()
player.stop()
runCatching {
if (fileSource.cryptoArgs != null) {
val tmpFile = fileSource.createTmpFileIfNeeded()
decryptCryptoFile(absoluteFilePath, fileSource.cryptoArgs, tmpFile.absolutePath)
player.media().prepare("file://${tmpFile.absolutePath}")
} else {
player.media().prepare("file://$absoluteFilePath")
}
}.onFailure {
Log.e(TAG, it.stackTraceToString())
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message)
return null
}
}
if (seek != null) player.seekTo(seek)
player.start()
currentlyPlaying.value = fileSource to onProgressUpdate
progressJob = CoroutineScope(Dispatchers.Default).launch {
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
while(isActive && (player.isPlaying || player.status().state() == State.OPENING)) {
// Even when current position is equal to duration, the player has isPlaying == true for some time,
// so help to make the playback stopped in UI immediately
if (player.currentPosition == player.duration) {
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
break
}
delay(50)
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
}
onProgressUpdate(null, TrackState.PAUSED)
currentlyPlaying.value?.first?.deleteTmpFile()
}
return player.duration
}
private fun pause(): Int {
progressJob?.cancel()
progressJob = null
val position = player.currentPosition
player.pause()
return position
}
override fun stop() {
/*LALAL*/
if (currentlyPlaying.value == null) return
player.stop()
stopListener()
}
override fun stop(item: ChatItem) {
/*LALAL*/
}
override fun stop(item: ChatItem) = stop(item.file?.fileName)
// FileName or filePath are ok
override fun stop(fileName: String?) {
TODO("Not yet implemented")
if (fileName != null && currentlyPlaying.value?.first?.filePath?.endsWith(fileName) == true) {
stop()
}
}
private fun stopListener() {
val afterCoroutineCancel: CompletionHandler = {
// Notify prev audio listener about stop
currentlyPlaying.value?.second?.invoke(null, TrackState.REPLACED)
currentlyPlaying.value?.first?.deleteTmpFile()
currentlyPlaying.value = null
}
/** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be:
* [TrackState.PLAYING] -> [TrackState.PAUSED] -> [TrackState.REPLACED] (in this order)
* */
if (progressJob != null) {
progressJob?.invokeOnCompletion(afterCoroutineCancel)
} else {
afterCoroutineCancel(null)
}
progressJob?.cancel()
progressJob = null
}
override fun play(
fileSource: CryptoFile,
audioPlaying: MutableState<Boolean>,
progress: MutableState<Int>,
duration: MutableState<Int>,
resetOnEnd: Boolean,
) {
if (progress.value == duration.value) {
progress.value = 0
}
val realDuration = start(fileSource, progress.value) { pro, state ->
if (pro != null) {
progress.value = pro
}
if (pro == null || pro == duration.value) {
audioPlaying.value = false
if (pro == duration.value) {
progress.value = if (resetOnEnd) 0 else duration.value
} else if (state == TrackState.REPLACED) {
progress.value = 0
}
}
}
audioPlaying.value = realDuration != null
// Update to real duration instead of what was received in ChatInfo
realDuration?.let { duration.value = it }
}
override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) {
TODO("Not yet implemented")
pro.value = pause()
audioPlaying.value = false
}
override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) {
/*LALAL*/
pro.value = ms
if (currentlyPlaying.value?.first?.filePath == filePath) {
player.seekTo(ms)
}
}
override fun duration(unencryptedFilePath: String): Int? {
/*LALAL*/
return null
var res: Int? = null
try {
val helperPlayer = AudioPlayerComponent().mediaPlayer()
helperPlayer.media().startPaused("file://$unencryptedFilePath")
res = helperPlayer.duration
helperPlayer.stop()
helperPlayer.release()
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
}
return res
}
}
val MediaPlayer.isPlaying: Boolean
get() = status().isPlaying
fun MediaPlayer.seekTo(time: Int) {
controls().setTime(time.toLong())
}
fun MediaPlayer.start() {
controls().start()
}
fun MediaPlayer.pause() {
controls().pause()
}
fun MediaPlayer.stop() {
controls().stop()
}
private val MediaPlayer.currentPosition: Int
get() = max(0, status().time().toInt())
val MediaPlayer.duration: Int
get() = media().info().duration().toInt()
actual object SoundPlayer: SoundPlayerInterface {
override fun start(scope: CoroutineScope, sound: Boolean) { /*LALAL*/ }
override fun stop() { /*LALAL*/ }
@@ -3,51 +3,213 @@ package chat.simplex.common.platform
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.ImageBitmap
import chat.simplex.common.views.usersettings.showInDevelopingAlert
import androidx.compose.ui.graphics.toComposeImageBitmap
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
import uk.co.caprica.vlcj.player.base.MediaPlayer
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent
import java.awt.Component
import java.io.File
import java.net.URI
import kotlin.math.max
actual class VideoPlayer: VideoPlayerInterface {
actual companion object {
actual fun getOrCreate(
uri: URI,
gallery: Boolean,
defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayer = VideoPlayer().also {
it.preview.value = defaultPreview
it.duration.value = defaultDuration
it.soundEnabled.value = soundEnabled
}
actual fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean { /*TODO*/ return false }
actual fun release(uri: URI, gallery: Boolean, remove: Boolean) { /*TODO*/ }
actual fun stopAll() { /*LALAL*/ }
actual fun releaseAll() { /*LALAL*/ }
}
actual class VideoPlayer actual constructor(
override val uri: URI,
override val gallery: Boolean,
private val defaultPreview: ImageBitmap,
defaultDuration: Long,
soundEnabled: Boolean
): VideoPlayerInterface {
override val soundEnabled: MutableState<Boolean> = mutableStateOf(false)
override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
override val progress: MutableState<Long> = mutableStateOf(0L)
override val duration: MutableState<Long> = mutableStateOf(0L)
override val preview: MutableState<ImageBitmap> = mutableStateOf(ImageBitmap(0, 0))
override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview)
val mediaPlayerComponent = initializeMediaPlayerComponent()
val player by lazy { mediaPlayerComponent.mediaPlayer() }
init {
withBGApi {
setPreviewAndDuration()
}
}
private val currentVolume: Int by lazy { player.audio().volume() }
private var isReleased: Boolean = false
private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null)
private var progressJob: Job? = null
enum class TrackState {
PLAYING, PAUSED, STOPPED
}
private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean {
val filepath = getAppFilePath(uri)
if (filepath == null || !File(filepath).exists()) {
Log.e(TAG, "No such file: $uri")
brokenVideo.value = true
return false
}
if (soundEnabled.value) {
RecorderInterface.stopRecording?.invoke()
}
AudioPlayer.stop()
VideoPlayerHolder.stopAll()
val playerFilePath = uri.toString().replaceFirst("file:", "file://")
if (listener.value == null) {
runCatching {
player.media().prepare(playerFilePath)
if (seek != null) {
player.seekTo(seek.toInt())
}
}.onFailure {
Log.e(TAG, it.stackTraceToString())
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message)
brokenVideo.value = true
return false
}
}
player.start()
if (seek != null) player.seekTo(seek.toInt())
if (!player.isPlaying) {
// Can happen when video file is broken
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error))
brokenVideo.value = true
return false
}
listener.value = onProgressUpdate
// Player can only be accessed in one specific thread
progressJob = CoroutineScope(Dispatchers.Main).launch {
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
while (isActive && !isReleased && player.isPlaying) {
// Even when current position is equal to duration, the player has isPlaying == true for some time,
// so help to make the playback stopped in UI immediately
if (player.currentPosition == player.duration) {
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
break
}
delay(50)
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
}
if (isActive && !isReleased) {
onProgressUpdate(player.currentPosition.toLong(), TrackState.PAUSED)
}
onProgressUpdate(null, TrackState.PAUSED)
}
return true
}
override fun stop() {
/*TODO*/
if (isReleased || !videoPlaying.value) return
player.controls().stop()
stopListener()
}
private fun stopListener() {
val afterCoroutineCancel: CompletionHandler = {
// Notify prev video listener about stop
listener.value?.invoke(null, TrackState.STOPPED)
}
/** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be:
* [TrackState.PLAYING] -> [TrackState.PAUSED] -> [TrackState.STOPPED] (in this order)
* */
if (progressJob != null) {
progressJob?.invokeOnCompletion(afterCoroutineCancel)
} else {
afterCoroutineCancel(null)
}
progressJob?.cancel()
progressJob = null
}
override fun play(resetOnEnd: Boolean) {
if (appPlatform.isDesktop) {
showInDevelopingAlert()
if (progress.value == duration.value) {
progress.value = 0
}
videoPlaying.value = start(progress.value) { pro, _ ->
if (pro != null) {
progress.value = pro
}
if ((pro == null || pro == duration.value) && duration.value != 0L) {
videoPlaying.value = false
if (pro == duration.value) {
progress.value = if (resetOnEnd) 0 else duration.value
}/* else if (state == TrackState.STOPPED) {
progress.value = 0 //
}*/
}
}
}
override fun enableSound(enable: Boolean): Boolean {
/*TODO*/
return false
if (isReleased) return false
if (soundEnabled.value == enable) return false
soundEnabled.value = enable
player.audio().setVolume(if (enable) currentVolume else 0)
return true
}
override fun release(remove: Boolean) {
/*TODO*/
override fun release(remove: Boolean) { withApi {
if (isReleased) return@withApi
isReleased = true
// TODO
/** [player.release] freezes thread for some reason. It happens periodically. So doing this we don't see the freeze, but it's still there */
if (player.isPlaying) player.stop()
CoroutineScope(Dispatchers.IO).launch { player.release() }
if (remove) {
VideoPlayerHolder.players.remove(uri to gallery)
}
}}
private val MediaPlayer.currentPosition: Int
get() = if (isReleased) 0 else max(0, player.status().time().toInt())
private suspend fun setPreviewAndDuration() {
// It freezes main thread, doing it in IO thread
CoroutineScope(Dispatchers.IO).launch {
val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo() }
withContext(Dispatchers.Main) {
preview.value = previewAndDuration.preview ?: defaultPreview
duration.value = (previewAndDuration.duration ?: 0)
}
}
}
private suspend fun getBitmapFromVideo(): VideoPlayerInterface.PreviewAndDuration {
val player = CallbackMediaPlayerComponent().mediaPlayer()
val filepath = getAppFilePath(uri)
if (filepath == null || !File(filepath).exists()) {
return VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L)
}
player.media().startPaused(filepath)
val start = System.currentTimeMillis()
while (player.snapshots()?.get() == null && start + 5000 > System.currentTimeMillis()) {
delay(10)
}
val preview = player.snapshots()?.get()?.toComposeImageBitmap()
val duration = player.duration.toLong()
CoroutineScope(Dispatchers.IO).launch { player.release() }
return VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration)
}
private fun initializeMediaPlayerComponent(): Component {
return if (desktopPlatform.isMac()) {
CallbackMediaPlayerComponent()
} else {
EmbeddedMediaPlayerComponent()
}
}
private fun Component.mediaPlayer() = when (this) {
is CallbackMediaPlayerComponent -> mediaPlayer()
is EmbeddedMediaPlayerComponent -> mediaPlayer()
else -> error("mediaPlayer() can only be called on vlcj player components")
}
}
@@ -6,9 +6,7 @@ import androidx.compose.ui.unit.Dp
import chat.simplex.common.platform.VideoPlayer
@Composable
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {
/* LALAL */
}
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {}
@Composable
actual fun LocalWindowWidth(): Dp {
@@ -1,14 +1,23 @@
package chat.simplex.common.views.chat.item
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
import chat.simplex.common.platform.VideoPlayer
import androidx.compose.ui.unit.dp
import chat.simplex.common.platform.*
import chat.simplex.common.simplexWindowState
import chat.simplex.common.views.helpers.getBitmapFromByteArray
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlin.math.max
@Composable
actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) {
@@ -20,6 +29,43 @@ actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap:
)
}
@Composable
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier) {
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) {
// Workaround. Without changing size of the window the screen flashes a lot even if it's not being recomposed
LaunchedEffect(Unit) {
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = simplexWindowState.windowState.size.width + 1.dp)
delay(50)
player.play(true)
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = simplexWindowState.windowState.size.width - 1.dp)
}
Box {
Box(Modifier.fillMaxSize().padding(bottom = 50.dp)) {
val factory = remember { { player.mediaPlayerComponent } }
SwingPanel(
background = Color.Transparent,
modifier = Modifier,
factory = factory
)
}
Controls(player, close)
}
}
@Composable
private fun BoxScope.Controls(player: VideoPlayer, close: () -> Unit) {
val playing = remember(player) { player.videoPlaying }
val progress = remember(player) { player.progress }
val duration = remember(player) { player.duration }
Row(Modifier.fillMaxWidth().align(Alignment.BottomCenter).height(50.dp)) {
IconButton(onClick = { if (playing.value) player.player.pause() else player.play(true) },) {
Icon(painterResource(if (playing.value) MR.images.ic_pause_filled else MR.images.ic_play_arrow_filled), null, Modifier.size(30.dp), tint = MaterialTheme.colors.primary)
}
Slider(
value = progress.value.toFloat() / max(0.0001f, duration.value.toFloat()),
onValueChange = { player.player.seekTo((it * duration.value).toInt()) },
modifier = Modifier.fillMaxWidth().weight(1f)
)
IconButton(onClick = close,) {
Icon(painterResource(MR.images.ic_close), null, Modifier.size(30.dp), tint = MaterialTheme.colors.primary)
}
}
}
@@ -133,7 +133,6 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean)
}
actual fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration {
// LALAL
return VideoPlayerInterface.PreviewAndDuration(preview = null, timestamp = 0L, duration = 0L)
}