From c81fa7e6b0e4d47f0d23686b7b738e4ee2832a4b Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:50:30 +0700 Subject: [PATCH 01/24] android, desktop: marking chat read fix (#5658) * android, desktop: marking chat read fix * comments --- apps/ios/SimpleXChat/ChatTypes.swift | 1 + .../kotlin/chat/simplex/common/model/ChatModel.kt | 3 ++- .../chat/simplex/common/views/chat/ChatItemsLoader.kt | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 60d5082584..554207e90d 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1612,6 +1612,7 @@ public struct ChatStats: Decodable, Hashable { // actual only via getChats() and getChat(.initial), otherwise, zero public var reportsCount: Int = 0 public var minUnreadItemId: Int64 = 0 + // actual only via getChats(), otherwise, false public var unreadChat: Bool = false } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 04fd6b380d..656937641c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1233,10 +1233,11 @@ data class Chat( @Serializable data class ChatStats( val unreadCount: Int = 0, - // actual only via getChats() and getChat(.initial), otherwise, zero val unreadMentions: Int = 0, + // actual only via getChats() and getChat(.initial), otherwise, zero val reportsCount: Int = 0, val minUnreadItemId: Long = 0, + // actual only via getChats(), otherwise, false val unreadChat: Boolean = false ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt index 3613ceaa7c..6a8c277ae1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt @@ -54,11 +54,13 @@ suspend fun processLoadedChat( if (contentTag == null) { // update main chats, not content tagged withChats { - if (getChat(chat.id) == null) { + val oldChat = getChat(chat.id) + if (oldChat == null) { addChat(chat) } else { updateChatInfo(chat.remoteHostId, chat.chatInfo) - updateChatStats(chat.remoteHostId, chat.id, chat.chatStats) + // unreadChat is currently not actual in getChat query (always false) + updateChatStats(chat.remoteHostId, chat.id, chat.chatStats.copy(unreadChat = oldChat.chatStats.unreadChat)) } } } From 4f00f9efa0c0b991fb4bcaf95e233ba7d0e8387b Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:12:12 +0700 Subject: [PATCH 02/24] android, desktop: correct width of quoted message with link view (#5659) --- .../kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt index 3ce3bfe6da..de4e9fe281 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layoutId import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -19,6 +20,7 @@ import chat.simplex.common.model.LinkPreview import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.chatViewScrollState +import chat.simplex.common.views.chat.item.CHAT_IMAGE_LAYOUT_ID import chat.simplex.res.MR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -133,7 +135,7 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel @Composable fun ChatItemLinkView(linkPreview: LinkPreview, showMenu: State, onLongClick: () -> Unit) { - Column(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { + Column(Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID).widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } Image( base64ToBitmap(linkPreview.image), From b96b6c70d2fac550851e8f4c3b1b2657edee5b01 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:28:46 +0700 Subject: [PATCH 03/24] android, desktop: fix showing big gifs (opens in viewer) (#5660) --- .../chat/simplex/common/views/chat/item/CIImageView.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 401d098bea..0863c28007 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -8,6 +8,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale @@ -122,7 +123,7 @@ fun CIImageView( // IllegalStateException: Recording currently in progress - missing #endRecording() call? // but can display 5000px image. Using even lower value here just to feel safer. // It happens to WebP because it's not compressed while sending since it can be animated. - if (painter.intrinsicSize.width <= 4320 && painter.intrinsicSize.height <= 4320) { + if (painter.intrinsicSize != Size.Unspecified && painter.intrinsicSize.width <= 4320 && painter.intrinsicSize.height <= 4320) { Image( painter, contentDescription = stringResource(MR.strings.image_descr), @@ -140,7 +141,7 @@ fun CIImageView( ) } else { Box(Modifier - .width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH) + .width(if (painter.intrinsicSize != Size.Unspecified && painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH) .combinedClickable( onLongClick = { showMenu.value = true }, onClick = {} From c972298dd213b32b41798100be96655257652c31 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:31:41 +0700 Subject: [PATCH 04/24] ui: drop filter when no such tags exist (#5662) * ui: drop filter when no such tags exist * ios * android/desktop * change * change * ios * change * change * ios * android * ios --- apps/ios/Shared/Model/ChatModel.swift | 21 ++++++++++------ .../chat/simplex/common/model/ChatModel.kt | 24 ++++++++++++------- .../common/views/chatlist/TagListView.kt | 5 ++-- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 0be8436320..9171bf5073 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -123,11 +123,9 @@ class ChatTagsModel: ObservableObject { } } } - if case let .presetTag(tag) = tm.activeFilter, (newPresetTags[tag] ?? 0) == 0 { - activeFilter = nil - } presetTags = newPresetTags unreadTags = newUnreadTags + clearActiveChatFilterIfNeeded() } func updateChatFavorite(favorite: Bool, wasFavorite: Bool) { @@ -136,9 +134,7 @@ class ChatTagsModel: ObservableObject { presetTags[.favorites] = (count ?? 0) + 1 } else if !favorite && wasFavorite, let count { presetTags[.favorites] = max(0, count - 1) - if case .presetTag(.favorites) = activeFilter, (presetTags[.favorites] ?? 0) == 0 { - activeFilter = nil - } + clearActiveChatFilterIfNeeded() } } @@ -162,6 +158,7 @@ class ChatTagsModel: ObservableObject { } } } + clearActiveChatFilterIfNeeded() } func markChatTagRead(_ chat: Chat) -> Void { @@ -192,7 +189,17 @@ class ChatTagsModel: ObservableObject { func changeGroupReportsTag(_ by: Int = 0) { if by == 0 { return } - presetTags[.groupReports] = (presetTags[.groupReports] ?? 0) + by + presetTags[.groupReports] = max(0, (presetTags[.groupReports] ?? 0) + by) + clearActiveChatFilterIfNeeded() + } + + func clearActiveChatFilterIfNeeded() { + let clear = switch activeFilter { + case let .presetTag(tag): (presetTags[tag] ?? 0) == 0 + case let .userTag(tag): !userTags.contains(tag) + case .unread, nil: false + } + if clear { activeFilter = nil } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 656937641c..6424397531 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -247,15 +247,12 @@ object ChatModel { } } - if (activeChatTagFilter.value is ActiveFilter.PresetTag && - (newPresetTags[(activeChatTagFilter.value as ActiveFilter.PresetTag).tag] ?: 0) == 0) { - activeChatTagFilter.value = null - } - presetTags.clear() presetTags.putAll(newPresetTags) unreadTags.clear() unreadTags.putAll(newUnreadTags) + + clearActiveChatFilterIfNeeded() } fun updateChatFavorite(favorite: Boolean, wasFavorite: Boolean) { @@ -265,9 +262,7 @@ object ChatModel { presetTags[PresetTagKind.FAVORITES] = (count ?: 0) + 1 } else if (!favorite && wasFavorite && count != null) { presetTags[PresetTagKind.FAVORITES] = maxOf(0, count - 1) - if (activeChatTagFilter.value == ActiveFilter.PresetTag(PresetTagKind.FAVORITES) && (presetTags[PresetTagKind.FAVORITES] ?: 0) == 0) { - activeChatTagFilter.value = null - } + clearActiveChatFilterIfNeeded() } } @@ -288,6 +283,7 @@ object ChatModel { } } } + clearActiveChatFilterIfNeeded() } fun moveChatTagUnread(chat: Chat, oldTags: List?, newTags: List) { @@ -875,10 +871,20 @@ object ChatModel { private fun changeGroupReportsTagNoContentTag(by: Int = 0) { if (by == 0 || contentTag != null) return - presetTags[PresetTagKind.GROUP_REPORTS] = (presetTags[PresetTagKind.GROUP_REPORTS] ?: 0) + by + presetTags[PresetTagKind.GROUP_REPORTS] = kotlin.math.max(0, (presetTags[PresetTagKind.GROUP_REPORTS] ?: 0) + by) + clearActiveChatFilterIfNeeded() } } + fun clearActiveChatFilterIfNeeded() { + val clear = when(val f = activeChatTagFilter.value) { + is ActiveFilter.PresetTag -> (presetTags[f.tag] ?: 0) == 0 + is ActiveFilter.UserTag -> userTags.value.none { it.chatTagId == f.tag.chatTagId } + is ActiveFilter.Unread, null -> false + } + if (clear) activeChatTagFilter.value = null + } + fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) { val current = currentUser.value ?: return val updated = current.copy( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt index 1b563e6d02..6c88162f8a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt @@ -31,6 +31,7 @@ import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.apiDeleteChatTag import chat.simplex.common.model.ChatController.apiSetChatTags import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatModel.clearActiveChatFilterIfNeeded import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.platform.* @@ -447,9 +448,7 @@ private fun deleteTag(rhId: Long?, tag: ChatTag, saving: MutableState) val tagId = tag.chatTagId if (apiDeleteChatTag(rhId, tagId)) { chatModel.userTags.value = chatModel.userTags.value.filter { it.chatTagId != tagId } - if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) { - chatModel.activeChatTagFilter.value = null - } + clearActiveChatFilterIfNeeded() chatModel.chats.value.forEach { c -> when (val cInfo = c.chatInfo) { is ChatInfo.Direct -> { From b494c43706eafe3b7a253b659670d711e395e1be Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 24 Feb 2025 18:34:27 +0000 Subject: [PATCH 05/24] website: translations (#5664) * Translated using Weblate (Dutch) Currently translated at 100.0% (258 of 258 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/nl/ * Translated using Weblate (Dutch) Currently translated at 100.0% (258 of 258 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/nl/ --------- Co-authored-by: M1K4 --- website/langs/nl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/langs/nl.json b/website/langs/nl.json index c725cc3d3e..4db6126c2c 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -255,5 +255,6 @@ "docs-dropdown-10": "Transparantie", "docs-dropdown-11": "FAQ", "docs-dropdown-12": "Beveiliging", - "hero-overlay-card-3-p-3": "Trail of Bits heeft in juli 2024 het cryptografische ontwerp van SimpleX-netwerkprotocollen beoordeeld. Lees meer." + "hero-overlay-card-3-p-3": "Trail of Bits heeft in juli 2024 het cryptografische ontwerp van SimpleX-netwerkprotocollen beoordeeld. Lees meer.", + "docs-dropdown-14": "SimpleX voor bedrijven" } From a5334b36f8c12afaa6a03c8974f1f32af93505a2 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 24 Feb 2025 20:23:20 +0000 Subject: [PATCH 06/24] ui: translations (#5663) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (German) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (German) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/ * Translated using Weblate (Italian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Czech) Currently translated at 92.0% (2130 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Polish) Currently translated at 90.3% (1850 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pl/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Persian) Currently translated at 77.2% (1788 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/ * Translated using Weblate (Romanian) Currently translated at 34.5% (799 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Catalan) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ca/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Turkish) Currently translated at 90.8% (2102 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Vietnamese) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2314 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (French) Currently translated at 99.0% (2292 of 2314 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Italian) Currently translated at 99.8% (2323 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Catalan) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ca/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Croatian) Currently translated at 60.8% (1415 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hr/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2326 of 2326 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2047 of 2047 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * import/export --------- Co-authored-by: mlanp Co-authored-by: Random Co-authored-by: 大王叫我来巡山 Co-authored-by: No name Co-authored-by: M1K4 Co-authored-by: idk Co-authored-by: jonnysemon Co-authored-by: Jester Hodl Co-authored-by: summoner001 Co-authored-by: Goudarz Jafari Co-authored-by: Cosmin Co-authored-by: Rafi Co-authored-by: fran secs Co-authored-by: Bezruchenko Simon Co-authored-by: Doğaç Tanrıverdi Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com> Co-authored-by: Karlos Sagan Co-authored-by: Nenad --- .../bg.xcloc/Localized Contents/bg.xliff | 56 ++ .../cs.xcloc/Localized Contents/cs.xliff | 56 ++ .../de.xcloc/Localized Contents/de.xliff | 105 ++- .../en.xcloc/Localized Contents/en.xliff | 70 ++ .../es.xcloc/Localized Contents/es.xliff | 109 ++- .../fi.xcloc/Localized Contents/fi.xliff | 56 ++ .../fr.xcloc/Localized Contents/fr.xliff | 56 ++ .../hu.xcloc/Localized Contents/hu.xliff | 645 ++++++++++------- .../it.xcloc/Localized Contents/it.xliff | 99 +++ .../ja.xcloc/Localized Contents/ja.xliff | 56 ++ .../nl.xcloc/Localized Contents/nl.xliff | 101 ++- .../pl.xcloc/Localized Contents/pl.xliff | 73 ++ .../ru.xcloc/Localized Contents/ru.xliff | 56 ++ .../th.xcloc/Localized Contents/th.xliff | 56 ++ .../tr.xcloc/Localized Contents/tr.xliff | 56 ++ .../uk.xcloc/Localized Contents/uk.xliff | 56 ++ .../Localized Contents/zh-Hans.xliff | 56 ++ .../SimpleX NSE/pl.lproj/Localizable.strings | 8 +- .../SimpleX SE/hu.lproj/Localizable.strings | 4 +- apps/ios/de.lproj/Localizable.strings | 138 +++- apps/ios/es.lproj/Localizable.strings | 147 +++- apps/ios/hu.lproj/Localizable.strings | 676 +++++++++++------- apps/ios/it.lproj/Localizable.strings | 132 +++- apps/ios/nl.lproj/Localizable.strings | 134 +++- apps/ios/pl.lproj/Localizable.strings | 48 ++ .../commonMain/resources/MR/ar/strings.xml | 78 +- .../commonMain/resources/MR/ca/strings.xml | 26 + .../commonMain/resources/MR/cs/strings.xml | 169 +++++ .../commonMain/resources/MR/de/strings.xml | 21 +- .../commonMain/resources/MR/es/strings.xml | 42 +- .../commonMain/resources/MR/fa/strings.xml | 1 + .../commonMain/resources/MR/fr/strings.xml | 5 + .../commonMain/resources/MR/hr/strings.xml | 223 +++++- .../commonMain/resources/MR/hu/strings.xml | 447 ++++++------ .../commonMain/resources/MR/in/strings.xml | 11 + .../commonMain/resources/MR/it/strings.xml | 27 +- .../commonMain/resources/MR/nl/strings.xml | 25 +- .../commonMain/resources/MR/ro/strings.xml | 72 +- .../commonMain/resources/MR/tr/strings.xml | 23 +- .../commonMain/resources/MR/uk/strings.xml | 18 +- .../commonMain/resources/MR/vi/strings.xml | 11 + .../resources/MR/zh-rCN/strings.xml | 23 + 42 files changed, 3409 insertions(+), 862 deletions(-) diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 396232e038..dce5d2030c 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -1212,6 +1212,10 @@ По-добри групи No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. По-добри дати на съобщението. @@ -1232,6 +1236,10 @@ Подобрени известия No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ По-добра сигурност ✅ @@ -2730,6 +2738,10 @@ This is your own one-time link! Не активирай No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Не показвай отново @@ -3452,11 +3464,19 @@ This is your own one-time link! Бързо и без чакане, докато подателят е онлайн! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. По-бързо присъединяване и по-надеждни съобщения. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Любим @@ -3731,6 +3751,10 @@ Error: %2$@ GIF файлове и стикери No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! message preview @@ -3843,6 +3867,10 @@ Error: %2$@ Помощ No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Скрит @@ -4595,6 +4623,10 @@ This is your link for group %@! Членовете на групата могат да изпращат гласови съобщения. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus No comment provided by engineer. @@ -5027,6 +5059,10 @@ This is your link for group %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -5357,6 +5393,10 @@ Requires compatible VPN. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Други @@ -5608,6 +5648,10 @@ Error: %@ Поверителни имена на файлове No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing No comment provided by engineer. @@ -6470,6 +6514,10 @@ Enable in *Network & servers* settings. Изпращай известия No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Изпращайте въпроси и идеи @@ -6690,6 +6738,10 @@ Enable in *Network & servers* settings. Задайте го вместо системната идентификация. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Задай kод за достъп @@ -7444,6 +7496,10 @@ It can happen because of some bug or when the connection is compromised.This link was used with another mobile device, please create a new link on the desktop. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Тази настройка се прилага за съобщения в текущия ви профил **%@**. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 1f417ad85a..55b750b371 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -1156,6 +1156,10 @@ Better groups No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. No comment provided by engineer. @@ -1173,6 +1177,10 @@ Better notifications No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ No comment provided by engineer. @@ -2611,6 +2619,10 @@ This is your own one-time link! Nepovolovat No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Znovu neukazuj @@ -3310,10 +3322,18 @@ This is your own one-time link! Rychle a bez čekání, než bude odesílatel online! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Oblíbené @@ -3579,6 +3599,10 @@ Error: %2$@ GIFy a nálepky No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! message preview @@ -3689,6 +3713,10 @@ Error: %2$@ Pomoc No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Skryté @@ -4414,6 +4442,10 @@ This is your link for group %@! Členové skupiny mohou posílat hlasové zprávy. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4830,6 +4862,10 @@ This is your link for group %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -5150,6 +5186,10 @@ Vyžaduje povolení sítě VPN. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other No comment provided by engineer. @@ -5392,6 +5432,10 @@ Error: %@ Soukromé názvy souborů No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing No comment provided by engineer. @@ -6233,6 +6277,10 @@ Enable in *Network & servers* settings. Odeslat oznámení No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Zasílání otázek a nápadů @@ -6451,6 +6499,10 @@ Enable in *Network & servers* settings. Nastavte jej namísto ověřování systému. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Nastavit heslo @@ -7184,6 +7236,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován This link was used with another mobile device, please create a new link on the desktop. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Toto nastavení platí pro zprávy ve vašem aktuálním chat profilu **%@**. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 9ab9b2a818..3efdfa942b 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -491,7 +491,7 @@ 1 month - Älter als einen Monat + Älter als ein Monat delete after time time interval @@ -545,10 +545,12 @@ @%@ + @%@ No comment provided by engineer. @'%@' + @'%@' No comment provided by engineer. @@ -653,6 +655,7 @@ Active + Aktiv token status text @@ -842,6 +845,7 @@ All reports will be archived for you. + Alle Meldungen werden für Sie archiviert. No comment provided by engineer. @@ -921,6 +925,7 @@ Allow to report messsages to moderators. + Melden von Nachrichten an Moderatoren erlauben. No comment provided by engineer. @@ -1090,10 +1095,12 @@ Archive %lld reports? + Archiviere %lld Meldungen? No comment provided by engineer. Archive all reports? + Alle Meldungen archivieren? No comment provided by engineer. @@ -1118,6 +1125,7 @@ Archive reports + Meldungen archivieren swipe action @@ -1235,6 +1243,10 @@ Bessere Gruppen No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. Verbesserte Nachrichten-Datumsinformation @@ -1255,6 +1267,10 @@ Verbesserte Benachrichtigungen No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ Verbesserte Sicherheit ✅ @@ -1679,6 +1695,7 @@ Clear group? + Gruppe entfernen? No comment provided by engineer. @@ -1828,6 +1845,7 @@ Confirmed + Bestätigt token status text @@ -2532,7 +2550,7 @@ Das ist Ihr eigener Einmal-Link! Delete messages after - Löschen der Nachrichten + Nachrichten löschen No comment provided by engineer. @@ -2845,6 +2863,10 @@ Das ist Ihr eigener Einmal-Link! Nicht aktivieren No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Nicht nochmals anzeigen @@ -3203,6 +3225,7 @@ Das ist Ihr eigener Einmal-Link! Error checking token status + Fehler beim Überprüfen des Token-Status No comment provided by engineer. @@ -3362,6 +3385,7 @@ Das ist Ihr eigener Einmal-Link! Error registering for notifications + Fehler beim Registrieren für Benachrichtigungen alert title @@ -3471,6 +3495,7 @@ Das ist Ihr eigener Einmal-Link! Error testing server connection + Fehler beim Testen der Server-Verbindung No comment provided by engineer. @@ -3555,6 +3580,7 @@ Das ist Ihr eigener Einmal-Link! Expired + Abgelaufen token status text @@ -3597,11 +3623,19 @@ Das ist Ihr eigener Einmal-Link! Schnell und ohne warten auf den Absender, bis er online ist! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Schnellerer Gruppenbeitritt und zuverlässigere Nachrichtenzustellung. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Favorit @@ -3758,6 +3792,7 @@ Das ist Ihr eigener Einmal-Link! For all moderators + Für alle Moderatoren No comment provided by engineer. @@ -3777,6 +3812,7 @@ Das ist Ihr eigener Einmal-Link! For me + Für mich No comment provided by engineer. @@ -3903,6 +3939,10 @@ Fehler: %2$@ GIFs und Sticker No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! Guten Nachmittag! @@ -4018,6 +4058,10 @@ Fehler: %2$@ Hilfe No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Verborgen @@ -4329,22 +4373,27 @@ Weitere Verbesserungen sind bald verfügbar! Invalid + Ungültig token status text Invalid (bad token) + Ungültig (falsches Token) token status text Invalid (expired) + Ungültig (abgelaufen) token status text Invalid (unregistered) + Ungültig (nicht registriert) token status text Invalid (wrong topic) + Ungültig (falsches Thema) token status text @@ -4727,6 +4776,7 @@ Das ist Ihr Link für die Gruppe %@! Member reports + Mitglieder-Meldungen chat feature @@ -4766,6 +4816,7 @@ Das ist Ihr Link für die Gruppe %@! Members can report messsages to moderators. + Mitglieder können Nachrichten an Moderatoren melden. No comment provided by engineer. @@ -4793,6 +4844,10 @@ Das ist Ihr Link für die Gruppe %@! Gruppenmitglieder können Sprachnachrichten versenden. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus Menüs @@ -5045,6 +5100,7 @@ Das ist Ihr Link für die Gruppe %@! Mute all + Alle stummschalten notification label action @@ -5099,6 +5155,7 @@ Das ist Ihr Link für die Gruppe %@! New + Neu token status text @@ -5256,6 +5313,10 @@ Das ist Ihr Link für die Gruppe %@! Keine Medien- und Dateiserver. servers error + + No message + No comment provided by engineer. + No message servers. Keine Nachrichten-Server. @@ -5313,6 +5374,7 @@ Das ist Ihr Link für die Gruppe %@! No token! + Kein Token! alert title @@ -5357,6 +5419,7 @@ Das ist Ihr Link für die Gruppe %@! Notifications error + Benachrichtigungs-Fehler alert title @@ -5366,6 +5429,7 @@ Das ist Ihr Link für die Gruppe %@! Notifications status + Benachrichtigungs-Status alert title @@ -5611,6 +5675,10 @@ Dies erfordert die Aktivierung eines VPNs. Oder zum privaten Teilen No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Andere @@ -5804,14 +5872,17 @@ Fehler: %@ Please try to disable and re-enable notfications. + Bitte versuchen Sie, die Benachrichtigungen zu deaktivieren und wieder zu aktivieren. token info Please wait for token activation to complete. + Bitte warten Sie, bis die Token-Aktivierung abgeschlossen ist. token info Please wait for token to be registered. + Bitte warten Sie auf die Registrierung des Tokens. token info @@ -5874,6 +5945,10 @@ Fehler: %@ Neutrale Dateinamen No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing Privates Nachrichten-Routing @@ -5951,6 +6026,7 @@ Fehler: %@ Prohibit reporting messages to moderators. + Melden von Nachrichten an Moderatoren nicht erlauben. No comment provided by engineer. @@ -6212,14 +6288,17 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Register + Registrieren No comment provided by engineer. Register notification token? + Benachrichtigungs-Token registrieren? token info Registered + Registriert token status text @@ -6360,10 +6439,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Report: (text.isEmpty ? reason.text : text) + Meldung: (text.isEmpty ? reason.text : text) report in notification Reporting messages to moderators is prohibited. + Melden von Nachrichten an Moderatoren ist nicht erlaubt. No comment provided by engineer. @@ -6792,6 +6873,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Benachrichtigungen senden No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Senden Sie Fragen und Ideen @@ -7032,6 +7117,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Anstelle der System-Authentifizierung festlegen. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Zugangscode einstellen @@ -7498,6 +7587,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. TCP port for messaging + TCP-Port für Nachrichtenübermittlung No comment provided by engineer. @@ -7577,6 +7667,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Test notifications + Benachrichtigungen testen No comment provided by engineer. @@ -7836,6 +7927,10 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil **%@**. @@ -7950,6 +8045,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Token status: %@. + Token-Status: %@. token status @@ -8211,6 +8307,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use TCP port %@ when no port is specified. + Solange kein Port konfiguriert ist, wird TCP-Port %@ genutzt. No comment provided by engineer. @@ -8290,6 +8387,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use web port + Web-Port nutzen No comment provided by engineer. @@ -8848,6 +8946,7 @@ Verbindungsanfrage wiederholen? You should receive notifications. + Sie sollten Benachrichtigungen erhalten. token info @@ -9327,7 +9426,7 @@ Verbindungsanfrage wiederholen? default (%@) - Voreinstellung (%@) + Default (%@) delete after time pref value diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index f1edf1e8cd..6d0086bd34 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -1243,6 +1243,11 @@ Better groups No comment provided by engineer. + + Better groups performance + Better groups performance + No comment provided by engineer. + Better message dates. Better message dates. @@ -1263,6 +1268,11 @@ Better notifications No comment provided by engineer. + + Better privacy and security + Better privacy and security + No comment provided by engineer. + Better security ✅ Better security ✅ @@ -2855,6 +2865,11 @@ This is your own one-time link! Don't enable No comment provided by engineer. + + Don't miss important messages. + Don't miss important messages. + No comment provided by engineer. + Don't show again Don't show again @@ -3611,11 +3626,21 @@ This is your own one-time link! Fast and no wait until the sender is online! No comment provided by engineer. + + Faster deletion of groups. + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Faster joining and more reliable messages. No comment provided by engineer. + + Faster sending messages. + Faster sending messages. + No comment provided by engineer. + Favorite Favorite @@ -3919,6 +3944,11 @@ Error: %2$@ GIFs and stickers No comment provided by engineer. + + Get notified when mentioned. + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! Good afternoon! @@ -4034,6 +4064,11 @@ Error: %2$@ Help No comment provided by engineer. + + Help admins moderating their groups. + Help admins moderating their groups. + No comment provided by engineer. + Hidden Hidden @@ -4816,6 +4851,11 @@ This is your link for group %@! Members can send voice messages. No comment provided by engineer. + + Mention members 👋 + Mention members 👋 + No comment provided by engineer. + Menus Menus @@ -5281,6 +5321,11 @@ This is your link for group %@! No media & file servers. servers error + + No message + No message + No comment provided by engineer. + No message servers. No message servers. @@ -5639,6 +5684,11 @@ Requires compatible VPN. Or to share privately No comment provided by engineer. + + Organize chats into lists + Organize chats into lists + No comment provided by engineer. + Other Other @@ -5905,6 +5955,11 @@ Error: %@ Private filenames No comment provided by engineer. + + Private media file names. + Private media file names. + No comment provided by engineer. + Private message routing Private message routing @@ -6829,6 +6884,11 @@ Enable in *Network & servers* settings. Send notifications No comment provided by engineer. + + Send private reports + Send private reports + No comment provided by engineer. + Send questions and ideas Send questions and ideas @@ -7069,6 +7129,11 @@ Enable in *Network & servers* settings. Set it instead of system authentication. No comment provided by engineer. + + Set message expiration in chats. + Set message expiration in chats. + No comment provided by engineer. + Set passcode Set passcode @@ -7875,6 +7940,11 @@ It can happen because of some bug or when the connection is compromised.This link was used with another mobile device, please create a new link on the desktop. No comment provided by engineer. + + This message was deleted or not received yet. + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. This setting applies to messages in your current chat profile **%@**. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 968dd52f13..c2b996fd2e 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -545,10 +545,12 @@ @%@ + @%@ No comment provided by engineer. @'%@' + @'%@' No comment provided by engineer. @@ -653,6 +655,7 @@ Active + Activo token status text @@ -842,6 +845,7 @@ All reports will be archived for you. + Todos los informes serán archivados para ti. No comment provided by engineer. @@ -921,6 +925,7 @@ Allow to report messsages to moderators. + Se permite informar de mensajes a los moderadores. No comment provided by engineer. @@ -1090,10 +1095,12 @@ Archive %lld reports? + ¿Archivar %lld informes? No comment provided by engineer. Archive all reports? + ¿Archivar todos los informes? No comment provided by engineer. @@ -1118,6 +1125,7 @@ Archive reports + Archivar informes swipe action @@ -1235,6 +1243,10 @@ Grupos mejorados No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. Sistema de fechas mejorado. @@ -1255,6 +1267,10 @@ Notificaciones mejoradas No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ Seguridad mejorada ✅ @@ -1679,6 +1695,7 @@ Clear group? + ¿Vaciar grupo? No comment provided by engineer. @@ -1828,6 +1845,7 @@ Confirmed + Confirmado token status text @@ -2527,7 +2545,7 @@ This is your own one-time link! Delete messages - Eliminar mensajes + Activar alert button @@ -2845,6 +2863,10 @@ This is your own one-time link! No activar No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again No volver a mostrar @@ -3203,6 +3225,7 @@ This is your own one-time link! Error checking token status + Error al verificar el estado del token No comment provided by engineer. @@ -3362,11 +3385,12 @@ This is your own one-time link! Error registering for notifications + Error al registrarse para notificaciones alert title Error removing member - Error al eliminar miembro + Error al expulsar miembro No comment provided by engineer. @@ -3471,6 +3495,7 @@ This is your own one-time link! Error testing server connection + Error al testar la conexión al servidor No comment provided by engineer. @@ -3555,6 +3580,7 @@ This is your own one-time link! Expired + Expirado token status text @@ -3597,11 +3623,19 @@ This is your own one-time link! ¡Rápido y sin necesidad de esperar a que el remitente esté en línea! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Mensajería más segura y conexión más rápida. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Favoritos @@ -3758,6 +3792,7 @@ This is your own one-time link! For all moderators + Para todos los moderadores No comment provided by engineer. @@ -3777,6 +3812,7 @@ This is your own one-time link! For me + para mí No comment provided by engineer. @@ -3903,6 +3939,10 @@ Error: %2$@ GIFs y stickers No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! ¡Buenas tardes! @@ -4018,6 +4058,10 @@ Error: %2$@ Ayuda No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Oculto @@ -4329,22 +4373,27 @@ More improvements are coming soon! Invalid + No válido token status text Invalid (bad token) + No válido (token incorrecto) token status text Invalid (expired) + No válido (expirado) token status text Invalid (unregistered) + No válido (no registrado) token status text Invalid (wrong topic) + No válido (tópico incorrecto) token status text @@ -4727,6 +4776,7 @@ This is your link for group %@! Member reports + Informes de miembro chat feature @@ -4766,6 +4816,7 @@ This is your link for group %@! Members can report messsages to moderators. + Los miembros pueden informar de mensajes a los moderadores. No comment provided by engineer. @@ -4793,6 +4844,10 @@ This is your link for group %@! Los miembros del grupo pueden enviar mensajes de voz. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus Menus @@ -5045,6 +5100,7 @@ This is your link for group %@! Mute all + Silenciar todo notification label action @@ -5099,6 +5155,7 @@ This is your link for group %@! New + Nuevo token status text @@ -5256,6 +5313,10 @@ This is your link for group %@! Sin servidores para archivos y multimedia. servers error + + No message + No comment provided by engineer. + No message servers. Sin servidores para mensajes. @@ -5313,6 +5374,7 @@ This is your link for group %@! No token! + ¡Sin token! alert title @@ -5357,6 +5419,7 @@ This is your link for group %@! Notifications error + Error en notificaciones alert title @@ -5366,6 +5429,7 @@ This is your link for group %@! Notifications status + Estado notificaciones alert title @@ -5611,6 +5675,10 @@ Requiere activación de la VPN. O para compartir en privado No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Otro @@ -5804,14 +5872,17 @@ Error: %@ Please try to disable and re-enable notfications. + Por favor, intenta desactivar y reactivar las notificaciones. token info Please wait for token activation to complete. + Por favor, espera a que el token de activación se complete. token info Please wait for token to be registered. + Por favor, espera a que el token se registre. token info @@ -5874,6 +5945,10 @@ Error: %@ Nombres de archivos privados No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing Enrutamiento privado de mensajes @@ -5951,6 +6026,7 @@ Error: %@ Prohibit reporting messages to moderators. + No se permite informar de mensajes a los moderadores. No comment provided by engineer. @@ -6212,14 +6288,17 @@ Actívalo en ajustes de *Servidores y Redes*. Register + Registrar No comment provided by engineer. Register notification token? + ¿Registrar el token de notificaciones? token info Registered + Registrado token status text @@ -6360,10 +6439,12 @@ Actívalo en ajustes de *Servidores y Redes*. Report: (text.isEmpty ? reason.text : text) + Informe: (text.isEmpty ? reason.text : text) report in notification Reporting messages to moderators is prohibited. + No se permite informar de mensajes a los moderadores. No comment provided by engineer. @@ -6792,6 +6873,10 @@ Actívalo en ajustes de *Servidores y Redes*. Enviar notificaciones No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Consultas y sugerencias @@ -7032,6 +7117,10 @@ Actívalo en ajustes de *Servidores y Redes*. Úsalo en lugar de la autenticación del sistema. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Código autodestrucción @@ -7498,6 +7587,7 @@ Actívalo en ajustes de *Servidores y Redes*. TCP port for messaging + Puerto TCP para mensajes No comment provided by engineer. @@ -7577,6 +7667,7 @@ Actívalo en ajustes de *Servidores y Redes*. Test notifications + Probar notificaciones No comment provided by engineer. @@ -7783,7 +7874,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. - Todos los mensajes previos a la selección serán eliminados del chat. ¡No podrá deshacerse! + Todos los mensajes previos al período seleccionado serán eliminados del chat. ¡No podrá deshacerse! alert message @@ -7836,6 +7927,10 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Esta configuración se aplica a los mensajes del perfil actual **%@**. @@ -7950,6 +8045,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. Token status: %@. + Estado token: %@. token status @@ -8211,6 +8307,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Use TCP port %@ when no port is specified. + Se usa el puerto TCP %@ cuando no se ha especificado otro. No comment provided by engineer. @@ -8290,6 +8387,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Use web port + Usar puerto web No comment provided by engineer. @@ -8848,6 +8946,7 @@ Repeat connection request? You should receive notifications. + Deberías recibir notificaciones. token info @@ -9242,7 +9341,7 @@ Repeat connection request? connecting - conectando + conectando... No comment provided by engineer. @@ -9353,7 +9452,7 @@ Repeat connection request? deleted group - grupo eliminado + ha eliminado el grupo rcv group event chat item diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index e8113a5553..d0a7bd7e77 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -1142,6 +1142,10 @@ Better groups No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. No comment provided by engineer. @@ -1159,6 +1163,10 @@ Better notifications No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ No comment provided by engineer. @@ -2596,6 +2604,10 @@ This is your own one-time link! Älä salli No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Älä näytä uudelleen @@ -3292,10 +3304,18 @@ This is your own one-time link! Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Suosikki @@ -3561,6 +3581,10 @@ Error: %2$@ GIFit ja tarrat No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! message preview @@ -3671,6 +3695,10 @@ Error: %2$@ Apua No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Piilotettu @@ -4396,6 +4424,10 @@ This is your link for group %@! Ryhmän jäsenet voivat lähettää ääniviestejä. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4811,6 +4843,10 @@ This is your link for group %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -5130,6 +5166,10 @@ Edellyttää VPN:n sallimista. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other No comment provided by engineer. @@ -5372,6 +5412,10 @@ Error: %@ Yksityiset tiedostonimet No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing No comment provided by engineer. @@ -6212,6 +6256,10 @@ Enable in *Network & servers* settings. Lähetys ilmoitukset No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Lähetä kysymyksiä ja ideoita @@ -6430,6 +6478,10 @@ Enable in *Network & servers* settings. Aseta se järjestelmän todennuksen sijaan. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Aseta pääsykoodi @@ -7162,6 +7214,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.This link was used with another mobile device, please create a new link on the desktop. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Tämä asetus koskee nykyisen keskusteluprofiilisi viestejä *%@**. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 0c9a87126e..2d838b54a4 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -1222,6 +1222,10 @@ Des groupes plus performants No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. Meilleures dates de messages. @@ -1242,6 +1246,10 @@ Notifications améliorées No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ Sécurité accrue ✅ @@ -2815,6 +2823,10 @@ Il s'agit de votre propre lien unique ! Ne pas activer No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Ne plus afficher @@ -3561,11 +3573,19 @@ Il s'agit de votre propre lien unique ! Rapide et ne nécessitant pas d'attendre que l'expéditeur soit en ligne ! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Connexion plus rapide et messages plus fiables. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Favoris @@ -3864,6 +3884,10 @@ Erreur : %2$@ GIFs et stickers No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! Bonjour ! @@ -3978,6 +4002,10 @@ Erreur : %2$@ Aide No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Caché @@ -4747,6 +4775,10 @@ Voici votre lien pour le groupe %@ ! Les membres du groupe peuvent envoyer des messages vocaux. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus Menus @@ -5205,6 +5237,10 @@ Voici votre lien pour le groupe %@ ! Pas de serveurs de médias et de fichiers. servers error + + No message + No comment provided by engineer. + No message servers. Pas de serveurs de messages. @@ -5553,6 +5589,10 @@ Nécessite l'activation d'un VPN. Ou à partager en privé No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Autres @@ -5816,6 +5856,10 @@ Erreur : %@ Noms de fichiers privés No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing Routage privé des messages @@ -6725,6 +6769,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Envoi de notifications No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Envoyez vos questions et idées @@ -6964,6 +7012,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Il permet de remplacer l'authentification du système. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Définir le code d'accès @@ -7765,6 +7817,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le bureau. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Ce paramètre s'applique aux messages de votre profil de chat actuel **%@**. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 14746bfb92..cbc05bda5e 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -44,7 +44,7 @@ ## In reply to - ## Válaszul erre: + ## Válaszul erre copied message info @@ -109,7 +109,7 @@ %@ is not verified - %@ nem hitelesített + %@ nincs hitelesítve No comment provided by engineer. @@ -239,7 +239,7 @@ %lld file(s) with total size of %@ - %lld fájl, amely(ek)nek teljes mérete: %@ + %lld fájl, %@ összméretben No comment provided by engineer. @@ -264,7 +264,7 @@ %lld messages marked deleted - %lld törlésre megjelölt üzenet + %lld üzenet megjelölve törlésre No comment provided by engineer. @@ -284,17 +284,17 @@ %lld seconds - %lld másodperc + %lld mp No comment provided by engineer. %lldd - %lldd + %lldn No comment provided by engineer. %lldh - %lldh + %lldó No comment provided by engineer. @@ -304,22 +304,22 @@ %lldm - %lldm + %lldp No comment provided by engineer. %lldmth - %lldmth + %lldh No comment provided by engineer. %llds - %llds + %lldmp No comment provided by engineer. %lldw - %lldw + %lldhét No comment provided by engineer. @@ -354,7 +354,7 @@ **Create 1-time link**: to create and share a new invitation link. - **Ismerős hozzáadása:** új meghívó-hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz. + **Ismerős hozzáadása:** új meghívási hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz. No comment provided by engineer. @@ -379,7 +379,7 @@ **Please note**: you will NOT be able to recover or change passphrase if you lose it. - **Megjegyzés:** NEM fogja tudni helyreállítani, vagy megváltoztatni a jelmondatot abban az esetben, ha elveszíti. + **Megjegyzés:** NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti. No comment provided by engineer. @@ -436,7 +436,7 @@ - a bit better groups. - and more! - stabilabb üzenetkézbesítés. -- valamivel jobb csoportok. +- picit továbbfejlesztett csoportok. - és még sok más! No comment provided by engineer. @@ -454,7 +454,7 @@ - custom time to disappear. - editing history. - legfeljebb 5 perc hosszúságú hangüzenetek. -- egyedi üzenet-eltűnési időkorlát. +- egyéni üzenet-eltűnési időkorlát. - előzmények szerkesztése. No comment provided by engineer. @@ -508,12 +508,12 @@ 1-time link - Egyszer használható meghívó-hivatkozás + Egyszer használható meghívási hivatkozás No comment provided by engineer. 1-time link can be used *with one contact only* - share in person or via any messenger. - Az egyszer használható meghívó-hivatkozás csak *egyetlen ismerőssel használható* – személyesen vagy bármilyen üzenetküldőn keresztül megosztható. + Az egyszer használható meghívási hivatkozás csak *egyetlen ismerőssel használható* – személyesen vagy bármilyen üzenetküldőn keresztül megosztható. No comment provided by engineer. @@ -545,10 +545,12 @@ @%@ + @%@ No comment provided by engineer. @'%@' + @'%@' No comment provided by engineer. @@ -585,17 +587,17 @@ Abort changing address - Címváltoztatás megszakítása + Cím módosításának megszakítása No comment provided by engineer. Abort changing address? - Címváltoztatás megszakítása?? + Cím módosításának megszakítása? No comment provided by engineer. About SimpleX Chat - SimpleX Chat névjegye + A SimpleX Chat névjegye No comment provided by engineer. @@ -632,7 +634,7 @@ Accept incognito - Fogadás inkognitóban + Elfogadás inkognitóban accept contact request via notification swipe action @@ -653,6 +655,7 @@ Active + Aktív token status text @@ -747,12 +750,12 @@ Address change will be aborted. Old receiving address will be used. - A cím megváltoztatása meg fog szakadni. A régi fogadási cím lesz használva. + A cím módosítása meg fog szakadni. A régi fogadási cím lesz használva. No comment provided by engineer. Address or 1-time link? - Cím vagy egyszer használható meghívó-hivatkozás? + Cím vagy egyszer használható meghívási hivatkozás? No comment provided by engineer. @@ -767,7 +770,7 @@ Admins can create the links to join groups. - Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való kapcsolódáshoz. + Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz. No comment provided by engineer. @@ -842,6 +845,7 @@ All reports will be archived for you. + Az összes jelentés archiválva lesz az Ön számára. No comment provided by engineer. @@ -891,12 +895,12 @@ Allow message reactions only if your contact allows them. - Az üzenetreakciók küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. + A reakciók hozzáadása az üzenetekhez csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. No comment provided by engineer. Allow message reactions. - Üzenetreakciók engedélyezése. + A reakciók hozzáadása az üzenetekhez engedélyezve van. No comment provided by engineer. @@ -916,11 +920,12 @@ Allow to irreversibly delete sent messages. (24 hours) - Elküldött üzenetek végleges törlésének engedélyezése. (24 óra) + Az elküldött üzenetek végleges törlése engedélyezve van. (24 óra) No comment provided by engineer. Allow to report messsages to moderators. + Lehetővé teszi az üzenetek jelentését a moderátorok felé. No comment provided by engineer. @@ -930,12 +935,12 @@ Allow to send files and media. - Fájlok és médiatartalmak küldésének engedélyezése. + A fájlok- és a médiatartalmak küldése engedélyezve van. No comment provided by engineer. Allow to send voice messages. - Hangüzenetek küldésének engedélyezése. + A hangüzenetek küldése engedélyezve van. No comment provided by engineer. @@ -950,7 +955,7 @@ Allow your contacts adding message reactions. - Az üzenetreakciók küldése engedélyezve van az ismerősei számára. + A reakciók hozzáadása az üzenetekhez engedélyezve van az ismerősei számára. No comment provided by engineer. @@ -1000,7 +1005,7 @@ An empty chat profile with the provided name is created, and the app opens as usual. - Egy üres csevegési profil jön létre a megadott névvel, és az alkalmazás a szokásos módon megnyílik. + Egy üres csevegési profil lesz létrehozva a megadott névvel, és az alkalmazás a szokásos módon megnyílik. No comment provided by engineer. @@ -1090,10 +1095,12 @@ Archive %lld reports? + %lld jelentés archiválása? No comment provided by engineer. Archive all reports? + Az összes jelentés archiválása? No comment provided by engineer. @@ -1118,6 +1125,7 @@ Archive reports + Jelentések archiválása swipe action @@ -1152,7 +1160,7 @@ Audio/video calls - Hang-/videóhívások + Hang- és videóhívások chat feature @@ -1232,7 +1240,11 @@ Better groups - Javított csoportok + Továbbfejlesztett csoportok + No comment provided by engineer. + + + Better groups performance No comment provided by engineer. @@ -1242,7 +1254,7 @@ Better messages - Jobb üzenetek + Továbbfejlesztett üzenetek No comment provided by engineer. @@ -1255,6 +1267,10 @@ Továbbfejlesztett értesítések No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ Továbbfejlesztett biztonság ✅ @@ -1277,7 +1293,7 @@ Block for all - Letiltás az összes tag számára + Letiltás No comment provided by engineer. @@ -1287,7 +1303,7 @@ Block member - Tag letiltása + Letiltás No comment provided by engineer. @@ -1302,7 +1318,7 @@ Blocked by admin - Az adminisztrátor letiltotta + Letiltva az adminisztrátor által No comment provided by engineer. @@ -1317,12 +1333,12 @@ Both you and your contact can add message reactions. - Mindkét fél is hozzáadhat üzenetreakciókat. + Mindkét fél hozzáadhat az üzenetekhez reakciókat. No comment provided by engineer. Both you and your contact can irreversibly delete sent messages. (24 hours) - Mindkét fél törölheti véglegesen az elküldött üzeneteket. (24 óra) + Mindkét fél véglegesen törölheti az elküldött üzeneteket. (24 óra) No comment provided by engineer. @@ -1448,7 +1464,7 @@ Change - Megváltoztatás + Módosítás No comment provided by engineer. @@ -1458,52 +1474,52 @@ Change chat profiles - Csevegési profilok megváltoztatása + Csevegési profilok módosítása authentication reason Change database passphrase? - Az adatbázis jelmondatának megváltoztatása? + Az adatbázis jelmondatának módosítása? No comment provided by engineer. Change lock mode - Zárolási mód megváltoztatása + Zárolási mód módosítása authentication reason Change member role? - Tag szerepkörének megváltoztatása? + Tag szerepkörének módosítása? No comment provided by engineer. Change passcode - Jelkód megváltoztatása + Jelkód módosítása authentication reason Change receiving address - A fogadócím megváltoztatása + A fogadási cím módosítása No comment provided by engineer. Change receiving address? - Megváltoztatja a fogadócímet? + Módosítja a fogadási címet? No comment provided by engineer. Change role - Szerepkör megváltoztatása + Szerepkör módosítása No comment provided by engineer. Change self-destruct mode - Önmegsemmisítő-mód megváltoztatása + Önmegsemmisítő-mód módosítása authentication reason Change self-destruct passcode - Önmegsemmisítő-jelkód megváltoztatása + Önmegsemmisítő-jelkód módosítása authentication reason set passcode view @@ -1579,12 +1595,12 @@ Chat preferences - Csevegési beállítások + Csevegés-beállítások No comment provided by engineer. Chat preferences were changed. - A csevegési beállítások megváltoztak. + A csevegés-beállítások módosultak. alert message @@ -1679,6 +1695,7 @@ Clear group? + Csoport kiürítése? No comment provided by engineer. @@ -1713,7 +1730,7 @@ Compare file - Fájl összehasonlítás + Fájl-összehasonlítás server test step @@ -1728,7 +1745,7 @@ Conditions accepted on: %@. - Feltételek elfogadva ekkor: %@. + Feltételek elfogadva: %@. No comment provided by engineer. @@ -1748,7 +1765,7 @@ Conditions will be accepted for enabled operators after 30 days. - A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltető számára. + A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltetők számára. No comment provided by engineer. @@ -1828,6 +1845,7 @@ Confirmed + Megerősítve token status text @@ -1871,7 +1889,7 @@ Ez az Ön SimpleX-címe! Connect to yourself? This is your own one-time link! Kapcsolódás saját magához? -Ez az Ön egyszer használható meghívó-hivatkozása! +Ez az Ön egyszer használható meghívási hivatkozása! No comment provided by engineer. @@ -1886,7 +1904,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Connect via one-time link - Kapcsolódás egyszer használható meghívó-hivatkozáson keresztül + Kapcsolódás egyszer használható meghívási hivatkozáson keresztül No comment provided by engineer. @@ -2033,7 +2051,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Contact hidden: - Ismerős elrejtve: + Rejtett név: notification @@ -2098,7 +2116,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Core version: v%@ - Alapverziószám: v%@ + Fő verzió: v%@ No comment provided by engineer. @@ -2118,7 +2136,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Create 1-time link - Egyszer használható meghívó-hivatkozás létrehozása + Egyszer használható meghívási hivatkozás létrehozása No comment provided by engineer. @@ -2128,7 +2146,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Create a group using a random profile. - Csoport létrehozása véletlenszerűen létrehozott profillal. + Csoport létrehozása véletlenszerű profillal. No comment provided by engineer. @@ -2168,7 +2186,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Create queue - Sorbaállítás létrehozása + Sorba állítás létrehozása server test step @@ -2188,12 +2206,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Created at - Létrehozva ekkor: + Létrehozva No comment provided by engineer. Created at: %@ - Létrehozva ekkor: %@ + Létrehozva: %@ copied message info @@ -2228,17 +2246,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Currently maximum supported file size is %@. - Jelenleg a maximális támogatott fájlméret %@. + Jelenleg támogatott legnagyobb fájl méret: %@. No comment provided by engineer. Custom time - Személyreszabott idő + Egyéni időköz No comment provided by engineer. Customizable message shape. - Testreszabható üzenetbuborékok. + Testre szabható üzenetbuborékok. No comment provided by engineer. @@ -2291,7 +2309,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Database encryption passphrase will be updated. - Az datbázis titkosítási jelmondata frissülni fog. + Az adatbázis titkosítási jelmondata frissítve lesz. No comment provided by engineer. @@ -2302,12 +2320,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Database is encrypted using a random passphrase, you can change it. - Az adatbázis egy véletlenszerű jelmondattal van titkosítva, ami megváltoztatható. + Az adatbázis egy véletlenszerű jelmondattal van titkosítva, amelyet szabadon módosíthat. No comment provided by engineer. Database is encrypted using a random passphrase. Please change it before exporting. - Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt változtassa meg. + Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt módosítsa. No comment provided by engineer. @@ -2377,12 +2395,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete %lld messages of members? - Tagok %lld üzenetének törlése? + Törli a tagok %lld üzenetét? No comment provided by engineer. Delete %lld messages? - Töröl %lld üzenetet? + %lld üzenet törlése? No comment provided by engineer. @@ -2397,7 +2415,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete after - Törlés ennyi idő után: + Törlés ennyi idő után No comment provided by engineer. @@ -2467,7 +2485,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete files and media? - Fájlok és a médiatartalmak törlése? + A fájlok- és a médiatartalmak törlése? No comment provided by engineer. @@ -2502,7 +2520,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete link - Hivatkozás törlése + Törlés No comment provided by engineer. @@ -2532,7 +2550,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete messages after - Üzenetek törlése ennyi idő után: + Üzenetek törlése ennyi idő után No comment provided by engineer. @@ -2562,7 +2580,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete queue - Sorbaállítás törlése + Sorba állítás törlése server test step @@ -2592,12 +2610,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Deleted at - Törölve ekkor: + Törölve No comment provided by engineer. Deleted at: %@ - Törölve ekkor: %@ + Törölve: %@ copied message info @@ -2617,7 +2635,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delivery receipts are disabled! - A kézbesítési jelentések ki vannak kapcsolva! + A kézbesítési jelentések le vannak tiltva! No comment provided by engineer. @@ -2637,7 +2655,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Desktop app version %@ is not compatible with this app. - A számítógép-alkalmazás verziója %@ nem kompatibilis ezzel az alkalmazással. + A számítógép-alkalmazás verziója (%@) nem kompatibilis ezzel az alkalmazással. No comment provided by engineer. @@ -2652,7 +2670,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Destination server error: %@ - Célkiszolgáló hiba: %@ + Célkiszolgáló-hiba: %@ snd error text @@ -2717,7 +2735,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Direct messages between members are prohibited. - A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban. + A tagok közötti közvetlen üzenetek le vannak tiltva. No comment provided by engineer. @@ -2742,7 +2760,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Disable for all - Letiltás az összes tag számára + Letiltás No comment provided by engineer. @@ -2767,17 +2785,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Disappearing messages are prohibited. - Az eltűnő üzenetek küldése le van tiltva ebben a csoportban. + Az eltűnő üzenetek küldése le van tiltva. No comment provided by engineer. Disappears at - Eltűnik ekkor: + Eltűnik No comment provided by engineer. Disappears at: %@ - Eltűnik ekkor: %@ + Eltűnik: %@ copied message info @@ -2792,7 +2810,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Discover and join groups - Helyi csoportok felfedezése és csatlakozás + Csoportok felfedezése és csatlakozás No comment provided by engineer. @@ -2807,7 +2825,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Do NOT use SimpleX for emergency calls. - NE használja a SimpleX-et segélyhívásokhoz. + NE használja a SimpleXet segélyhívásokhoz. No comment provided by engineer. @@ -2845,6 +2863,10 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Ne engedélyezze No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Ne mutasd újra @@ -3013,7 +3035,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Enabled for - Számukra engedélyezve: + Számukra engedélyezve No comment provided by engineer. @@ -3078,12 +3100,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Encryption re-negotiation error - Titkosítás-újraegyeztetési hiba + Hiba a titkosítás újraegyeztetésekor message decrypt error item Encryption re-negotiation failed. - Sikertelen titkosítás-újraegyeztetés. + Nem sikerült a titkosítást újraegyeztetni. No comment provided by engineer. @@ -3153,7 +3175,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error aborting address change - Hiba a cím megváltoztatásának megszakításakor + Hiba a cím módosításának megszakításakor No comment provided by engineer. @@ -3178,7 +3200,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error changing address - Hiba a cím megváltoztatásakor + Hiba a cím módosításakor No comment provided by engineer. @@ -3188,12 +3210,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error changing role - Hiba a szerepkör megváltoztatásakor + Hiba a szerepkör módosításakor No comment provided by engineer. Error changing setting - Hiba a beállítás megváltoztatásakor + Hiba a beállítás módosításakor No comment provided by engineer. @@ -3203,6 +3225,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error checking token status + Hiba a token állapotának ellenőrzésekor No comment provided by engineer. @@ -3337,7 +3360,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error migrating settings - Hiba a beallítások átköltöztetésekor + Hiba a beállítások átköltöztetésekor No comment provided by engineer. @@ -3362,6 +3385,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error registering for notifications + Hiba az értesítések regisztrálásakor alert title @@ -3416,7 +3440,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error saving user password - Hiba a felhasználói jelszó mentésekor + Hiba a felhasználó jelszavának mentésekor No comment provided by engineer. @@ -3461,7 +3485,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error switching profile! - Hiba a profilváltásakor! + Hiba a profilváltáskor! alertTitle @@ -3471,6 +3495,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error testing server connection + Hiba a kiszolgáló kapcsolatának tesztelésekor No comment provided by engineer. @@ -3555,6 +3580,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Expired + Lejárt token status text @@ -3597,11 +3623,19 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Gyors és nem kell várni, amíg a feladó online lesz! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Kedvenc @@ -3638,17 +3672,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! File server error: %@ - Fájlkiszolgáló hiba: %@ + Fájlkiszolgáló-hiba: %@ file error text File status - Fájlállapot + Fájl állapota No comment provided by engineer. File status: %@ - Fájlállapot: %@ + Fájl állapota: %@ copied message info @@ -3688,7 +3722,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Files and media are prohibited. - A fájlok- és a médiatartalmak le vannak tiltva ebben a csoportban. + A fájlok- és a médiatartalmak küldése le van tiltva. No comment provided by engineer. @@ -3758,6 +3792,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! For all moderators + Az összes moderátor számára No comment provided by engineer. @@ -3777,6 +3812,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! For me + Csak magamnak No comment provided by engineer. @@ -3826,7 +3862,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Forwarded from - Továbbítva innen: + Továbbítva innen No comment provided by engineer. @@ -3853,7 +3889,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Forwarding server: %1$@ Destination server error: %2$@ Továbbító-kiszolgáló: %1$@ -Célkiszolgáló hiba:%2$@ +Célkiszolgáló-hiba: %2$@ snd error text @@ -3903,6 +3939,10 @@ Hiba: %2$@ GIF-ek és matricák No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! Jó napot! @@ -3975,7 +4015,7 @@ Hiba: %2$@ Group moderation - Csoport moderáció + Csoport moderálása No comment provided by engineer. @@ -4018,6 +4058,10 @@ Hiba: %2$@ Súgó No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Se név, se üzenet @@ -4090,12 +4134,12 @@ Hiba: %2$@ How to use it - Hogyan használja + Használati útmutató No comment provided by engineer. How to use your servers - Saját kiszolgálók használata + Hogyan használja a saját kiszolgálóit No comment provided by engineer. @@ -4155,7 +4199,7 @@ Hiba: %2$@ Immune to spam - Spam és visszaélések elleni védelem + Védett a kéretlen tartalommal szemben No comment provided by engineer. @@ -4217,7 +4261,7 @@ További fejlesztések hamarosan! In reply to - Válasz neki + Válaszul erre No comment provided by engineer. @@ -4307,7 +4351,7 @@ További fejlesztések hamarosan! Instant - Azonnal + Azonnali No comment provided by engineer. @@ -4329,22 +4373,27 @@ További fejlesztések hamarosan! Invalid + Érvénytelen token status text Invalid (bad token) + Érvénytelen (hibás token) token status text Invalid (expired) + Érvénytelen (lejárt) token status text Invalid (unregistered) + Érvénytelen (nincs regisztrálva) token status text Invalid (wrong topic) + Érvénytelen (rossz topic) token status text @@ -4429,7 +4478,7 @@ További fejlesztések hamarosan! Irreversible message deletion is prohibited. - Az üzenetek végleges törlése le van tiltva ebben a csoportban. + Az üzenetek végleges törlése le van tiltva. No comment provided by engineer. @@ -4449,7 +4498,7 @@ További fejlesztések hamarosan! 3. The connection was compromised. Ez akkor fordulhat elő, ha: 1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak. -2. Az üzenet visszafejtése sikertelen volt, mert vagy az ismerőse régebbi adatbázis biztonsági mentést használt. +2. Az üzenet visszafejtése sikertelen volt, mert Ön, vagy az ismerőse régebbi adatbázis biztonsági mentést használt. 3. A kapcsolat sérült. No comment provided by engineer. @@ -4517,7 +4566,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Keep - Megtart + Megtartás alert action @@ -4627,7 +4676,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! List name and emoji should be different for all lists. - Az összes lista nevének és emodzsiának különbözőnek kell lennie. + Az összes lista nevének és emodzsijának különbözőnek kell lennie. No comment provided by engineer. @@ -4677,7 +4726,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. - Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. + Győződjön meg arról, hogy a megadott WebRTC ICE-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva. No comment provided by engineer. @@ -4707,7 +4756,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Media & file servers - Média és fájlkiszolgálók + Média- és fájlkiszolgálók No comment provided by engineer. @@ -4727,21 +4776,22 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Member reports + Tagok jelentései chat feature Member role will be changed to "%@". All chat members will be notified. - A tag szerepeköre meg fog változni a következőre: "%@". A csevegés tagjai értesítést fognak kapni. + A tag szerepköre a következőre fog módosulni: „%@”. A csevegés összes tagja értesítést fog kapni. No comment provided by engineer. Member role will be changed to "%@". All group members will be notified. - A tag szerepköre meg fog változni a következőre: „%@”. A csoportban az összes tag értesítve lesz. + A tag szerepköre a következőre fog módosulni: „%@”. A csoport az összes tagja értesítést fog kapni. No comment provided by engineer. Member role will be changed to "%@". The member will receive a new invitation. - A tag szerepköre meg fog változni a következőre: „%@”. A tag új meghívást fog kapni. + A tag szerepköre a következőre fog módosulni: „%@”. A tag új meghívást fog kapni. No comment provided by engineer. @@ -4756,41 +4806,46 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Members can add message reactions. - Csoporttagok üzenetreakciókat adhatnak hozzá. + A tagok reakciókat adhatnak hozzá az üzenetekhez. No comment provided by engineer. Members can irreversibly delete sent messages. (24 hours) - A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra) + A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra) No comment provided by engineer. Members can report messsages to moderators. + A tagok jelenthetik az üzeneteket a moderátorok felé. No comment provided by engineer. Members can send SimpleX links. - A csoport tagjai küldhetnek SimpleX-hivatkozásokat. + A tagok küldhetnek SimpleX-hivatkozásokat. No comment provided by engineer. Members can send direct messages. - A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket. + A tagok küldhetnek egymásnak közvetlen üzeneteket. No comment provided by engineer. Members can send disappearing messages. - A csoport tagjai küldhetnek eltűnő üzeneteket. + A tagok küldhetnek eltűnő üzeneteket. No comment provided by engineer. Members can send files and media. - A csoport tagjai küldhetnek fájlokat és médiatartalmakat. + A tagok küldhetnek fájlokat és médiatartalmakat. No comment provided by engineer. Members can send voice messages. - A csoport tagjai küldhetnek hangüzeneteket. + A tagok küldhetnek hangüzeneteket. + No comment provided by engineer. + + + Mention members 👋 No comment provided by engineer. @@ -4830,7 +4885,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Message queue info - Üzenet-sorbaállítási információ + Üzenetsorbaállítási információ No comment provided by engineer. @@ -4840,12 +4895,12 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Message reactions are prohibited in this chat. - Az üzenetreakciók küldése le van tiltva ebben a csevegésben. + A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben. No comment provided by engineer. Message reactions are prohibited. - Az üzenetreakciók küldése le van tiltva ebben a csoportban. + A reakciók hozzáadása az üzenetekhez le van tiltva. No comment provided by engineer. @@ -4860,7 +4915,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Message shape - Üzenetbuborék formája + Üzenetbuborék alakja No comment provided by engineer. @@ -4870,12 +4925,12 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Message status - Üzenetállapot + Üzenet állapota No comment provided by engineer. Message status: %@ - Üzenetállapot: %@ + Üzenet állapota: %@ copied message info @@ -4900,7 +4955,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Messages from %@ will be shown! - A(z) %@ által írt üzenetek megjelennek! + Az összes %@ által írt üzenet meg fog jelenni! No comment provided by engineer. @@ -5000,12 +5055,12 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Moderated at - Moderálva ekkor: + Moderálva No comment provided by engineer. Moderated at: %@ - Moderálva ekkor: %@ + Moderálva: %@ copied message info @@ -5045,6 +5100,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Mute all + Összes némítása notification label action @@ -5099,6 +5155,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! New + Új token status text @@ -5118,7 +5175,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! New chat - Új beszélgetés + Új csevegés No comment provided by engineer. @@ -5223,7 +5280,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! No device token! - Nincs kiszüléktoken! + Nincs készüléktoken! No comment provided by engineer. @@ -5256,6 +5313,10 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Nincsenek média- és fájlkiszolgálók. servers error + + No message + No comment provided by engineer. + No message servers. Nincsenek üzenet-kiszolgálók. @@ -5298,12 +5359,12 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! No servers to receive files. - Nincsenek fájlfogadó-kiszolgálók. + Nincsenek fájlfogadási kiszolgálók. servers error No servers to receive messages. - Nincsenek üzenetfogadó-kiszolgálók. + Nincsenek üzenetfogadási kiszolgálók. servers error @@ -5313,6 +5374,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! No token! + Nincs token! alert title @@ -5357,6 +5419,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Notifications error + Értesítési hiba alert title @@ -5366,6 +5429,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Notifications status + Értesítések állapota alert title @@ -5399,7 +5463,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! One-time invitation link - Egyszer használható meghívó-hivatkozás + Egyszer használható meghívási hivatkozás No comment provided by engineer. @@ -5418,12 +5482,12 @@ VPN engedélyezése szükséges. Onion hosts will not be used. - Onion-kiszolgálók nem lesznek használva. + Az onion-kiszolgálók nem lesznek használva. No comment provided by engineer. Only chat owners can change preferences. - Csak a csevegés tulajdonosai változtathatják meg a beállításokat. + Csak a csevegés tulajdonosai módosíthatják a csevegés-beállításokat. No comment provided by engineer. @@ -5438,17 +5502,17 @@ VPN engedélyezése szükséges. Only group owners can change group preferences. - Csak a csoport-tulajdonosok változtathatják meg a csoport-beállításokat. + Csak a csoport tulajdonosai módosíthatják a csoport-beállításokat. No comment provided by engineer. Only group owners can enable files and media. - Csak a csoport-tulajdonosok engedélyezhetik a fájlok- és a médiatartalmak küldését. + Csak a csoport tulajdonosai engedélyezhetik a fájlok- és a médiatartalmak küldését. No comment provided by engineer. Only group owners can enable voice messages. - Csak a csoport-tulajdonosok engedélyezhetik a hangüzenetek küldését. + Csak a csoport tulajdonosai engedélyezhetik a hangüzenetek küldését. No comment provided by engineer. @@ -5463,7 +5527,7 @@ VPN engedélyezése szükséges. Only you can add message reactions. - Csak Ön adhat hozzá üzenetreakciókat. + Csak Ön adhat hozzá reakciókat az üzenetekhez. No comment provided by engineer. @@ -5488,7 +5552,7 @@ VPN engedélyezése szükséges. Only your contact can add message reactions. - Csak az ismerőse tud üzenetreakciókat küldeni. + Csak az ismerőse adhat hozzá reakciókat az üzenetekhez. No comment provided by engineer. @@ -5523,7 +5587,7 @@ VPN engedélyezése szükséges. Open changes - Változások megnyitása + Módosítások megtekintése No comment provided by engineer. @@ -5578,7 +5642,7 @@ VPN engedélyezése szükséges. Operator server - Kiszolgáló üzemeltető + Kiszolgáló-üzemeltető alert title @@ -5611,6 +5675,10 @@ VPN engedélyezése szükséges. Vagy a privát megosztáshoz No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other További @@ -5640,7 +5708,7 @@ VPN engedélyezése szükséges. Passcode changed! - A jelkód megváltozott! + A jelkód módosult! No comment provided by engineer. @@ -5650,7 +5718,7 @@ VPN engedélyezése szükséges. Passcode not changed! - A jelkód nem változott meg! + A jelkód nem módosult! No comment provided by engineer. @@ -5665,7 +5733,7 @@ VPN engedélyezése szükséges. Password to show - Jelszó megjelenítése + Jelszó a megjelenítéshez No comment provided by engineer. @@ -5700,7 +5768,7 @@ VPN engedélyezése szükséges. Periodic - Rendszeresen + Időszakos No comment provided by engineer. @@ -5799,19 +5867,22 @@ Hiba: %@ Please store passphrase securely, you will NOT be able to change it if you lose it. - Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja megváltoztatni. + Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja módosítani. No comment provided by engineer. Please try to disable and re-enable notfications. + Próbálja meg letiltani és újra engedélyezni az értesítéseket. token info Please wait for token activation to complete. + Várjon, amíg a token aktiválása befejeződik. token info Please wait for token to be registered. + Várjon a token regisztrálására. token info @@ -5836,7 +5907,7 @@ Hiba: %@ Preset server address - Előre beállított kiszolgáló címe + Az előre beállított kiszolgáló címe No comment provided by engineer. @@ -5874,6 +5945,10 @@ Hiba: %@ Privát fájlnevek No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing Privát üzenet-útválasztás @@ -5896,7 +5971,7 @@ Hiba: %@ Private routing error - Privát útválasztáshiba + Privát útválasztási hiba No comment provided by engineer. @@ -5936,21 +6011,22 @@ Hiba: %@ Prohibit irreversible message deletion. - Az üzenetek véglegesen való törlése le van tiltva. + Az elküldött üzenetek végleges törlése le van tiltva. No comment provided by engineer. Prohibit message reactions. - Az üzenetreakciók küldése le van tiltva. + A reakciók hozzáadása az üzenethez le van tiltva. No comment provided by engineer. Prohibit messages reactions. - Az üzenetreakciók tiltása. + A reakciók hozzáadása az üzenetekhez le van tiltva. No comment provided by engineer. Prohibit reporting messages to moderators. + Az üzenetek a moderátorok felé történő jelentésének megtiltása. No comment provided by engineer. @@ -5970,7 +6046,7 @@ Hiba: %@ Prohibit sending files and media. - Fájlok- és a médiatartalmak küldésének letiltása. + A fájlok- és a médiatartalmak küldése le van tiltva. No comment provided by engineer. @@ -5992,7 +6068,7 @@ Hiba: %@ Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings. Védje IP-címét az ismerősei által kiválasztott üzenetváltó-továbbító-kiszolgálókkal szemben. -Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. +Engedélyezze a *Hálózat és kiszolgálók* menüben. No comment provided by engineer. @@ -6097,12 +6173,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Received at - Fogadva ekkor: + Fogadva No comment provided by engineer. Received at: %@ - Fogadva ekkor: %@ + Fogadva: %@ copied message info @@ -6132,7 +6208,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Receiving address will be changed to a different server. Address change will complete after sender comes online. - A fogadócím egy másik kiszolgálóra változik. A cím megváltoztatása a feladó online állapotba kerülése után fejeződik be. + A fogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása a feladó online állapotba kerülése után fejeződik be. No comment provided by engineer. @@ -6142,7 +6218,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Receiving via - Fogadás a + Fogadás a következőn keresztül: No comment provided by engineer. @@ -6197,12 +6273,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Record updated at - A bejegyzés frissítve + Bejegyzés frissítve No comment provided by engineer. Record updated at: %@ - A bejegyzés frissítve: %@ + Bejegyzés frissítve: %@ copied message info @@ -6212,14 +6288,17 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Register + Regisztrálás No comment provided by engineer. Register notification token? + Értesítési token regisztrálása? token info Registered + Regisztrálva token status text @@ -6280,7 +6359,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Renegotiate - Újraegyzetetés + Újraegyeztetés No comment provided by engineer. @@ -6360,10 +6439,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Report: (text.isEmpty ? reason.text : text) + Jelentés: (text.isEmpty ? reason.text : text) report in notification Reporting messages to moderators is prohibited. + Az üzenetek jelentése a moderátorok felé le van tiltva. No comment provided by engineer. @@ -6604,7 +6685,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Saved from - Elmentve innen: + Elmentve innen No comment provided by engineer. @@ -6659,7 +6740,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Search bar accepts invitation links. - A keresősáv elfogadja a meghívó-hivatkozásokat. + A keresősáv elfogadja a meghívási hivatkozásokat. No comment provided by engineer. @@ -6674,7 +6755,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Secure queue - Biztonságos sorbaállítás + Biztonságos sorba állítás server test step @@ -6709,7 +6790,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Selected chat preferences prohibit this message. - A kijelölt csevegési beállítások tiltják ezt az üzenetet. + A kijelölt csevegés-beállítások tiltják ezt az üzenetet. No comment provided by engineer. @@ -6724,7 +6805,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Self-destruct passcode changed! - Az önmegsemmisítő-jelkód megváltozott! + Az önmegsemmisítő-jelkód módosult! No comment provided by engineer. @@ -6764,7 +6845,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Send link previews - Hivatkozás előnézetek küldése + Hivatkozás előnézete No comment provided by engineer. @@ -6792,9 +6873,13 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Értesítések küldése No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas - Ötletek és kérdések beküldése + Ötletek és javaslatok No comment provided by engineer. @@ -6864,12 +6949,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Sent at - Elküldve ekkor: + Elküldve No comment provided by engineer. Sent at: %@ - Elküldve ekkor: %@ + Elküldve: %@ copied message info @@ -6939,7 +7024,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Server operator changed. - A kiszolgáló üzemeltetője megváltozott. + A kiszolgáló üzemeltetője módosult. alert title @@ -6949,17 +7034,17 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Server protocol changed. - A kiszolgáló-protokoll megváltozott. + A kiszolgáló-protokoll módosult. alert title Server requires authorization to create queues, check password - A kiszolgálónak engedélyre van szüksége a sorbaállítás létrehozásához, ellenőrizze jelszavát + A kiszolgálónak engedélyre van szüksége a sorba állítás létrehozásához, ellenőrizze a jelszavát server test error Server requires authorization to upload, check password - A kiszolgálónak engedélyre van szüksége a várólisták feltöltéséhez, ellenőrizze jelszavát + A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát server test error @@ -7029,7 +7114,11 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Set it instead of system authentication. - Rendszerhitelesítés helyetti beállítás. + Beállítás a rendszer-hitelesítés helyett. + No comment provided by engineer. + + + Set message expiration in chats. No comment provided by engineer. @@ -7049,7 +7138,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Set the message shown to new members! - Megjelenő üzenet beállítása az új tagok számára! + Megjelenítendő üzenet beállítása az új tagok számára! No comment provided by engineer. @@ -7064,7 +7153,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Settings were changed. - A beállítások megváltoztak. + A beállítások módosultak. alert message @@ -7080,12 +7169,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Share 1-time link - Egyszer használható hivatkozás megosztása + Egyszer használható meghívási hivatkozás megosztása No comment provided by engineer. Share 1-time link with a friend - Egyszer használható meghívó-hivatkozás megosztása egy baráttal + Egyszer használható meghívási hivatkozás megosztása egy baráttal No comment provided by engineer. @@ -7115,7 +7204,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Share link - Hivatkozás megosztása + Megosztás No comment provided by engineer. @@ -7125,12 +7214,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Share this 1-time invite link - Egyszer használható meghívó-hivatkozás megosztása + Ennek az egyszer használható meghívási hivatkozásnak a megosztása No comment provided by engineer. Share to SimpleX - Megosztás a SimpleX-ben + Megosztás a SimpleXben No comment provided by engineer. @@ -7160,7 +7249,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Show message status - Üzenetállapot megjelenítése + Üzenet állapotának megjelenítése No comment provided by engineer. @@ -7230,12 +7319,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX address and 1-time links are safe to share via any messenger. - A SimpleX-cím és az egyszer használható meghívó-hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül. + A SimpleX-cím és az egyszer használható meghívási hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül. No comment provided by engineer. SimpleX address or 1-time link? - SimpleX-cím vagy egyszer használható meghívó-hivatkozás? + SimpleX-cím vagy egyszer használható meghívási hivatkozás? No comment provided by engineer. @@ -7260,7 +7349,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX links are prohibited. - A SimpleX-hivatkozások küldése le van tiltva ebben a csoportban. + A SimpleX-hivatkozások küldése le van tiltva. No comment provided by engineer. @@ -7270,7 +7359,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX one-time invitation - Egyszer használható SimpleX-meghívó-hivatkozás + Egyszer használható SimpleX-meghívási hivatkozás simplex link type @@ -7368,7 +7457,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Starting from %@. - Kezdve ettől %@. + Statisztikagyűjtés kezdete: %@. No comment provided by engineer. @@ -7473,7 +7562,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Switch chat profile for 1-time invitations. - Csevegési profilváltás az egyszer használható meghívó-hivatkozásokhoz. + Csevegési profilváltás az egyszer használható meghívási hivatkozásokhoz. No comment provided by engineer. @@ -7483,7 +7572,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. System authentication - Rendszerhitelesítés + Rendszer-hitelesítés No comment provided by engineer. @@ -7498,6 +7587,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. TCP port for messaging + TCP-port az üzenetváltáshoz No comment provided by engineer. @@ -7552,7 +7642,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Tap to join incognito - Koppintson ide az inkognitóban való csatlakozáshoz + Koppintson ide az inkognitóban való kapcsolódáshoz No comment provided by engineer. @@ -7562,21 +7652,22 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Tap to scan - Koppintson ide a QR-kód beolvasáshoz + Koppintson ide a QR-kód beolvasásához No comment provided by engineer. Temporary file error - Ideiglenesfájl-hiba + Ideiglenes fájlhiba file error alert title Test failed at step %@. - A teszt sikertelen volt a(z) %@ lépésnél. + A teszt a(z) %@ lépésnél sikertelen volt. server test failure Test notifications + Értesítések tesztelése No comment provided by engineer. @@ -7623,7 +7714,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. The app protects your privacy by using different operators in each conversation. - Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetésben más-más üzemeltetőket használ. + Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ. No comment provided by engineer. @@ -7633,7 +7724,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. The attempt to change database passphrase was not completed. - Az adatbázis jelmondatának megváltoztatására tett kísérlet nem fejeződött be. + Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be. No comment provided by engineer. @@ -7653,7 +7744,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. The contact you shared this link with will NOT be able to connect! - Ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni! + Az ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni! No comment provided by engineer. @@ -7708,12 +7799,12 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. The same conditions will apply to operator **%@**. - Ugyanezek a feltételek lesznek elfogadva a következő üzemeltetőre is: **%@**. + Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára is: **%@**. No comment provided by engineer. The same conditions will apply to operator(s): **%@**. - Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**. + Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k) számára is: **%@**. No comment provided by engineer. @@ -7798,7 +7889,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. This chat is protected by quantum resistant end-to-end encryption. - Ez a csevegés végpontok közötti kvantumálló tikosítással védett. + Ez a csevegés végpontok közötti kvantumálló titkosítással védett. E2EE info chat item @@ -7828,7 +7919,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. This is your own one-time link! - Ez az Ön egyszer használható meghívó-hivatkozása! + Ez az Ön egyszer használható meghívási hivatkozása! No comment provided by engineer. @@ -7836,6 +7927,10 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. Ezt a hivatkozást egy másik hordozható eszközön már használták, hozzon létre egy új hivatkozást a számítógépén. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Ez a beállítás csak az Ön jelenlegi **%@** nevű csevegő-profiljában lévő üzenetekre vonatkozik. @@ -7890,7 +7985,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To protect your privacy, SimpleX uses separate IDs for each of your contacts. - Az adatvédelem érdekében (a más csevegési platformokon megszokott felhasználó-azonosítók helyett) a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, az összes ismerőséhez különbözőt. + Az Ön adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához. No comment provided by engineer. @@ -7915,7 +8010,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. - Rejtett profilja megjelenítéséhez adja meg a teljes jelszót a keresőmezőben, a **Csevegési profilok** menüben. + Rejtett profilja felfedéséhez adja meg a teljes jelszót a keresőmezőben, a **Csevegési profilok** menüben. No comment provided by engineer. @@ -7950,6 +8045,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Token status: %@. + Token állapota: %@. token status @@ -7964,7 +8060,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Transport isolation - Átvitel-izoláció módja + Átvitel-izoláció No comment provided by engineer. @@ -8009,7 +8105,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Unblock for all - Letiltás feloldása az összes tag számára + Feloldás No comment provided by engineer. @@ -8101,7 +8197,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Unlink desktop? - Számítógép szétkapcsolása? + Számítógép leválasztása? No comment provided by engineer. @@ -8136,12 +8232,12 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Update database passphrase - Az adatbázis jelmondatának megváltoztatása + Az adatbázis jelmondatának módosítása No comment provided by engineer. Update network settings? - Hálózati beállítások megváltoztatása? + Hálózati beállítások módosítása? No comment provided by engineer. @@ -8211,11 +8307,12 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Use TCP port %@ when no port is specified. + A következő TCP-port használata, amikor nincs port megadva: %@. No comment provided by engineer. Use chat - Simplex Chat használata + SimpleX Chat használata No comment provided by engineer. @@ -8290,6 +8387,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Use web port + Webport használata No comment provided by engineer. @@ -8404,7 +8502,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Voice messages are prohibited. - A hangüzenetek küldése le van tiltva ebben a csoportban. + A hangüzenetek küldése le van tiltva. No comment provided by engineer. @@ -8414,7 +8512,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Voice messages prohibited! - A hangüzenetek le vannak tilva! + A hangüzenetek le vannak tiltva! No comment provided by engineer. @@ -8454,7 +8552,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Warning: starting chat on multiple devices is not supported and will cause message delivery failures - Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, továbbá üzenetkézbesítési hibákat okozhat + Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, mert üzenetkézbesítési hibákat okoz No comment provided by engineer. @@ -8469,7 +8567,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Welcome %@! - Üdvözöllek %@! + Üdvözöljük %@! No comment provided by engineer. @@ -8499,7 +8597,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc When more than one operator is enabled, none of them has metadata to learn who communicates with whom. - Amikor egynél több hálózati üzemeltető van engedélyezve, egyikük sem rendelkezik olyan metaadatokkal ahhoz, hogy felderítse, ki kommunikál kivel. + Amikor egynél több üzemeltető van engedélyezve, akkor egyik sem rendelkezik olyan metaadatokkal, amelyekből megtudható, hogy ki kivel kommunikál. No comment provided by engineer. @@ -8599,7 +8697,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc You are already connected to %@. - Ön már kapcsolódva van a következőhöz: %@. + Ön már kapcsolódott a következőhöz: %@. No comment provided by engineer. @@ -8609,12 +8707,12 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc You are already connecting to %@. - Már folyamatban van a kapcsolódás a következőhöz: %@. + A kapcsolódás már folyamatban van a következőhöz: %@. No comment provided by engineer. You are already connecting via this one-time link! - A kapcsolódás már folyamatban van ezen az egyszer használható meghívó-hivatkozáson keresztül! + A kapcsolódás már folyamatban van ezen az egyszer használható meghívási hivatkozáson keresztül! No comment provided by engineer. @@ -8640,7 +8738,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc You are already joining the group! Repeat join request? - Csatlakozás folyamatban! + A csatlakozás már folyamatban van a csoporthoz! Csatlakozáskérés megismétlése? No comment provided by engineer. @@ -8651,7 +8749,7 @@ Csatlakozáskérés megismétlése? You are invited to group - Meghívást kapott a csoportba + Ön meghívást kapott a csoportba No comment provided by engineer. @@ -8666,12 +8764,12 @@ Csatlakozáskérés megismétlése? You can change it in Appearance settings. - Ezt a „Megjelenés” menüben változtathatja meg. + Ezt a „Megjelenés” menüben módosíthatja. No comment provided by engineer. You can configure operators in Network & servers settings. - Az üzemeltetőket a „Hálózat és kiszolgálók” beállításaban konfigurálhatja. + Az üzemeltetőket a „Hálózat és kiszolgálók” menüben konfigurálhatja. No comment provided by engineer. @@ -8691,7 +8789,7 @@ Csatlakozáskérés megismétlése? You can enable them later via app Privacy & Security settings. - Később engedélyezheti őket az alkalmazás „Adatvédelem és biztonság” menüjében. + Később engedélyezheti őket az „Adatvédelem és biztonság” menüben. No comment provided by engineer. @@ -8736,7 +8834,7 @@ Csatlakozáskérés megismétlése? You can share this address with your contacts to let them connect with **%@**. - Megoszthatja ezt a címet az ismerőseivel, hogy kapcsolatba léphessenek Önnel a(z) **%@** nevű profilján keresztül. + Megoszthatja ezt a SimpleX-címet az ismerőseivel, hogy kapcsolatba léphessenek vele: **%@**. No comment provided by engineer. @@ -8761,7 +8859,7 @@ Csatlakozáskérés megismétlése? You can view invitation link again in connection details. - A meghívó-hivatkozást újra megtekintheti a kapcsolat részleteinél. + A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél. alert message @@ -8798,7 +8896,7 @@ Kapcsolatkérés megismétlése? You invited a contact - Meghívta egy ismerősét + Ön meghívta egy ismerősét No comment provided by engineer. @@ -8848,6 +8946,7 @@ Kapcsolatkérés megismétlése? You should receive notifications. + Ön megkapja az értesítéseket. token info @@ -8872,7 +8971,7 @@ Kapcsolatkérés megismétlése? You will be required to authenticate when you start or resume the app after 30 seconds in background. - Az alkalmazás indításakor, vagy 30 másodpercnyi háttérben töltött idő után az alkalmazáshoz visszatérve hitelesítés szükséges. + Az alkalmazás elindításához vagy 30 másodpercnyi háttérben töltött idő után, az alkalmazáshoz való visszatéréshez hitelesítésre lesz szükség. No comment provided by engineer. @@ -8942,7 +9041,7 @@ Kapcsolatkérés megismétlése? Your chat preferences - Csevegési beállítások + Az Ön csevegés-beállításai alert title @@ -8957,7 +9056,7 @@ Kapcsolatkérés megismétlése? Your contact sent a file that is larger than currently supported maximum size (%@). - Az ismerőse olyan fájlt küldött, amely meghaladja a jelenleg támogatott maximális méretet (%@). + Az ismerőse a jelenleg megengedett maximális méretű (%@) fájlnál nagyobbat küldött. No comment provided by engineer. @@ -9012,7 +9111,7 @@ Kapcsolatkérés megismétlése? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. - A profilja megváltozott. Ha elmenti, a profilfrissítés el lesz küldve az Ön ismerősei számára. + A profilja módosult. Ha elmenti, a profilfrissítés el lesz küldve az Ön ismerősei számára. alert message @@ -9072,7 +9171,7 @@ Kapcsolatkérés megismétlése? accepted call - elfogadott hívás + fogadott hívás call status @@ -9152,7 +9251,7 @@ Kapcsolatkérés megismétlése? blocked %@ - letiltotta %@-t + letiltotta őt: %@ rcv group event chat item @@ -9192,32 +9291,32 @@ Kapcsolatkérés megismétlése? changed address for you - megváltoztatta a címet az Ön számára + módosította a címet az Ön számára chat item text changed role of %1$@ to %2$@ - %1$@ szerepkörét megváltoztatta a következőre: %2$@ + a következőre módosította %1$@ szerepkörét: „%2$@” rcv group event chat item changed your role to %@ - megváltoztatta az Ön szerepkörét a következőre: %@ + a következőre módosította az Ön szerepkörét: „%@” rcv group event chat item changing address for %@… - cím megváltoztatása %@ számára… + cím módosítása %@ számára… chat item text changing address… - cím megváltoztatása… + cím módosítása… chat item text colored - színes + színezett No comment provided by engineer. @@ -9262,7 +9361,7 @@ Kapcsolatkérés megismétlése? connecting (introduction invitation) - kapcsolódás (bemutatkozó-meghívó) + kapcsolódás (bemutatkozó meghívó) No comment provided by engineer. @@ -9287,17 +9386,17 @@ Kapcsolatkérés megismétlése? contact %1$@ changed to %2$@ - %1$@ megváltoztatta a nevét a következőre: %2$@ + %1$@ a következőre módosította a nevét: %2$@ profile update event chat item contact has e2e encryption - az ismerősnél az e2e titkosítás elérhető + az ismerős e2e titkosítással rendelkezik No comment provided by engineer. contact has no e2e encryption - az ismerősnél az e2e titkosítás nem elérhető + az ismerős nem rendelkezik e2e titkosítással No comment provided by engineer. @@ -9307,7 +9406,7 @@ Kapcsolatkérés megismétlése? custom - egyedi + egyéni dropdown time picker choice @@ -9358,7 +9457,7 @@ Kapcsolatkérés megismétlése? different migration in the app/database: %@ / %@ - különböző átköltöztetések az alkalmazásban/adatbázisban: %@ / %@ + különböző átköltöztetés az alkalmazásban/adatbázisban: %@ / %@ No comment provided by engineer. @@ -9418,12 +9517,12 @@ Kapcsolatkérés megismétlése? encryption ok for %@ - titkosítás rendben van %@ számára + titkosítás rendben %@ számára chat item text encryption re-negotiation allowed - titkosítás-újraegyeztetés engedélyezve + a titkosítás újraegyeztetése engedélyezve van chat item text @@ -9433,7 +9532,7 @@ Kapcsolatkérés megismétlése? encryption re-negotiation required - titkosítás-újraegyeztetés szükséges + a titkosítás újraegyeztetése szükséges chat item text @@ -9498,7 +9597,7 @@ Kapcsolatkérés megismétlése? iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. - Az iOS kulcstartó biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat megváltoztatása után – lehetővé teszi a push-értesítések fogadását. + Az iOS kulcstartó biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat módosítása után – lehetővé teszi a push-értesítések fogadását. No comment provided by engineer. @@ -9518,7 +9617,7 @@ Kapcsolatkérés megismétlése? incognito via one-time link - inkognitó egy egyszer használható meghívó-hivatkozáson keresztül + inkognitó egy egyszer használható meghívási hivatkozáson keresztül chat list item description @@ -9578,7 +9677,7 @@ Kapcsolatkérés megismétlése? join as %@ - csatlakozás mint: %@ + csatlakozás mint %@ No comment provided by engineer. @@ -9598,7 +9697,7 @@ Kapcsolatkérés megismétlése? member %1$@ changed to %2$@ - %1$@ megváltoztatta a nevét a következőre: %2$@ + %1$@ a következőre módosította a nevét: %2$@ profile update event chat item @@ -9690,7 +9789,7 @@ Kapcsolatkérés megismétlése? offered %1$@: %2$@ - ajánlotta %1$@: %2$@-kor + ajánlotta: %1$@, ekkor: %2$@ feature offered item @@ -9720,7 +9819,7 @@ Kapcsolatkérés megismétlése? peer-to-peer - ponttól-pontig + egyenrangú No comment provided by engineer. @@ -9805,7 +9904,7 @@ Kapcsolatkérés megismétlése? security code changed - a biztonsági kód megváltozott + a biztonsági kód módosult chat item text @@ -9817,14 +9916,14 @@ Kapcsolatkérés megismétlése? server queue info: %1$@ last received msg: %2$@ - a kiszolgáló üzenet-sorbaállítási információi: %1$@ + a kiszolgáló sorbaállítási információi: %1$@ utoljára fogadott üzenet: %2$@ queue info set new contact address - új kapcsolattartási cím beállítása + új kapcsolattartási címet állított be profile update event chat item @@ -9909,7 +10008,7 @@ utoljára fogadott üzenet: %2$@ via one-time link - egy egyszer használható meghívó-hivatkozáson keresztül + egy egyszer használható meghívási hivatkozáson keresztül chat list item description @@ -9964,7 +10063,7 @@ utoljára fogadott üzenet: %2$@ you are invited to group - meghívást kapott a csoportba + Ön meghívást kapott a csoportba No comment provided by engineer. @@ -9979,22 +10078,22 @@ utoljára fogadott üzenet: %2$@ you changed address - Ön megváltoztatta a címet + Ön módosította a címet chat item text you changed address for %@ - Ön megváltoztatta a címet %@ számára + Ön módosította a címet %@ számára chat item text you changed role for yourself to %@ - Ön megváltoztatta a saját szerepkörét a következőre: %@ + Ön a következőre módosította a saját szerepkörét: „%@” snd group event chat item you changed role of %1$@ to %2$@ - Ön megváltoztatta %1$@ szerepkörét a következőre: %@ + Ön a következőre módosította %1$@ szerepkörét: „%2$@” snd group event chat item @@ -10009,12 +10108,12 @@ utoljára fogadott üzenet: %2$@ you shared one-time link - Ön egy egyszer használható meghívó-hivatkozást osztott meg + Ön egy egyszer használható meghívási hivatkozást osztott meg chat list item description you shared one-time link incognito - Ön egy egyszer használható meghívó-hivatkozást osztott meg inkognitóban + Ön egy egyszer használható meghívási hivatkozást osztott meg inkognitóban chat list item description @@ -10184,7 +10283,7 @@ utoljára fogadott üzenet: %2$@ Currently maximum supported file size is %@. - Jelenleg a maximálisan támogatott fájlméret: %@. + Jelenleg támogatott legnagyobb fájl méret: %@. No comment provided by engineer. @@ -10289,7 +10388,7 @@ utoljára fogadott üzenet: %2$@ Selected chat preferences prohibit this message. - A kijelölt csevegési beállítások tiltják ezt az üzenetet. + A kijelölt csevegés-beállítások tiltják ezt az üzenetet. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 8dd8b44054..c01699933c 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -545,10 +545,12 @@ @%@ + @%@ No comment provided by engineer. @'%@' + @'%@' No comment provided by engineer. @@ -653,6 +655,7 @@ Active + Attivo token status text @@ -842,6 +845,7 @@ All reports will be archived for you. + Tutte le segnalazioni verranno archiviate per te. No comment provided by engineer. @@ -921,6 +925,7 @@ Allow to report messsages to moderators. + Consenti di segnalare messaggi ai moderatori. No comment provided by engineer. @@ -1090,10 +1095,12 @@ Archive %lld reports? + Archiviare %lld segnalazioni? No comment provided by engineer. Archive all reports? + Archiviare tutte le segnalazioni? No comment provided by engineer. @@ -1118,6 +1125,7 @@ Archive reports + Archivia segnalazioni swipe action @@ -1235,6 +1243,10 @@ Gruppi migliorati No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. Date dei messaggi migliorate. @@ -1255,6 +1267,10 @@ Notifiche migliorate No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ Sicurezza migliorata ✅ @@ -1679,6 +1695,7 @@ Clear group? + Svuotare il gruppo? No comment provided by engineer. @@ -1828,6 +1845,7 @@ Confirmed + Confermato token status text @@ -2845,6 +2863,10 @@ Questo è il tuo link una tantum! Non attivare No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Non mostrare più @@ -3203,6 +3225,7 @@ Questo è il tuo link una tantum! Error checking token status + Errore di controllo dello stato del token No comment provided by engineer. @@ -3362,6 +3385,7 @@ Questo è il tuo link una tantum! Error registering for notifications + Errore di registrazione per le notifiche alert title @@ -3471,6 +3495,7 @@ Questo è il tuo link una tantum! Error testing server connection + Errore provando la connessione al server No comment provided by engineer. @@ -3555,6 +3580,7 @@ Questo è il tuo link una tantum! Expired + Scaduto token status text @@ -3597,11 +3623,19 @@ Questo è il tuo link una tantum! Veloce e senza aspettare che il mittente sia in linea! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Ingresso più veloce e messaggi più affidabili. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Preferito @@ -3758,6 +3792,7 @@ Questo è il tuo link una tantum! For all moderators + Per tutti i moderatori No comment provided by engineer. @@ -3777,6 +3812,7 @@ Questo è il tuo link una tantum! For me + Per me No comment provided by engineer. @@ -3903,6 +3939,10 @@ Errore: %2$@ GIF e adesivi No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! Buon pomeriggio! @@ -4018,6 +4058,10 @@ Errore: %2$@ Aiuto No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Nascosta @@ -4329,22 +4373,27 @@ Altri miglioramenti sono in arrivo! Invalid + Non valido token status text Invalid (bad token) + Non valido (token corrotto) token status text Invalid (expired) + Non valido (scaduto) token status text Invalid (unregistered) + Non valido (non registrato) token status text Invalid (wrong topic) + Non valido (argomento sbagliato) token status text @@ -4727,6 +4776,7 @@ Questo è il tuo link per il gruppo %@! Member reports + Segnalazioni dei membri chat feature @@ -4766,6 +4816,7 @@ Questo è il tuo link per il gruppo %@! Members can report messsages to moderators. + I membri possono segnalare messaggi ai moderatori. No comment provided by engineer. @@ -4793,6 +4844,10 @@ Questo è il tuo link per il gruppo %@! I membri del gruppo possono inviare messaggi vocali. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus Menu @@ -5045,6 +5100,7 @@ Questo è il tuo link per il gruppo %@! Mute all + Silenzia tutto notification label action @@ -5099,6 +5155,7 @@ Questo è il tuo link per il gruppo %@! New + Nuovo token status text @@ -5256,6 +5313,10 @@ Questo è il tuo link per il gruppo %@! Nessun server di multimediali e file. servers error + + No message + No comment provided by engineer. + No message servers. Nessun server dei messaggi. @@ -5313,6 +5374,7 @@ Questo è il tuo link per il gruppo %@! No token! + Nessun token! alert title @@ -5357,6 +5419,7 @@ Questo è il tuo link per il gruppo %@! Notifications error + Errore delle notifiche alert title @@ -5366,6 +5429,7 @@ Questo è il tuo link per il gruppo %@! Notifications status + Stato delle notifiche alert title @@ -5611,6 +5675,10 @@ Richiede l'attivazione della VPN. O per condividere in modo privato No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Altro @@ -5804,14 +5872,17 @@ Errore: %@ Please try to disable and re-enable notfications. + Prova a disattivare e riattivare le notifiche. token info Please wait for token activation to complete. + Attendi il completamento dell'attivazione del token. token info Please wait for token to be registered. + Attendi la registrazione del token. token info @@ -5874,6 +5945,10 @@ Errore: %@ Nomi di file privati No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing Instradamento privato dei messaggi @@ -5951,6 +6026,7 @@ Errore: %@ Prohibit reporting messages to moderators. + Vieta di segnalare messaggi ai moderatori. No comment provided by engineer. @@ -6212,14 +6288,17 @@ Attivalo nelle impostazioni *Rete e server*. Register + Registra No comment provided by engineer. Register notification token? + Registrare il token di notifica? token info Registered + Registrato token status text @@ -6360,10 +6439,12 @@ Attivalo nelle impostazioni *Rete e server*. Report: (text.isEmpty ? reason.text : text) + Segnalazione: (text.isEmpty ? reason.text : text) report in notification Reporting messages to moderators is prohibited. + È vietato segnalare messaggi ai moderatori. No comment provided by engineer. @@ -6792,6 +6873,10 @@ Attivalo nelle impostazioni *Rete e server*. Invia notifiche No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Invia domande e idee @@ -7032,6 +7117,10 @@ Attivalo nelle impostazioni *Rete e server*. Impostalo al posto dell'autenticazione di sistema. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Imposta codice @@ -7498,6 +7587,7 @@ Attivalo nelle impostazioni *Rete e server*. TCP port for messaging + Porta TCP per i messaggi No comment provided by engineer. @@ -7577,6 +7667,7 @@ Attivalo nelle impostazioni *Rete e server*. Test notifications + Prova le notifiche No comment provided by engineer. @@ -7836,6 +7927,10 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Questa impostazione si applica ai messaggi del profilo di chat attuale **%@**. @@ -7950,6 +8045,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Token status: %@. + Stato del token: %@. token status @@ -8211,6 +8307,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Use TCP port %@ when no port is specified. + Usa la porta TCP %@ quando non è specificata alcuna porta. No comment provided by engineer. @@ -8290,6 +8387,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Use web port + Usa porta web No comment provided by engineer. @@ -8848,6 +8946,7 @@ Ripetere la richiesta di connessione? You should receive notifications. + Dovresti ricevere le notifiche. token info diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 9884967d68..81ceeaecba 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -1191,6 +1191,10 @@ Better groups No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. No comment provided by engineer. @@ -1208,6 +1212,10 @@ Better notifications No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ No comment provided by engineer. @@ -2668,6 +2676,10 @@ This is your own one-time link! 有効にしない No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again 次から表示しない @@ -3365,10 +3377,18 @@ This is your own one-time link! 送信者がオンラインになるまでの待ち時間がなく、速い! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite お気に入り @@ -3634,6 +3654,10 @@ Error: %2$@ GIFとステッカー No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! message preview @@ -3744,6 +3768,10 @@ Error: %2$@ ヘルプ No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden プライベート @@ -4469,6 +4497,10 @@ This is your link for group %@! グループのメンバーが音声メッセージを送信できます。 No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4887,6 +4919,10 @@ This is your link for group %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -5207,6 +5243,10 @@ VPN を有効にする必要があります。 Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other No comment provided by engineer. @@ -5449,6 +5489,10 @@ Error: %@ プライベートなファイル名 No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing No comment provided by engineer. @@ -6289,6 +6333,10 @@ Enable in *Network & servers* settings. 通知を送信する No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas 質問やアイデアを送る @@ -6500,6 +6548,10 @@ Enable in *Network & servers* settings. システム認証の代わりに設定します。 No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode パスコードを設定する @@ -7232,6 +7284,10 @@ It can happen because of some bug or when the connection is compromised.This link was used with another mobile device, please create a new link on the desktop. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. この設定は現在のチャットプロフィール **%@** のメッセージに適用されます。 diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 044ea87cd6..5da67ed1e6 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -545,10 +545,12 @@ @%@ + @%@ No comment provided by engineer. @'%@' + @'%@' No comment provided by engineer. @@ -653,6 +655,7 @@ Active + actief token status text @@ -842,6 +845,7 @@ All reports will be archived for you. + Alle rapporten worden voor u gearchiveerd. No comment provided by engineer. @@ -921,6 +925,7 @@ Allow to report messsages to moderators. + Hiermee kunt u berichten rapporteren aan moderators. No comment provided by engineer. @@ -1090,10 +1095,12 @@ Archive %lld reports? + %lld rapporten archiveren? No comment provided by engineer. Archive all reports? + Alle rapporten archiveren? No comment provided by engineer. @@ -1118,6 +1125,7 @@ Archive reports + Rapporten archiveren swipe action @@ -1235,6 +1243,10 @@ Betere groepen No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. Betere datums voor berichten. @@ -1255,6 +1267,10 @@ Betere meldingen No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ Betere beveiliging ✅ @@ -1679,6 +1695,7 @@ Clear group? + Groep wissen? No comment provided by engineer. @@ -1828,6 +1845,7 @@ Confirmed + Bevestigd token status text @@ -2845,6 +2863,10 @@ Dit is uw eigen eenmalige link! Niet inschakelen No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Niet meer weergeven @@ -3203,6 +3225,7 @@ Dit is uw eigen eenmalige link! Error checking token status + Fout bij het controleren van de tokenstatus No comment provided by engineer. @@ -3362,6 +3385,7 @@ Dit is uw eigen eenmalige link! Error registering for notifications + Fout bij registreren voor meldingen alert title @@ -3471,6 +3495,7 @@ Dit is uw eigen eenmalige link! Error testing server connection + Fout bij het testen van de serververbinding No comment provided by engineer. @@ -3555,6 +3580,7 @@ Dit is uw eigen eenmalige link! Expired + Verlopen token status text @@ -3597,11 +3623,19 @@ Dit is uw eigen eenmalige link! Snel en niet wachten tot de afzender online is! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Snellere deelname en betrouwbaardere berichten. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Favoriet @@ -3758,6 +3792,7 @@ Dit is uw eigen eenmalige link! For all moderators + Voor alle moderators No comment provided by engineer. @@ -3777,6 +3812,7 @@ Dit is uw eigen eenmalige link! For me + Voor mij No comment provided by engineer. @@ -3903,6 +3939,10 @@ Fout: %2$@ GIF's en stickers No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! Goedemiddag! @@ -4018,6 +4058,10 @@ Fout: %2$@ Help No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Verborgen @@ -4329,22 +4373,27 @@ Binnenkort meer verbeteringen! Invalid + Ongeldig token status text Invalid (bad token) + Ongeldig (ongeldig token) token status text Invalid (expired) + Ongeldig (verlopen) token status text Invalid (unregistered) + Ongeldig (niet geregistreerd) token status text Invalid (wrong topic) + Ongeldig (verkeerd onderwerp) token status text @@ -4727,6 +4776,7 @@ Dit is jouw link voor groep %@! Member reports + Ledenrapporten chat feature @@ -4766,6 +4816,7 @@ Dit is jouw link voor groep %@! Members can report messsages to moderators. + Leden kunnen berichten melden bij moderators. No comment provided by engineer. @@ -4793,6 +4844,10 @@ Dit is jouw link voor groep %@! Groepsleden kunnen spraak berichten verzenden. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus Menu's @@ -5045,6 +5100,7 @@ Dit is jouw link voor groep %@! Mute all + Alles dempen notification label action @@ -5099,6 +5155,7 @@ Dit is jouw link voor groep %@! New + Nieuw token status text @@ -5256,6 +5313,10 @@ Dit is jouw link voor groep %@! Geen media- en bestandsservers. servers error + + No message + No comment provided by engineer. + No message servers. Geen berichtenservers. @@ -5313,6 +5374,7 @@ Dit is jouw link voor groep %@! No token! + Geen token! alert title @@ -5357,6 +5419,7 @@ Dit is jouw link voor groep %@! Notifications error + Meldingsfout alert title @@ -5366,6 +5429,7 @@ Dit is jouw link voor groep %@! Notifications status + Meldingsstatus alert title @@ -5611,6 +5675,10 @@ Vereist het inschakelen van VPN. Of om privé te delen No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Ander @@ -5804,14 +5872,17 @@ Fout: %@ Please try to disable and re-enable notfications. + Probeer meldingen uit en weer in te schakelen. token info Please wait for token activation to complete. + Wacht tot de tokenactivering voltooid is. token info Please wait for token to be registered. + Wacht tot het token is geregistreerd. token info @@ -5874,6 +5945,10 @@ Fout: %@ Privé bestandsnamen No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing Routering van privéberichten @@ -5951,6 +6026,7 @@ Fout: %@ Prohibit reporting messages to moderators. + Het melden van berichten aan moderators is niet toegestaan. No comment provided by engineer. @@ -6212,14 +6288,17 @@ Schakel dit in in *Netwerk en servers*-instellingen. Register + Register No comment provided by engineer. Register notification token? + Meldingstoken registreren? token info Registered + Geregistreerd token status text @@ -6360,10 +6439,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Report: (text.isEmpty ? reason.text : text) + Rapport: (text.isEmpty ? reason.text : text) report in notification Reporting messages to moderators is prohibited. + Het is niet toegestaan om berichten aan moderators te melden. No comment provided by engineer. @@ -6792,6 +6873,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Meldingen verzenden No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Stuur vragen en ideeën @@ -7032,6 +7117,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Stel het in in plaats van systeemverificatie. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Toegangscode instellen @@ -7498,6 +7587,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. TCP port for messaging + TCP-poort voor berichtenuitwisseling No comment provided by engineer. @@ -7577,6 +7667,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Test notifications + Testmeldingen No comment provided by engineer. @@ -7836,6 +7927,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Deze link is gebruikt met een ander mobiel apparaat. Maak een nieuwe link op de desktop. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Deze instelling is van toepassing op berichten in je huidige chatprofiel **%@**. @@ -7950,6 +8045,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Token status: %@. + Tokenstatus: %@. token status @@ -8211,6 +8307,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Use TCP port %@ when no port is specified. + Gebruik TCP-poort %@ als er geen poort is opgegeven. No comment provided by engineer. @@ -8290,6 +8387,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Use web port + Gebruik een webpoort No comment provided by engineer. @@ -8848,6 +8946,7 @@ Verbindingsverzoek herhalen? You should receive notifications. + U zou meldingen moeten ontvangen. token info @@ -9829,7 +9928,7 @@ laatst ontvangen bericht: %2$@ set new profile picture - nieuwe profielfoto instellen + nieuwe profielfoto profile update event chat item diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 1c39ed50ba..87200f0c24 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -544,10 +544,12 @@ @%@ + @%@ No comment provided by engineer. @'%@' + @'%@' No comment provided by engineer. @@ -652,6 +654,7 @@ Active + Aktywne token status text @@ -816,6 +819,7 @@ All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + Wszystkie wiadomości i pliki są wysyłane **z szyfrowaniem end-to-end**, z bezpieczeństwem postkwantowym w wiadomościach bezpośrednich. No comment provided by engineer. @@ -840,6 +844,7 @@ All reports will be archived for you. + Wszystkie raporty zostaną dla Ciebie zarchiwizowane. No comment provided by engineer. @@ -919,6 +924,7 @@ Allow to report messsages to moderators. + Zezwól na zgłaszanie wiadomości moderatorom. No comment provided by engineer. @@ -1003,6 +1009,7 @@ Another reason + Inny powód report reason @@ -1032,6 +1039,7 @@ App group: + Grupa aplikacji: No comment provided by engineer. @@ -1081,14 +1089,17 @@ Archive + Archiwizuj No comment provided by engineer. Archive %lld reports? + Archiwizować %lld reports? No comment provided by engineer. Archive all reports? + Archiwizować wszystkie zgłoszenia? No comment provided by engineer. @@ -1103,14 +1114,17 @@ Archive report + Archiwizuj zgłoszenie No comment provided by engineer. Archive report? + Archiwizować zgłoszenie? No comment provided by engineer. Archive reports + Archiwizuj zgłoszenia swipe action @@ -1125,6 +1139,7 @@ Ask + Zapytaj No comment provided by engineer. @@ -1227,6 +1242,10 @@ Lepsze grupy No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. Lepsze daty wiadomości. @@ -1247,6 +1266,10 @@ Lepsze powiadomienia No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ Lepsze zabezpieczenia ✅ @@ -2808,6 +2831,10 @@ To jest twój jednorazowy link! Nie włączaj No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Nie pokazuj ponownie @@ -3546,11 +3573,19 @@ To jest twój jednorazowy link! Szybko i bez czekania aż nadawca będzie online! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Szybsze dołączenie i bardziej niezawodne wiadomości. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Ulubione @@ -3844,6 +3879,10 @@ Błąd: %2$@ GIF-y i naklejki No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! Dzień dobry! @@ -3958,6 +3997,10 @@ Błąd: %2$@ Pomoc No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Ukryte @@ -4718,6 +4761,10 @@ To jest twój link do grupy %@! Członkowie grupy mogą wysyłać wiadomości głosowe. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus Menu @@ -5170,6 +5217,10 @@ To jest twój link do grupy %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -5505,6 +5556,10 @@ Wymaga włączenia VPN. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Inne @@ -5766,6 +5821,10 @@ Błąd: %@ Prywatne nazwy plików No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing Trasowanie prywatnych wiadomości @@ -6673,6 +6732,10 @@ Włącz w ustawianiach *Sieć i serwery* . Wyślij powiadomienia No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Wyślij pytania i pomysły @@ -6908,6 +6971,10 @@ Włącz w ustawianiach *Sieć i serwery* . Ustaw go zamiast uwierzytelniania systemowego. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Ustaw pin @@ -7690,6 +7757,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Ten link dostał użyty z innym urządzeniem mobilnym, proszę stworzyć nowy link na komputerze. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. To ustawienie dotyczy wiadomości Twojego bieżącego profilu czatu **%@**. @@ -8868,6 +8939,7 @@ Powtórzyć prośbę połączenia? Your servers + Twoje serwery No comment provided by engineer. @@ -9946,6 +10018,7 @@ ostatnia otrzymana wiadomość: %2$@ New messages in %d chats + Nowe wiadomości w %d czatach notification body diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 9bae61e56d..a7a8f1290a 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -1231,6 +1231,10 @@ Улучшенные группы No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. Улучшенные даты сообщений. @@ -1251,6 +1255,10 @@ Улучшенные уведомления No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ Улучшенная безопасность ✅ @@ -2824,6 +2832,10 @@ This is your own one-time link! Не включать No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Не показывать @@ -3570,11 +3582,19 @@ This is your own one-time link! Быстрые и не нужно ждать, когда отправитель онлайн! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Быстрое вступление и надежная доставка сообщений. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Избранный @@ -3873,6 +3893,10 @@ Error: %2$@ ГИФ файлы и стикеры No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! Добрый день! @@ -3987,6 +4011,10 @@ Error: %2$@ Помощь No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Скрытое @@ -4755,6 +4783,10 @@ This is your link for group %@! Члены группы могут отправлять голосовые сообщения. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus Меню @@ -5213,6 +5245,10 @@ This is your link for group %@! Нет серверов файлов и медиа. servers error + + No message + No comment provided by engineer. + No message servers. Нет серверов сообщений. @@ -5561,6 +5597,10 @@ Requires compatible VPN. Или поделиться конфиденциально No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Другaя сеть @@ -5824,6 +5864,10 @@ Error: %@ Защищенные имена файлов No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing Конфиденциальная доставка сообщений @@ -6733,6 +6777,10 @@ Enable in *Network & servers* settings. Отправлять уведомления No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Отправьте вопросы и идеи @@ -6972,6 +7020,10 @@ Enable in *Network & servers* settings. Установите код вместо системной аутентификации. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Установить код доступа @@ -7773,6 +7825,10 @@ It can happen because of some bug or when the connection is compromised.Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Эта настройка применяется к сообщениям в Вашем текущем профиле чата **%@**. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 7239c30b44..bfbfe297b2 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -1134,6 +1134,10 @@ Better groups No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. No comment provided by engineer. @@ -1151,6 +1155,10 @@ Better notifications No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ No comment provided by engineer. @@ -2583,6 +2591,10 @@ This is your own one-time link! อย่าเปิดใช้งาน No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again ไม่ต้องแสดงอีก @@ -3277,10 +3289,18 @@ This is your own one-time link! รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite ที่ชอบ @@ -3546,6 +3566,10 @@ Error: %2$@ GIFs และสติกเกอร์ No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! message preview @@ -3656,6 +3680,10 @@ Error: %2$@ ความช่วยเหลือ No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden ซ่อนอยู่ @@ -4379,6 +4407,10 @@ This is your link for group %@! สมาชิกกลุ่มสามารถส่งข้อความเสียง No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4792,6 +4824,10 @@ This is your link for group %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -5109,6 +5145,10 @@ Requires compatible VPN. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other No comment provided by engineer. @@ -5351,6 +5391,10 @@ Error: %@ ชื่อไฟล์ส่วนตัว No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing No comment provided by engineer. @@ -6189,6 +6233,10 @@ Enable in *Network & servers* settings. ส่งการแจ้งเตือน No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas ส่งคําถามและความคิด @@ -6405,6 +6453,10 @@ Enable in *Network & servers* settings. ตั้งแทนการรับรองความถูกต้องของระบบ No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode ตั้งรหัสผ่าน @@ -7134,6 +7186,10 @@ It can happen because of some bug or when the connection is compromised.This link was used with another mobile device, please create a new link on the desktop. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. การตั้งค่านี้ใช้กับข้อความในโปรไฟล์แชทปัจจุบันของคุณ **%@** diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 0963a750aa..de90854e24 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -1222,6 +1222,10 @@ Daha iyi gruplar No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. Daha iyi mesaj tarihleri. @@ -1242,6 +1246,10 @@ Daha iyi bildirimler No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ Daha iyi güvenlik ✅ @@ -2814,6 +2822,10 @@ Bu senin kendi tek kullanımlık bağlantın! Etkinleştirme No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Yeniden gösterme @@ -3560,11 +3572,19 @@ Bu senin kendi tek kullanımlık bağlantın! Hızlı ve gönderici çevrimiçi olana kadar beklemek yok! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Daha hızlı katılma ve daha güvenilir mesajlar. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Favori @@ -3863,6 +3883,10 @@ Hata: %2$@ GİFler ve çıkartmalar No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! İyi öğlenler! @@ -3977,6 +4001,10 @@ Hata: %2$@ Yardım No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Gizlenmiş @@ -4745,6 +4773,10 @@ Bu senin grup için bağlantın %@! Grup üyeleri sesli mesajlar gönderebilir. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus Menüler @@ -5197,6 +5229,10 @@ Bu senin grup için bağlantın %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -5532,6 +5568,10 @@ VPN'nin etkinleştirilmesi gerekir. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Diğer @@ -5793,6 +5833,10 @@ Hata: %@ Gizli dosya adları No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing Gizli mesaj yönlendirme @@ -6700,6 +6744,10 @@ Enable in *Network & servers* settings. Bildirimler gönder No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Fikirler ve sorular gönderin @@ -6935,6 +6983,10 @@ Enable in *Network & servers* settings. Sistem kimlik doğrulaması yerine ayarla. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Şifre ayarla @@ -7720,6 +7772,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Bu bağlantı başka bir mobil cihazda kullanıldı, lütfen masaüstünde yeni bir bağlantı oluşturun. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Bu ayar, geçerli sohbet profiliniz **%@** deki mesajlara uygulanır. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 8d48227e77..61eb8d8206 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -1222,6 +1222,10 @@ Кращі групи No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. Кращі дати повідомлень. @@ -1242,6 +1246,10 @@ Кращі сповіщення No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ Краща безпека ✅ @@ -2815,6 +2823,10 @@ This is your own one-time link! Не вмикати No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Більше не показувати @@ -3561,11 +3573,19 @@ This is your own one-time link! Швидко і без очікування, поки відправник буде онлайн! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Швидше приєднання та надійніші повідомлення. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Улюблений @@ -3864,6 +3884,10 @@ Error: %2$@ GIF-файли та наклейки No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! Доброго дня! @@ -3978,6 +4002,10 @@ Error: %2$@ Довідка No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Приховано @@ -4747,6 +4775,10 @@ This is your link for group %@! Учасники групи можуть надсилати голосові повідомлення. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus Меню @@ -5205,6 +5237,10 @@ This is your link for group %@! Ніяких медіа та файлових серверів. servers error + + No message + No comment provided by engineer. + No message servers. Ніяких серверів повідомлень. @@ -5553,6 +5589,10 @@ Requires compatible VPN. Або поділитися приватно No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Інше @@ -5816,6 +5856,10 @@ Error: %@ Приватні імена файлів No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing Маршрутизація приватних повідомлень @@ -6725,6 +6769,10 @@ Enable in *Network & servers* settings. Надсилати сповіщення No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Надсилайте запитання та ідеї @@ -6964,6 +7012,10 @@ Enable in *Network & servers* settings. Встановіть його замість аутентифікації системи. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Встановити пароль @@ -7765,6 +7817,10 @@ It can happen because of some bug or when the connection is compromised.Це посилання було використано з іншого мобільного пристрою, будь ласка, створіть нове посилання на робочому столі. No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 6ac247c6f1..c79f95ba8a 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -1197,6 +1197,10 @@ 更佳的群组 No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. No comment provided by engineer. @@ -1215,6 +1219,10 @@ Better notifications No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ No comment provided by engineer. @@ -2755,6 +2763,10 @@ This is your own one-time link! 不要启用 No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again 不再显示 @@ -3488,11 +3500,19 @@ This is your own one-time link! 快速且无需等待发件人在线! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. 加入速度更快、信息更可靠。 No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite 最喜欢 @@ -3780,6 +3800,10 @@ Error: %2$@ GIF 和贴纸 No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! 下午好! @@ -3894,6 +3918,10 @@ Error: %2$@ 帮助 No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden 隐藏 @@ -4653,6 +4681,10 @@ This is your link for group %@! 群组成员可以发送语音消息。 No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus 菜单 @@ -5101,6 +5133,10 @@ This is your link for group %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -5433,6 +5469,10 @@ Requires compatible VPN. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other 其他 @@ -5690,6 +5730,10 @@ Error: %@ 私密文件名 No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + Private message routing 私有消息路由 @@ -6591,6 +6635,10 @@ Enable in *Network & servers* settings. 发送通知 No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas 发送问题和想法 @@ -6825,6 +6873,10 @@ Enable in *Network & servers* settings. 设置它以代替系统身份验证。 No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode 设置密码 @@ -7602,6 +7654,10 @@ It can happen because of some bug or when the connection is compromised.此链接已在其他移动设备上使用,请在桌面上创建新链接。 No comment provided by engineer. + + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. 此设置适用于您当前聊天资料 **%@** 中的消息。 diff --git a/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings index 5ef592ec70..3a577620a0 100644 --- a/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings @@ -1,7 +1,3 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"New messages in %d chats" = "Nowe wiadomości w %d czatach"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings index 0ae5c52efa..2b7338396a 100644 --- a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings @@ -17,7 +17,7 @@ "Comment" = "Hozzászólás"; /* No comment provided by engineer. */ -"Currently maximum supported file size is %@." = "Jelenleg a maximálisan támogatott fájlméret: %@."; +"Currently maximum supported file size is %@." = "Jelenleg támogatott legnagyobb fájl méret: %@."; /* No comment provided by engineer. */ "Database downgrade required" = "Adatbázis visszafejlesztése szükséges"; @@ -80,7 +80,7 @@ "Please create a profile in the SimpleX app" = "Hozzon létre egy profilt a SimpleX alkalmazásban"; /* No comment provided by engineer. */ -"Selected chat preferences prohibit this message." = "A kijelölt csevegési beállítások tiltják ezt az üzenetet."; +"Selected chat preferences prohibit this message." = "A kijelölt csevegés-beállítások tiltják ezt az üzenetet."; /* No comment provided by engineer. */ "Sending a message takes longer than expected." = "Az üzenet elküldése a vártnál tovább tart."; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 874d8c9c88..c391c1882d 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -58,6 +58,12 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Stern auf GitHub vergeben](https://github.com/simplex-chat/simplex-chat)"; +/* No comment provided by engineer. */ +"@'%@'" = "@'%@'"; + +/* No comment provided by engineer. */ +"@%@" = "@%@"; + /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen."; @@ -304,7 +310,7 @@ /* delete after time time interval */ -"1 month" = "Älter als einen Monat"; +"1 month" = "Älter als ein Monat"; /* delete after time time interval */ @@ -397,6 +403,9 @@ /* No comment provided by engineer. */ "Acknowledgement errors" = "Fehler bei der Bestätigung"; +/* token status text */ +"Active" = "Aktiv"; + /* No comment provided by engineer. */ "Active connections" = "Aktive Verbindungen"; @@ -523,6 +532,9 @@ /* profile dropdown */ "All profiles" = "Alle Profile"; +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Alle Meldungen werden für Sie archiviert."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Alle Ihre Kontakte bleiben verbunden."; @@ -568,6 +580,9 @@ /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Unwiederbringliches löschen von gesendeten Nachrichten erlauben. (24 Stunden)"; +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Melden von Nachrichten an Moderatoren erlauben."; + /* No comment provided by engineer. */ "Allow to send files and media." = "Das Senden von Dateien und Medien erlauben."; @@ -673,6 +688,12 @@ /* No comment provided by engineer. */ "Archive" = "Archiv"; +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Archiviere %lld Meldungen?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Alle Meldungen archivieren?"; + /* No comment provided by engineer. */ "Archive and upload" = "Archivieren und Hochladen"; @@ -685,6 +706,9 @@ /* No comment provided by engineer. */ "Archive report?" = "Meldung archivieren?"; +/* swipe action */ +"Archive reports" = "Meldungen archivieren"; + /* No comment provided by engineer. */ "Archived contacts" = "Archivierte Kontakte"; @@ -1080,6 +1104,9 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Chat-Inhalte entfernen?"; +/* No comment provided by engineer. */ +"Clear group?" = "Gruppe entfernen?"; + /* No comment provided by engineer. */ "Clear or delete group?" = "Gruppe entfernen oder löschen?"; @@ -1173,6 +1200,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Hochladen bestätigen"; +/* token status text */ +"Confirmed" = "Bestätigt"; + /* server test step */ "Connect" = "Verbinden"; @@ -1550,7 +1580,7 @@ /* delete after time pref value */ -"default (%@)" = "Voreinstellung (%@)"; +"default (%@)" = "Default (%@)"; /* No comment provided by engineer. */ "default (no)" = "Voreinstellung (Nein)"; @@ -1656,7 +1686,7 @@ "Delete messages" = "Nachrichten löschen"; /* No comment provided by engineer. */ -"Delete messages after" = "Löschen der Nachrichten"; +"Delete messages after" = "Nachrichten löschen"; /* No comment provided by engineer. */ "Delete old database" = "Alte Datenbank löschen"; @@ -2127,6 +2157,9 @@ /* No comment provided by engineer. */ "Error changing to incognito!" = "Fehler beim Wechseln zum Inkognito-Profil!"; +/* No comment provided by engineer. */ +"Error checking token status" = "Fehler beim Überprüfen des Token-Status"; + /* No comment provided by engineer. */ "Error connecting to forwarding server %@. Please try later." = "Fehler beim Verbinden mit dem Weiterleitungsserver %@. Bitte versuchen Sie es später erneut."; @@ -2220,6 +2253,9 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Fehler beim Wiederherstellen der Verbindungen zu den Servern"; +/* alert title */ +"Error registering for notifications" = "Fehler beim Registrieren für Benachrichtigungen"; + /* No comment provided by engineer. */ "Error removing member" = "Fehler beim Entfernen des Mitglieds"; @@ -2283,6 +2319,9 @@ /* No comment provided by engineer. */ "Error synchronizing connection" = "Fehler beim Synchronisieren der Verbindung"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Fehler beim Testen der Server-Verbindung"; + /* No comment provided by engineer. */ "Error updating group link" = "Fehler beim Aktualisieren des Gruppen-Links"; @@ -2337,6 +2376,9 @@ /* No comment provided by engineer. */ "expired" = "Abgelaufen"; +/* token status text */ +"Expired" = "Abgelaufen"; + /* No comment provided by engineer. */ "Export database" = "Datenbank exportieren"; @@ -2454,6 +2496,9 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Reparatur wird vom Gruppenmitglied nicht unterstützt"; +/* No comment provided by engineer. */ +"For all moderators" = "Für alle Moderatoren"; + /* No comment provided by engineer. */ "for better metadata privacy." = "für einen besseren Metadatenschutz."; @@ -2466,6 +2511,9 @@ /* No comment provided by engineer. */ "For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Wenn Ihr Kontakt beispielsweise Nachrichten über einen SimpleX-Chatserver empfängt, wird Ihre App diese über einen der Server von Flux versenden."; +/* No comment provided by engineer. */ +"For me" = "Für mich"; + /* No comment provided by engineer. */ "For private routing" = "Für privates Routing"; @@ -2814,6 +2862,21 @@ /* No comment provided by engineer. */ "Interface colors" = "Interface-Farben"; +/* token status text */ +"Invalid" = "Ungültig"; + +/* token status text */ +"Invalid (bad token)" = "Ungültig (falsches Token)"; + +/* token status text */ +"Invalid (expired)" = "Ungültig (abgelaufen)"; + +/* token status text */ +"Invalid (unregistered)" = "Ungültig (nicht registriert)"; + +/* token status text */ +"Invalid (wrong topic)" = "Ungültig (falsches Thema)"; + /* invalid chat data */ "invalid chat" = "Ungültiger Chat"; @@ -3093,6 +3156,9 @@ /* item status text */ "Member inactive" = "Mitglied inaktiv"; +/* chat feature */ +"Member reports" = "Mitglieder-Meldungen"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All chat members will be notified." = "Die Rolle des Mitglieds wird auf \"%@\" geändert. Alle Chat-Mitglieder werden darüber informiert."; @@ -3114,6 +3180,9 @@ /* No comment provided by engineer. */ "Members can irreversibly delete sent messages. (24 hours)" = "Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)"; +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "Mitglieder können Nachrichten an Moderatoren melden."; + /* No comment provided by engineer. */ "Members can send direct messages." = "Gruppenmitglieder können Direktnachrichten versenden."; @@ -3303,6 +3372,9 @@ /* notification label action */ "Mute" = "Stummschalten"; +/* notification label action */ +"Mute all" = "Alle stummschalten"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Bei Inaktivität stummgeschaltet!"; @@ -3336,6 +3408,9 @@ /* delete after time */ "never" = "nie"; +/* token status text */ +"New" = "Neu"; + /* No comment provided by engineer. */ "New chat" = "Neuer Chat"; @@ -3474,6 +3549,9 @@ /* copied message info in history */ "no text" = "Kein Text"; +/* alert title */ +"No token!" = "Kein Token!"; + /* No comment provided by engineer. */ "No unread chats" = "Keine ungelesenen Chats"; @@ -3498,9 +3576,15 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Benachrichtigungen sind deaktiviert!"; +/* alert title */ +"Notifications error" = "Benachrichtigungs-Fehler"; + /* No comment provided by engineer. */ "Notifications privacy" = "Datenschutz für Benachrichtigungen"; +/* alert title */ +"Notifications status" = "Benachrichtigungs-Status"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Administratoren können nun\n- Nachrichten von Gruppenmitgliedern löschen\n- Gruppenmitglieder deaktivieren (\"Beobachter\"-Rolle)"; @@ -3785,6 +3869,15 @@ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Bitte bewahren Sie das Passwort sicher auf, Sie können es NICHT mehr ändern, wenn Sie es vergessen haben oder verlieren."; +/* token info */ +"Please try to disable and re-enable notfications." = "Bitte versuchen Sie, die Benachrichtigungen zu deaktivieren und wieder zu aktivieren."; + +/* token info */ +"Please wait for token activation to complete." = "Bitte warten Sie, bis die Token-Aktivierung abgeschlossen ist."; + +/* token info */ +"Please wait for token to be registered." = "Bitte warten Sie auf die Registrierung des Tokens."; + /* No comment provided by engineer. */ "Polish interface" = "Polnische Bedienoberfläche"; @@ -3866,6 +3959,9 @@ /* No comment provided by engineer. */ "Prohibit messages reactions." = "Reaktionen auf Nachrichten nicht erlauben."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Melden von Nachrichten an Moderatoren nicht erlauben."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben."; @@ -4028,6 +4124,15 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Reduzierter Batterieverbrauch"; +/* No comment provided by engineer. */ +"Register" = "Registrieren"; + +/* token info */ +"Register notification token?" = "Benachrichtigungs-Token registrieren?"; + +/* token status text */ +"Registered" = "Registriert"; + /* reject incoming call via notification swipe action */ "Reject" = "Ablehnen"; @@ -4128,6 +4233,12 @@ /* report reason */ "Report violation: only group moderators will see it." = "Verstoß melden: Nur Gruppenmoderatoren werden es sehen."; +/* report in notification */ +"Report: (text.isEmpty ? reason.text : text)" = "Meldung: (text.isEmpty ? reason.text : text)"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Melden von Nachrichten an Moderatoren ist nicht erlaubt."; + /* No comment provided by engineer. */ "Reports" = "Meldungen"; @@ -4881,6 +4992,9 @@ /* No comment provided by engineer. */ "TCP connection timeout" = "Timeout der TCP-Verbindung"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "TCP-Port für Nachrichtenübermittlung"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -4896,6 +5010,9 @@ /* server test failure */ "Test failed at step %@." = "Der Test ist beim Schritt %@ fehlgeschlagen."; +/* No comment provided by engineer. */ +"Test notifications" = "Benachrichtigungen testen"; + /* No comment provided by engineer. */ "Test server" = "Teste Server"; @@ -4971,9 +5088,6 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Das Profil wird nur mit Ihren Kontakten geteilt."; -/* No comment provided by engineer. */ -"The report will be archived for you." = "Die Meldung wird für Sie archiviert."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**."; @@ -5121,6 +5235,9 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Inkognito beim Verbinden einschalten."; +/* token status */ +"Token status: %@." = "Token-Status: %@."; + /* No comment provided by engineer. */ "Toolbar opacity" = "Deckkraft der Symbolleiste"; @@ -5334,12 +5451,18 @@ /* No comment provided by engineer. */ "Use SOCKS proxy" = "SOCKS-Proxy nutzen"; +/* No comment provided by engineer. */ +"Use TCP port %@ when no port is specified." = "Solange kein Port konfiguriert ist, wird TCP-Port %@ genutzt."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Die App kann während eines Anrufs genutzt werden."; /* No comment provided by engineer. */ "Use the app with one hand." = "Die App mit einer Hand bedienen."; +/* No comment provided by engineer. */ +"Use web port" = "Web-Port nutzen"; + /* No comment provided by engineer. */ "User selection" = "Benutzer-Auswahl"; @@ -5748,6 +5871,9 @@ /* chat list item description */ "you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt"; +/* token info */ +"You should receive notifications." = "Sie sollten Benachrichtigungen erhalten."; + /* snd group event chat item */ "you unblocked %@" = "Sie haben %@ freigegeben"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 2004e192c1..6e5d0e3d03 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -58,6 +58,12 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Estrella en GitHub](https://github.com/simplex-chat/simplex-chat)"; +/* No comment provided by engineer. */ +"@'%@'" = "@'%@'"; + +/* No comment provided by engineer. */ +"@%@" = "@%@"; + /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Añadir contacto**: crea un enlace de invitación nuevo."; @@ -397,6 +403,9 @@ /* No comment provided by engineer. */ "Acknowledgement errors" = "Errores de confirmación"; +/* token status text */ +"Active" = "Activo"; + /* No comment provided by engineer. */ "Active connections" = "Conexiones activas"; @@ -523,6 +532,9 @@ /* profile dropdown */ "All profiles" = "Todos los perfiles"; +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Todos los informes serán archivados para ti."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Todos tus contactos permanecerán conectados."; @@ -568,6 +580,9 @@ /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Se permite la eliminación irreversible de mensajes. (24 horas)"; +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Se permite informar de mensajes a los moderadores."; + /* No comment provided by engineer. */ "Allow to send files and media." = "Se permite enviar archivos y multimedia."; @@ -673,6 +688,12 @@ /* No comment provided by engineer. */ "Archive" = "Archivar"; +/* No comment provided by engineer. */ +"Archive %lld reports?" = "¿Archivar %lld informes?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "¿Archivar todos los informes?"; + /* No comment provided by engineer. */ "Archive and upload" = "Archivar y subir"; @@ -685,6 +706,9 @@ /* No comment provided by engineer. */ "Archive report?" = "¿Archivar informe?"; +/* swipe action */ +"Archive reports" = "Archivar informes"; + /* No comment provided by engineer. */ "Archived contacts" = "Contactos archivados"; @@ -817,7 +841,7 @@ /* rcv group event chat item */ "blocked %@" = "ha bloqueado a %@"; -/* blocked chat item */ +/* marked deleted chat item preview text */ "blocked by admin" = "bloqueado por administrador"; /* No comment provided by engineer. */ @@ -1080,6 +1104,9 @@ /* No comment provided by engineer. */ "Clear conversation?" = "¿Vaciar conversación?"; +/* No comment provided by engineer. */ +"Clear group?" = "¿Vaciar grupo?"; + /* No comment provided by engineer. */ "Clear or delete group?" = "¿Vaciar o eliminar grupo?"; @@ -1173,6 +1200,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Confirmar subida"; +/* token status text */ +"Confirmed" = "Confirmado"; + /* server test step */ "Connect" = "Conectar"; @@ -1231,7 +1261,7 @@ "Connected to desktop" = "Conectado con ordenador"; /* No comment provided by engineer. */ -"connecting" = "conectando"; +"connecting" = "conectando..."; /* No comment provided by engineer. */ "Connecting" = "Conectando"; @@ -1653,7 +1683,7 @@ "Delete message?" = "¿Eliminar mensaje?"; /* alert button */ -"Delete messages" = "Eliminar mensajes"; +"Delete messages" = "Activar"; /* No comment provided by engineer. */ "Delete messages after" = "Eliminar en"; @@ -1704,7 +1734,7 @@ "deleted contact" = "contacto eliminado"; /* rcv group event chat item */ -"deleted group" = "grupo eliminado"; +"deleted group" = "ha eliminado el grupo"; /* No comment provided by engineer. */ "Deletion errors" = "Errores de eliminación"; @@ -2127,6 +2157,9 @@ /* No comment provided by engineer. */ "Error changing to incognito!" = "¡Error al cambiar a incógnito!"; +/* No comment provided by engineer. */ +"Error checking token status" = "Error al verificar el estado del token"; + /* No comment provided by engineer. */ "Error connecting to forwarding server %@. Please try later." = "Error al conectar con el servidor de reenvío %@. Por favor, inténtalo más tarde."; @@ -2220,8 +2253,11 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Error al reconectar con los servidores"; +/* alert title */ +"Error registering for notifications" = "Error al registrarse para notificaciones"; + /* No comment provided by engineer. */ -"Error removing member" = "Error al eliminar miembro"; +"Error removing member" = "Error al expulsar miembro"; /* alert title */ "Error reordering lists" = "Error al reorganizar listas"; @@ -2283,6 +2319,9 @@ /* No comment provided by engineer. */ "Error synchronizing connection" = "Error al sincronizar conexión"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Error al testar la conexión al servidor"; + /* No comment provided by engineer. */ "Error updating group link" = "Error al actualizar enlace de grupo"; @@ -2307,8 +2346,7 @@ /* No comment provided by engineer. */ "Error: " = "Error: "; -/* file error text - snd error text */ +/* alert message */ "Error: %@" = "Error: %@"; /* No comment provided by engineer. */ @@ -2338,6 +2376,9 @@ /* No comment provided by engineer. */ "expired" = "expirados"; +/* token status text */ +"Expired" = "Expirado"; + /* No comment provided by engineer. */ "Export database" = "Exportar base de datos"; @@ -2455,6 +2496,9 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Corrección no compatible con miembro del grupo"; +/* No comment provided by engineer. */ +"For all moderators" = "Para todos los moderadores"; + /* No comment provided by engineer. */ "for better metadata privacy." = "para mejorar la privacidad de los metadatos."; @@ -2467,6 +2511,9 @@ /* No comment provided by engineer. */ "For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Si por ejemplo tu contacto recibe los mensajes a través de un servidor de SimpleX Chat, tu aplicación los entregará a través de un servidor de Flux."; +/* No comment provided by engineer. */ +"For me" = "para mí"; + /* No comment provided by engineer. */ "For private routing" = "Para enrutamiento privado"; @@ -2815,6 +2862,21 @@ /* No comment provided by engineer. */ "Interface colors" = "Colores del interfaz"; +/* token status text */ +"Invalid" = "No válido"; + +/* token status text */ +"Invalid (bad token)" = "No válido (token incorrecto)"; + +/* token status text */ +"Invalid (expired)" = "No válido (expirado)"; + +/* token status text */ +"Invalid (unregistered)" = "No válido (no registrado)"; + +/* token status text */ +"Invalid (wrong topic)" = "No válido (tópico incorrecto)"; + /* invalid chat data */ "invalid chat" = "chat no válido"; @@ -3094,6 +3156,9 @@ /* item status text */ "Member inactive" = "Miembro inactivo"; +/* chat feature */ +"Member reports" = "Informes de miembro"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All chat members will be notified." = "El rol del miembro cambiará a \"%@\" y todos serán notificados."; @@ -3115,6 +3180,9 @@ /* No comment provided by engineer. */ "Members can irreversibly delete sent messages. (24 hours)" = "Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas)"; +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "Los miembros pueden informar de mensajes a los moderadores."; + /* No comment provided by engineer. */ "Members can send direct messages." = "Los miembros del grupo pueden enviar mensajes directos."; @@ -3304,6 +3372,9 @@ /* notification label action */ "Mute" = "Silenciar"; +/* notification label action */ +"Mute all" = "Silenciar todo"; + /* No comment provided by engineer. */ "Muted when inactive!" = "¡Silenciado cuando está inactivo!"; @@ -3337,6 +3408,9 @@ /* delete after time */ "never" = "nunca"; +/* token status text */ +"New" = "Nuevo"; + /* No comment provided by engineer. */ "New chat" = "Nuevo chat"; @@ -3475,6 +3549,9 @@ /* copied message info in history */ "no text" = "sin texto"; +/* alert title */ +"No token!" = "¡Sin token!"; + /* No comment provided by engineer. */ "No unread chats" = "Ningún chat sin leer"; @@ -3499,9 +3576,15 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "¡Las notificaciones están desactivadas!"; +/* alert title */ +"Notifications error" = "Error en notificaciones"; + /* No comment provided by engineer. */ "Notifications privacy" = "Privacidad en las notificaciones"; +/* alert title */ +"Notifications status" = "Estado notificaciones"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Ahora los administradores pueden:\n- eliminar mensajes de los miembros.\n- desactivar el rol miembro (a rol \"observador\")"; @@ -3786,6 +3869,15 @@ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes."; +/* token info */ +"Please try to disable and re-enable notfications." = "Por favor, intenta desactivar y reactivar las notificaciones."; + +/* token info */ +"Please wait for token activation to complete." = "Por favor, espera a que el token de activación se complete."; + +/* token info */ +"Please wait for token to be registered." = "Por favor, espera a que el token se registre."; + /* No comment provided by engineer. */ "Polish interface" = "Interfaz en polaco"; @@ -3867,6 +3959,9 @@ /* No comment provided by engineer. */ "Prohibit messages reactions." = "No se permiten reacciones a los mensajes."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "No se permite informar de mensajes a los moderadores."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "No se permiten mensajes directos entre miembros."; @@ -4029,6 +4124,15 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Reducción del uso de batería"; +/* No comment provided by engineer. */ +"Register" = "Registrar"; + +/* token info */ +"Register notification token?" = "¿Registrar el token de notificaciones?"; + +/* token status text */ +"Registered" = "Registrado"; + /* reject incoming call via notification swipe action */ "Reject" = "Rechazar"; @@ -4129,6 +4233,12 @@ /* report reason */ "Report violation: only group moderators will see it." = "Informar de violación: sólo los moderadores del grupo lo verán."; +/* report in notification */ +"Report: (text.isEmpty ? reason.text : text)" = "Informe: (text.isEmpty ? reason.text : text)"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "No se permite informar de mensajes a los moderadores."; + /* No comment provided by engineer. */ "Reports" = "Informes"; @@ -4882,6 +4992,9 @@ /* No comment provided by engineer. */ "TCP connection timeout" = "Timeout de la conexión TCP"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "Puerto TCP para mensajes"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -4897,6 +5010,9 @@ /* server test failure */ "Test failed at step %@." = "Prueba no superada en el paso %@."; +/* No comment provided by engineer. */ +"Test notifications" = "Probar notificaciones"; + /* No comment provided by engineer. */ "Test server" = "Probar servidor"; @@ -4972,9 +5088,6 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "El perfil sólo se comparte con tus contactos."; -/* No comment provided by engineer. */ -"The report will be archived for you." = "El informe será archivado para ti."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Las mismas condiciones se aplicarán al operador **%@**."; @@ -5021,7 +5134,7 @@ "This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Esta acción es irreversible. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Podría tardar varios minutos."; /* alert message */ -"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Todos los mensajes previos a la selección serán eliminados del chat. ¡No podrá deshacerse!"; +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Todos los mensajes previos al período seleccionado serán eliminados del chat. ¡No podrá deshacerse!"; /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Esta acción es irreversible. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente."; @@ -5122,6 +5235,9 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Activa incógnito al conectar."; +/* token status */ +"Token status: %@." = "Estado token: %@."; + /* No comment provided by engineer. */ "Toolbar opacity" = "Opacidad barra"; @@ -5335,12 +5451,18 @@ /* No comment provided by engineer. */ "Use SOCKS proxy" = "Usar proxy SOCKS"; +/* No comment provided by engineer. */ +"Use TCP port %@ when no port is specified." = "Se usa el puerto TCP %@ cuando no se ha especificado otro."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Usar la aplicación durante la llamada."; /* No comment provided by engineer. */ "Use the app with one hand." = "Usa la aplicación con una sola mano."; +/* No comment provided by engineer. */ +"Use web port" = "Usar puerto web"; + /* No comment provided by engineer. */ "User selection" = "Selección de usuarios"; @@ -5749,6 +5871,9 @@ /* chat list item description */ "you shared one-time link incognito" = "has compartido enlace de un solo uso en modo incógnito"; +/* token info */ +"You should receive notifications." = "Deberías recibir notificaciones."; + /* snd group event chat item */ "you unblocked %@" = "has desbloqueado a %@"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index f12a39b3b3..b926536f04 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -17,13 +17,13 @@ "- connect to [directory service](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) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- kapcsolódás a [könyvtár szolgáltatáshoz](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) (BETA)!\n- kézbesítési jelentések (legfeljebb 20 tag).\n- gyorsabb és stabilabb."; /* No comment provided by engineer. */ -"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- stabilabb üzenetkézbesítés.\n- valamivel jobb csoportok.\n- és még sok más!"; +"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- stabilabb üzenetkézbesítés.\n- picit továbbfejlesztett csoportok.\n- és még sok más!"; /* No comment provided by engineer. */ "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- értesíti az ismerősöket a törlésről (nem kötelező)\n- profil nevek szóközökkel\n- és még sok más!"; /* No comment provided by engineer. */ -"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyedi üzenet-eltűnési időkorlát.\n- előzmények szerkesztése."; +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni üzenet-eltűnési időkorlát.\n- előzmények szerkesztése."; /* No comment provided by engineer. */ ", " = ", "; @@ -59,7 +59,13 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Csillagozás a GitHubon](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Create 1-time link**: to create and share a new invitation link." = "**Ismerős hozzáadása:** új meghívó-hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz."; +"@'%@'" = "@'%@'"; + +/* No comment provided by engineer. */ +"@%@" = "@%@"; + +/* No comment provided by engineer. */ +"**Create 1-time link**: to create and share a new invitation link." = "**Ismerős hozzáadása:** új meghívási hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Csoport létrehozása:** új csoport létrehozásához."; @@ -80,7 +86,7 @@ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Megjegyzés:** ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja az ismerőseitől érkező üzenetek visszafejtését."; /* No comment provided by engineer. */ -"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Megjegyzés:** NEM fogja tudni helyreállítani, vagy megváltoztatni a jelmondatot abban az esetben, ha elveszíti."; +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Megjegyzés:** NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti."; /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Megjegyzés:** az eszköztoken és az értesítések el lesznek küldve a SimpleX Chat értesítési kiszolgálóra, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik."; @@ -104,7 +110,7 @@ "## History" = "## Előzmények"; /* copied message info */ -"## In reply to" = "## Válaszul erre:"; +"## In reply to" = "## Válaszul erre"; /* No comment provided by engineer. */ "#secret#" = "#titok#"; @@ -143,7 +149,7 @@ "%@ is connected!" = "%@ kapcsolódott!"; /* No comment provided by engineer. */ -"%@ is not verified" = "%@ nem hitelesített"; +"%@ is not verified" = "%@ nincs hitelesítve"; /* No comment provided by engineer. */ "%@ is verified" = "%@ hitelesítve"; @@ -221,7 +227,7 @@ "%lld contact(s) selected" = "%lld ismerős kijelölve"; /* No comment provided by engineer. */ -"%lld file(s) with total size of %@" = "%lld fájl, amely(ek)nek teljes mérete: %@"; +"%lld file(s) with total size of %@" = "%lld fájl, %@ összméretben"; /* No comment provided by engineer. */ "%lld group events" = "%lld csoportesemény"; @@ -236,7 +242,7 @@ "%lld messages blocked by admin" = "%lld üzenetet letiltott az adminisztrátor"; /* No comment provided by engineer. */ -"%lld messages marked deleted" = "%lld törlésre megjelölt üzenet"; +"%lld messages marked deleted" = "%lld üzenet megjelölve törlésre"; /* No comment provided by engineer. */ "%lld messages moderated by %@" = "%@ %lld üzenetet moderált"; @@ -248,28 +254,28 @@ "%lld new interface languages" = "%lld új kezelőfelületi nyelv"; /* No comment provided by engineer. */ -"%lld seconds" = "%lld másodperc"; +"%lld seconds" = "%lld mp"; /* No comment provided by engineer. */ -"%lldd" = "%lldd"; +"%lldd" = "%lldn"; /* No comment provided by engineer. */ -"%lldh" = "%lldh"; +"%lldh" = "%lldó"; /* No comment provided by engineer. */ "%lldk" = "%lldk"; /* No comment provided by engineer. */ -"%lldm" = "%lldm"; +"%lldm" = "%lldp"; /* No comment provided by engineer. */ -"%lldmth" = "%lldmth"; +"%lldmth" = "%lldh"; /* No comment provided by engineer. */ -"%llds" = "%llds"; +"%llds" = "%lldmp"; /* No comment provided by engineer. */ -"%lldw" = "%lldw"; +"%lldw" = "%lldhét"; /* No comment provided by engineer. */ "%u messages failed to decrypt." = "%u üzenet visszafejtése sikertelen."; @@ -314,10 +320,10 @@ "1 year" = "1 év"; /* No comment provided by engineer. */ -"1-time link" = "Egyszer használható meghívó-hivatkozás"; +"1-time link" = "Egyszer használható meghívási hivatkozás"; /* No comment provided by engineer. */ -"1-time link can be used *with one contact only* - share in person or via any messenger." = "Az egyszer használható meghívó-hivatkozás csak *egyetlen ismerőssel használható* – személyesen vagy bármilyen üzenetküldőn keresztül megosztható."; +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Az egyszer használható meghívási hivatkozás csak *egyetlen ismerőssel használható* – személyesen vagy bármilyen üzenetküldőn keresztül megosztható."; /* No comment provided by engineer. */ "5 minutes" = "5 perc"; @@ -347,16 +353,16 @@ "Abort" = "Megszakítás"; /* No comment provided by engineer. */ -"Abort changing address" = "Címváltoztatás megszakítása"; +"Abort changing address" = "Cím módosításának megszakítása"; /* No comment provided by engineer. */ -"Abort changing address?" = "Címváltoztatás megszakítása??"; +"Abort changing address?" = "Cím módosításának megszakítása?"; /* No comment provided by engineer. */ "About operators" = "Az üzemeltetőkről"; /* No comment provided by engineer. */ -"About SimpleX Chat" = "SimpleX Chat névjegye"; +"About SimpleX Chat" = "A SimpleX Chat névjegye"; /* No comment provided by engineer. */ "above, then choose:" = "gombra fent, majd válassza ki:"; @@ -380,10 +386,10 @@ /* accept contact request via notification swipe action */ -"Accept incognito" = "Fogadás inkognitóban"; +"Accept incognito" = "Elfogadás inkognitóban"; /* call status */ -"accepted call" = "elfogadott hívás"; +"accepted call" = "fogadott hívás"; /* No comment provided by engineer. */ "Accepted conditions" = "Elfogadott feltételek"; @@ -397,6 +403,9 @@ /* No comment provided by engineer. */ "Acknowledgement errors" = "Visszaigazolási hibák"; +/* token status text */ +"Active" = "Aktív"; + /* No comment provided by engineer. */ "Active connections" = "Aktív kapcsolatok száma"; @@ -452,10 +461,10 @@ "Address" = "Cím"; /* No comment provided by engineer. */ -"Address change will be aborted. Old receiving address will be used." = "A cím megváltoztatása meg fog szakadni. A régi fogadási cím lesz használva."; +"Address change will be aborted. Old receiving address will be used." = "A cím módosítása meg fog szakadni. A régi fogadási cím lesz használva."; /* No comment provided by engineer. */ -"Address or 1-time link?" = "Cím vagy egyszer használható meghívó-hivatkozás?"; +"Address or 1-time link?" = "Cím vagy egyszer használható meghívási hivatkozás?"; /* No comment provided by engineer. */ "Address settings" = "Címbeállítások"; @@ -470,7 +479,7 @@ "Admins can block a member for all." = "Az adminisztrátorok egy tagot a csoport összes tagja számára letilthatnak."; /* No comment provided by engineer. */ -"Admins can create the links to join groups." = "Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való kapcsolódáshoz."; +"Admins can create the links to join groups." = "Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz."; /* No comment provided by engineer. */ "Advanced network settings" = "Speciális hálózati beállítások"; @@ -523,6 +532,9 @@ /* profile dropdown */ "All profiles" = "Összes profil"; +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Az összes jelentés archiválva lesz az Ön számára."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Az összes ismerősével kapcsolatban marad."; @@ -551,10 +563,10 @@ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. (24 óra)"; /* No comment provided by engineer. */ -"Allow message reactions only if your contact allows them." = "Az üzenetreakciók küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi."; +"Allow message reactions only if your contact allows them." = "A reakciók hozzáadása az üzenetekhez csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi."; /* No comment provided by engineer. */ -"Allow message reactions." = "Üzenetreakciók engedélyezése."; +"Allow message reactions." = "A reakciók hozzáadása az üzenetekhez engedélyezve van."; /* No comment provided by engineer. */ "Allow sending direct messages to members." = "A közvetlen üzenetek küldése a tagok között engedélyezve van."; @@ -566,16 +578,19 @@ "Allow sharing" = "Megosztás engedélyezése"; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages. (24 hours)" = "Elküldött üzenetek végleges törlésének engedélyezése. (24 óra)"; +"Allow to irreversibly delete sent messages. (24 hours)" = "Az elküldött üzenetek végleges törlése engedélyezve van. (24 óra)"; /* No comment provided by engineer. */ -"Allow to send files and media." = "Fájlok és médiatartalmak küldésének engedélyezése."; +"Allow to report messsages to moderators." = "Lehetővé teszi az üzenetek jelentését a moderátorok felé."; + +/* No comment provided by engineer. */ +"Allow to send files and media." = "A fájlok- és a médiatartalmak küldése engedélyezve van."; /* No comment provided by engineer. */ "Allow to send SimpleX links." = "A SimpleX-hivatkozások küldése engedélyezve van."; /* No comment provided by engineer. */ -"Allow to send voice messages." = "Hangüzenetek küldésének engedélyezése."; +"Allow to send voice messages." = "A hangüzenetek küldése engedélyezve van."; /* No comment provided by engineer. */ "Allow voice messages only if your contact allows them." = "A hangüzenetek küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi."; @@ -584,7 +599,7 @@ "Allow voice messages?" = "Hangüzenetek engedélyezése?"; /* No comment provided by engineer. */ -"Allow your contacts adding message reactions." = "Az üzenetreakciók küldése engedélyezve van az ismerősei számára."; +"Allow your contacts adding message reactions." = "A reakciók hozzáadása az üzenetekhez engedélyezve van az ismerősei számára."; /* No comment provided by engineer. */ "Allow your contacts to call you." = "A hívások kezdeményezése engedélyezve van az ismerősei számára."; @@ -617,7 +632,7 @@ "Always use relay" = "Mindig használjon továbbító-kiszolgálót"; /* No comment provided by engineer. */ -"An empty chat profile with the provided name is created, and the app opens as usual." = "Egy üres csevegési profil jön létre a megadott névvel, és az alkalmazás a szokásos módon megnyílik."; +"An empty chat profile with the provided name is created, and the app opens as usual." = "Egy üres csevegési profil lesz létrehozva a megadott névvel, és az alkalmazás a szokásos módon megnyílik."; /* No comment provided by engineer. */ "and %lld other events" = "és további %lld esemény"; @@ -673,6 +688,12 @@ /* No comment provided by engineer. */ "Archive" = "Archívum"; +/* No comment provided by engineer. */ +"Archive %lld reports?" = "%lld jelentés archiválása?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Az összes jelentés archiválása?"; + /* No comment provided by engineer. */ "Archive and upload" = "Archiválás és feltöltés"; @@ -685,6 +706,9 @@ /* No comment provided by engineer. */ "Archive report?" = "Jelentés archiválása?"; +/* swipe action */ +"Archive reports" = "Jelentések archiválása"; + /* No comment provided by engineer. */ "Archived contacts" = "Archivált ismerősök"; @@ -713,7 +737,7 @@ "audio call (not e2e encrypted)" = "hanghívás (nem e2e titkosított)"; /* chat feature */ -"Audio/video calls" = "Hang-/videóhívások"; +"Audio/video calls" = "Hang- és videóhívások"; /* No comment provided by engineer. */ "Audio/video calls are prohibited." = "A hívások kezdeményezése le van tiltva ebben a csevegésben."; @@ -770,13 +794,13 @@ "Better calls" = "Továbbfejlesztett hívásélmény"; /* No comment provided by engineer. */ -"Better groups" = "Javított csoportok"; +"Better groups" = "Továbbfejlesztett csoportok"; /* No comment provided by engineer. */ "Better message dates." = "Továbbfejlesztett üzenetdátumok."; /* No comment provided by engineer. */ -"Better messages" = "Jobb üzenetek"; +"Better messages" = "Továbbfejlesztett üzenetek"; /* No comment provided by engineer. */ "Better networking" = "Jobb hálózatkezelés"; @@ -797,13 +821,13 @@ "Block" = "Letiltás"; /* No comment provided by engineer. */ -"Block for all" = "Letiltás az összes tag számára"; +"Block for all" = "Letiltás"; /* No comment provided by engineer. */ "Block group members" = "Csoporttagok letiltása"; /* No comment provided by engineer. */ -"Block member" = "Tag letiltása"; +"Block member" = "Letiltás"; /* No comment provided by engineer. */ "Block member for all?" = "Az összes tag számára letiltja ezt a tagot?"; @@ -815,13 +839,13 @@ "blocked" = "letiltva"; /* rcv group event chat item */ -"blocked %@" = "letiltotta %@-t"; +"blocked %@" = "letiltotta őt: %@"; /* marked deleted chat item preview text */ "blocked by admin" = "letiltva az adminisztrátor által"; /* No comment provided by engineer. */ -"Blocked by admin" = "Az adminisztrátor letiltotta"; +"Blocked by admin" = "Letiltva az adminisztrátor által"; /* No comment provided by engineer. */ "Blur for better privacy." = "Elhomályosítás a jobb adatvédelemért."; @@ -833,10 +857,10 @@ "bold" = "félkövér"; /* No comment provided by engineer. */ -"Both you and your contact can add message reactions." = "Mindkét fél is hozzáadhat üzenetreakciókat."; +"Both you and your contact can add message reactions." = "Mindkét fél hozzáadhat az üzenetekhez reakciókat."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Mindkét fél törölheti véglegesen az elküldött üzeneteket. (24 óra)"; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Mindkét fél véglegesen törölheti az elküldött üzeneteket. (24 óra)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Mindkét fél tud hívásokat kezdeményezni."; @@ -927,56 +951,56 @@ "Cellular" = "Mobilhálózat"; /* No comment provided by engineer. */ -"Change" = "Megváltoztatás"; +"Change" = "Módosítás"; /* alert title */ "Change automatic message deletion?" = "Automatikus üzenettörlés módosítása?"; /* authentication reason */ -"Change chat profiles" = "Csevegési profilok megváltoztatása"; +"Change chat profiles" = "Csevegési profilok módosítása"; /* No comment provided by engineer. */ -"Change database passphrase?" = "Az adatbázis jelmondatának megváltoztatása?"; +"Change database passphrase?" = "Az adatbázis jelmondatának módosítása?"; /* authentication reason */ -"Change lock mode" = "Zárolási mód megváltoztatása"; +"Change lock mode" = "Zárolási mód módosítása"; /* No comment provided by engineer. */ -"Change member role?" = "Tag szerepkörének megváltoztatása?"; +"Change member role?" = "Tag szerepkörének módosítása?"; /* authentication reason */ -"Change passcode" = "Jelkód megváltoztatása"; +"Change passcode" = "Jelkód módosítása"; /* No comment provided by engineer. */ -"Change receiving address" = "A fogadócím megváltoztatása"; +"Change receiving address" = "A fogadási cím módosítása"; /* No comment provided by engineer. */ -"Change receiving address?" = "Megváltoztatja a fogadócímet?"; +"Change receiving address?" = "Módosítja a fogadási címet?"; /* No comment provided by engineer. */ -"Change role" = "Szerepkör megváltoztatása"; +"Change role" = "Szerepkör módosítása"; /* authentication reason */ -"Change self-destruct mode" = "Önmegsemmisítő-mód megváltoztatása"; +"Change self-destruct mode" = "Önmegsemmisítő-mód módosítása"; /* authentication reason set passcode view */ -"Change self-destruct passcode" = "Önmegsemmisítő-jelkód megváltoztatása"; +"Change self-destruct passcode" = "Önmegsemmisítő-jelkód módosítása"; /* chat item text */ -"changed address for you" = "megváltoztatta a címet az Ön számára"; +"changed address for you" = "módosította a címet az Ön számára"; /* rcv group event chat item */ -"changed role of %@ to %@" = "%1$@ szerepkörét megváltoztatta a következőre: %2$@"; +"changed role of %@ to %@" = "a következőre módosította %1$@ szerepkörét: „%2$@”"; /* rcv group event chat item */ -"changed your role to %@" = "megváltoztatta az Ön szerepkörét a következőre: %@"; +"changed your role to %@" = "a következőre módosította az Ön szerepkörét: „%@”"; /* chat item text */ -"changing address for %@…" = "cím megváltoztatása %@ számára…"; +"changing address for %@…" = "cím módosítása %@ számára…"; /* chat item text */ -"changing address…" = "cím megváltoztatása…"; +"changing address…" = "cím módosítása…"; /* No comment provided by engineer. */ "Chat" = "Csevegés"; @@ -1021,10 +1045,10 @@ "Chat migrated!" = "A csevegés átköltöztetve!"; /* No comment provided by engineer. */ -"Chat preferences" = "Csevegési beállítások"; +"Chat preferences" = "Csevegés-beállítások"; /* alert message */ -"Chat preferences were changed." = "A csevegési beállítások megváltoztak."; +"Chat preferences were changed." = "A csevegés-beállítások módosultak."; /* No comment provided by engineer. */ "Chat profile" = "Csevegési profil"; @@ -1080,6 +1104,9 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Üzenetek kiürítése?"; +/* No comment provided by engineer. */ +"Clear group?" = "Csoport kiürítése?"; + /* No comment provided by engineer. */ "Clear or delete group?" = "Csoport kiürítése vagy törlése?"; @@ -1096,13 +1123,13 @@ "Color mode" = "Színmód"; /* No comment provided by engineer. */ -"colored" = "színes"; +"colored" = "színezett"; /* report reason */ "Community guidelines violation" = "Közösségi irányelvek megsértése"; /* server test step */ -"Compare file" = "Fájl összehasonlítás"; +"Compare file" = "Fájl-összehasonlítás"; /* No comment provided by engineer. */ "Compare security codes with your contacts." = "Biztonsági kódok összehasonlítása az ismerősökével."; @@ -1114,7 +1141,7 @@ "Completed" = "Elkészült"; /* No comment provided by engineer. */ -"Conditions accepted on: %@." = "Feltételek elfogadva ekkor: %@."; +"Conditions accepted on: %@." = "Feltételek elfogadva: %@."; /* No comment provided by engineer. */ "Conditions are accepted for the operator(s): **%@**." = "A következő üzemeltető(k) számára elfogadott feltételek: **%@**."; @@ -1126,7 +1153,7 @@ "Conditions of use" = "Használati feltételek"; /* No comment provided by engineer. */ -"Conditions will be accepted for enabled operators after 30 days." = "A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltető számára."; +"Conditions will be accepted for enabled operators after 30 days." = "A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltetők számára."; /* No comment provided by engineer. */ "Conditions will be accepted for operator(s): **%@**." = "A feltételek el lesznek fogadva a következő üzemeltető(k) számára: **%@**."; @@ -1173,6 +1200,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Feltöltés megerősítése"; +/* token status text */ +"Confirmed" = "Megerősítve"; + /* server test step */ "Connect" = "Kapcsolódás"; @@ -1195,7 +1225,7 @@ "Connect to yourself?" = "Kapcsolódás saját magához?"; /* No comment provided by engineer. */ -"Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódás saját magához?\nEz az Ön egyszer használható meghívó-hivatkozása!"; +"Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódás saját magához?\nEz az Ön egyszer használható meghívási hivatkozása!"; /* No comment provided by engineer. */ "Connect to yourself?\nThis is your own SimpleX address!" = "Kapcsolódás saját magához?\nEz az Ön SimpleX-címe!"; @@ -1207,7 +1237,7 @@ "Connect via link" = "Kapcsolódás egy hivatkozáson keresztül"; /* No comment provided by engineer. */ -"Connect via one-time link" = "Kapcsolódás egyszer használható meghívó-hivatkozáson keresztül"; +"Connect via one-time link" = "Kapcsolódás egyszer használható meghívási hivatkozáson keresztül"; /* No comment provided by engineer. */ "Connect with %@" = "Kapcsolódás a következővel: %@"; @@ -1246,7 +1276,7 @@ "connecting (introduced)" = "kapcsolódás (bejelentve)"; /* No comment provided by engineer. */ -"connecting (introduction invitation)" = "kapcsolódás (bemutatkozó-meghívó)"; +"connecting (introduction invitation)" = "kapcsolódás (bemutatkozó meghívó)"; /* call status */ "connecting call" = "kapcsolódási hívás…"; @@ -1318,7 +1348,7 @@ "Connections" = "Kapcsolatok"; /* profile update event chat item */ -"contact %@ changed to %@" = "%1$@ megváltoztatta a nevét a következőre: %2$@"; +"contact %@ changed to %@" = "%1$@ a következőre módosította a nevét: %2$@"; /* No comment provided by engineer. */ "Contact allows" = "Ismerős engedélyezi"; @@ -1330,13 +1360,13 @@ "Contact deleted!" = "Ismerős törölve!"; /* No comment provided by engineer. */ -"contact has e2e encryption" = "az ismerősnél az e2e titkosítás elérhető"; +"contact has e2e encryption" = "az ismerős e2e titkosítással rendelkezik"; /* No comment provided by engineer. */ -"contact has no e2e encryption" = "az ismerősnél az e2e titkosítás nem elérhető"; +"contact has no e2e encryption" = "az ismerős nem rendelkezik e2e titkosítással"; /* notification */ -"Contact hidden:" = "Ismerős elrejtve:"; +"Contact hidden:" = "Rejtett név:"; /* notification */ "Contact is connected" = "Ismerőse kapcsolódott"; @@ -1375,7 +1405,7 @@ "Copy error" = "Másolási hiba"; /* No comment provided by engineer. */ -"Core version: v%@" = "Alapverziószám: v%@"; +"Core version: v%@" = "Fő verzió: v%@"; /* No comment provided by engineer. */ "Corner" = "Sarok"; @@ -1387,10 +1417,10 @@ "Create" = "Létrehozás"; /* No comment provided by engineer. */ -"Create 1-time link" = "Egyszer használható meghívó-hivatkozás létrehozása"; +"Create 1-time link" = "Egyszer használható meghívási hivatkozás létrehozása"; /* No comment provided by engineer. */ -"Create a group using a random profile." = "Csoport létrehozása véletlenszerűen létrehozott profillal."; +"Create a group using a random profile." = "Csoport létrehozása véletlenszerű profillal."; /* server test step */ "Create file" = "Fájl létrehozása"; @@ -1414,7 +1444,7 @@ "Create profile" = "Profil létrehozása"; /* server test step */ -"Create queue" = "Sorbaállítás létrehozása"; +"Create queue" = "Sorba állítás létrehozása"; /* No comment provided by engineer. */ "Create secret group" = "Titkos csoport létrehozása"; @@ -1429,10 +1459,10 @@ "Created" = "Létrehozva"; /* No comment provided by engineer. */ -"Created at" = "Létrehozva ekkor:"; +"Created at" = "Létrehozva"; /* copied message info */ -"Created at: %@" = "Létrehozva ekkor: %@"; +"Created at: %@" = "Létrehozva: %@"; /* No comment provided by engineer. */ "Creating archive link" = "Archívum hivatkozás létrehozása"; @@ -1456,16 +1486,16 @@ "Current profile" = "Jelenlegi profil"; /* No comment provided by engineer. */ -"Currently maximum supported file size is %@." = "Jelenleg a maximális támogatott fájlméret %@."; +"Currently maximum supported file size is %@." = "Jelenleg támogatott legnagyobb fájl méret: %@."; /* dropdown time picker choice */ -"custom" = "egyedi"; +"custom" = "egyéni"; /* No comment provided by engineer. */ -"Custom time" = "Személyreszabott idő"; +"Custom time" = "Egyéni időköz"; /* No comment provided by engineer. */ -"Customizable message shape." = "Testreszabható üzenetbuborékok."; +"Customizable message shape." = "Testre szabható üzenetbuborékok."; /* No comment provided by engineer. */ "Customize theme" = "Téma személyre szabása"; @@ -1486,7 +1516,7 @@ "Database encryption passphrase will be updated and stored in the keychain.\n" = "Az adatbázis titkosítási jelmondata frissülni fog és a kulcstartóban lesz tárolva.\n"; /* No comment provided by engineer. */ -"Database encryption passphrase will be updated.\n" = "Az datbázis titkosítási jelmondata frissülni fog.\n"; +"Database encryption passphrase will be updated.\n" = "Az adatbázis titkosítási jelmondata frissítve lesz.\n"; /* No comment provided by engineer. */ "Database error" = "Adatbázishiba"; @@ -1501,10 +1531,10 @@ "Database IDs and Transport isolation option." = "Adatbázis-azonosítók és átvitel-izolációs beállítások."; /* No comment provided by engineer. */ -"Database is encrypted using a random passphrase, you can change it." = "Az adatbázis egy véletlenszerű jelmondattal van titkosítva, ami megváltoztatható."; +"Database is encrypted using a random passphrase, you can change it." = "Az adatbázis egy véletlenszerű jelmondattal van titkosítva, amelyet szabadon módosíthat."; /* No comment provided by engineer. */ -"Database is encrypted using a random passphrase. Please change it before exporting." = "Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt változtassa meg."; +"Database is encrypted using a random passphrase. Please change it before exporting." = "Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt módosítsa."; /* No comment provided by engineer. */ "Database passphrase" = "Adatbázis-jelmondat"; @@ -1563,10 +1593,10 @@ "Delete" = "Törlés"; /* No comment provided by engineer. */ -"Delete %lld messages of members?" = "Tagok %lld üzenetének törlése?"; +"Delete %lld messages of members?" = "Törli a tagok %lld üzenetét?"; /* No comment provided by engineer. */ -"Delete %lld messages?" = "Töröl %lld üzenetet?"; +"Delete %lld messages?" = "%lld üzenet törlése?"; /* No comment provided by engineer. */ "Delete address" = "Cím törlése"; @@ -1575,7 +1605,7 @@ "Delete address?" = "Cím törlése?"; /* No comment provided by engineer. */ -"Delete after" = "Törlés ennyi idő után:"; +"Delete after" = "Törlés ennyi idő után"; /* No comment provided by engineer. */ "Delete all files" = "Az összes fájl törlése"; @@ -1617,7 +1647,7 @@ "Delete file" = "Fájl törlése"; /* No comment provided by engineer. */ -"Delete files and media?" = "Fájlok és a médiatartalmak törlése?"; +"Delete files and media?" = "A fájlok- és a médiatartalmak törlése?"; /* No comment provided by engineer. */ "Delete files for all chat profiles" = "Fájlok törlése az összes csevegési profilból"; @@ -1638,7 +1668,7 @@ "Delete invitation" = "Meghívó törlése"; /* No comment provided by engineer. */ -"Delete link" = "Hivatkozás törlése"; +"Delete link" = "Törlés"; /* No comment provided by engineer. */ "Delete link?" = "Hivatkozás törlése?"; @@ -1656,7 +1686,7 @@ "Delete messages" = "Üzenetek törlése"; /* No comment provided by engineer. */ -"Delete messages after" = "Üzenetek törlése ennyi idő után:"; +"Delete messages after" = "Üzenetek törlése ennyi idő után"; /* No comment provided by engineer. */ "Delete old database" = "Régi adatbázis törlése"; @@ -1674,7 +1704,7 @@ "Delete profile" = "Profil törlése"; /* server test step */ -"Delete queue" = "Sorbaállítás törlése"; +"Delete queue" = "Sorba állítás törlése"; /* No comment provided by engineer. */ "Delete report" = "Jelentés törlése"; @@ -1695,10 +1725,10 @@ "Deleted" = "Törölve"; /* No comment provided by engineer. */ -"Deleted at" = "Törölve ekkor:"; +"Deleted at" = "Törölve"; /* copied message info */ -"Deleted at: %@" = "Törölve ekkor: %@"; +"Deleted at: %@" = "Törölve: %@"; /* rcv direct event chat item */ "deleted contact" = "törölt ismerős"; @@ -1716,7 +1746,7 @@ "Delivery" = "Kézbesítés"; /* No comment provided by engineer. */ -"Delivery receipts are disabled!" = "A kézbesítési jelentések ki vannak kapcsolva!"; +"Delivery receipts are disabled!" = "A kézbesítési jelentések le vannak tiltva!"; /* No comment provided by engineer. */ "Delivery receipts!" = "Kézbesítési jelentések!"; @@ -1728,7 +1758,7 @@ "Desktop address" = "Számítógép címe"; /* No comment provided by engineer. */ -"Desktop app version %@ is not compatible with this app." = "A számítógép-alkalmazás verziója %@ nem kompatibilis ezzel az alkalmazással."; +"Desktop app version %@ is not compatible with this app." = "A számítógép-alkalmazás verziója (%@) nem kompatibilis ezzel az alkalmazással."; /* No comment provided by engineer. */ "Desktop devices" = "Számítógépek"; @@ -1737,7 +1767,7 @@ "Destination server address of %@ is incompatible with forwarding server %@ settings." = "A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbító kiszolgáló beállításaival."; /* snd error text */ -"Destination server error: %@" = "Célkiszolgáló hiba: %@"; +"Destination server error: %@" = "Célkiszolgáló-hiba: %@"; /* No comment provided by engineer. */ "Destination server version of %@ is incompatible with forwarding server %@." = "A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbító kiszolgálóval."; @@ -1767,7 +1797,7 @@ "Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén."; /* No comment provided by engineer. */ -"different migration in the app/database: %@ / %@" = "különböző átköltöztetések az alkalmazásban/adatbázisban: %@ / %@"; +"different migration in the app/database: %@ / %@" = "különböző átköltöztetés az alkalmazásban/adatbázisban: %@ / %@"; /* No comment provided by engineer. */ "Different names, avatars and transport isolation." = "Különböző nevek, profilképek és átvitel-izoláció."; @@ -1782,7 +1812,7 @@ "Direct messages between members are prohibited in this chat." = "A tagok közötti közvetlen üzenetek le vannak tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Direct messages between members are prohibited." = "A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban."; +"Direct messages between members are prohibited." = "A tagok közötti közvetlen üzenetek le vannak tiltva."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Letiltás (felülírások megtartásával)"; @@ -1794,7 +1824,7 @@ "Disable delete messages" = "Üzenetek törlésének letiltása"; /* No comment provided by engineer. */ -"Disable for all" = "Letiltás az összes tag számára"; +"Disable for all" = "Letiltás"; /* authentication reason */ "Disable SimpleX Lock" = "SimpleX-zár kikapcsolása"; @@ -1815,13 +1845,13 @@ "Disappearing messages are prohibited in this chat." = "Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited." = "Az eltűnő üzenetek küldése le van tiltva ebben a csoportban."; +"Disappearing messages are prohibited." = "Az eltűnő üzenetek küldése le van tiltva."; /* No comment provided by engineer. */ -"Disappears at" = "Eltűnik ekkor:"; +"Disappears at" = "Eltűnik"; /* copied message info */ -"Disappears at: %@" = "Eltűnik ekkor: %@"; +"Disappears at: %@" = "Eltűnik: %@"; /* server test step */ "Disconnect" = "Kapcsolat bontása"; @@ -1830,7 +1860,7 @@ "Disconnect desktop?" = "Számítógép leválasztása?"; /* No comment provided by engineer. */ -"Discover and join groups" = "Helyi csoportok felfedezése és csatlakozás"; +"Discover and join groups" = "Csoportok felfedezése és csatlakozás"; /* No comment provided by engineer. */ "Discover via local network" = "Felfedezés helyi hálózaton keresztül"; @@ -1851,7 +1881,7 @@ "Do NOT use private routing." = "NE használjon privát útválasztást."; /* No comment provided by engineer. */ -"Do NOT use SimpleX for emergency calls." = "NE használja a SimpleX-et segélyhívásokhoz."; +"Do NOT use SimpleX for emergency calls." = "NE használja a SimpleXet segélyhívásokhoz."; /* No comment provided by engineer. */ "Documents:" = "Dokumentumok:"; @@ -1975,7 +2005,7 @@ "Enabled" = "Engedélyezve"; /* No comment provided by engineer. */ -"Enabled for" = "Számukra engedélyezve:"; +"Enabled for" = "Számukra engedélyezve"; /* enabled status */ "enabled for contact" = "engedélyezve az ismerős számára"; @@ -2029,22 +2059,22 @@ "encryption ok" = "titkosítás rendben"; /* chat item text */ -"encryption ok for %@" = "titkosítás rendben van %@ számára"; +"encryption ok for %@" = "titkosítás rendben %@ számára"; /* chat item text */ -"encryption re-negotiation allowed" = "titkosítás-újraegyeztetés engedélyezve"; +"encryption re-negotiation allowed" = "a titkosítás újraegyeztetése engedélyezve van"; /* chat item text */ "encryption re-negotiation allowed for %@" = "a titkosítás újraegyeztetése engedélyezve van %@ számára"; /* message decrypt error item */ -"Encryption re-negotiation error" = "Titkosítás-újraegyeztetési hiba"; +"Encryption re-negotiation error" = "Hiba a titkosítás újraegyeztetésekor"; /* No comment provided by engineer. */ -"Encryption re-negotiation failed." = "Sikertelen titkosítás-újraegyeztetés."; +"Encryption re-negotiation failed." = "Nem sikerült a titkosítást újraegyeztetni."; /* chat item text */ -"encryption re-negotiation required" = "titkosítás-újraegyeztetés szükséges"; +"encryption re-negotiation required" = "a titkosítás újraegyeztetése szükséges"; /* chat item text */ "encryption re-negotiation required for %@" = "a titkosítás újraegyeztetése szükséges %@ számára"; @@ -2098,7 +2128,7 @@ "Error" = "Hiba"; /* No comment provided by engineer. */ -"Error aborting address change" = "Hiba a cím megváltoztatásának megszakításakor"; +"Error aborting address change" = "Hiba a cím módosításának megszakításakor"; /* alert title */ "Error accepting conditions" = "Hiba a feltételek elfogadásakor"; @@ -2113,20 +2143,23 @@ "Error adding server" = "Hiba a kiszolgáló hozzáadásakor"; /* No comment provided by engineer. */ -"Error changing address" = "Hiba a cím megváltoztatásakor"; +"Error changing address" = "Hiba a cím módosításakor"; /* No comment provided by engineer. */ "Error changing connection profile" = "Hiba a kapcsolati profilra való váltáskor"; /* No comment provided by engineer. */ -"Error changing role" = "Hiba a szerepkör megváltoztatásakor"; +"Error changing role" = "Hiba a szerepkör módosításakor"; /* No comment provided by engineer. */ -"Error changing setting" = "Hiba a beállítás megváltoztatásakor"; +"Error changing setting" = "Hiba a beállítás módosításakor"; /* No comment provided by engineer. */ "Error changing to incognito!" = "Hiba az inkognitóprofilra való váltáskor!"; +/* No comment provided by engineer. */ +"Error checking token status" = "Hiba a token állapotának ellenőrzésekor"; + /* No comment provided by engineer. */ "Error connecting to forwarding server %@. Please try later." = "Hiba a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később."; @@ -2206,7 +2239,7 @@ "Error loading servers" = "Hiba a kiszolgálók betöltésekor"; /* No comment provided by engineer. */ -"Error migrating settings" = "Hiba a beallítások átköltöztetésekor"; +"Error migrating settings" = "Hiba a beállítások átköltöztetésekor"; /* No comment provided by engineer. */ "Error opening chat" = "Hiba a csevegés megnyitásakor"; @@ -2220,6 +2253,9 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Hiba a kiszolgálókhoz való újrakapcsolódáskor"; +/* alert title */ +"Error registering for notifications" = "Hiba az értesítések regisztrálásakor"; + /* No comment provided by engineer. */ "Error removing member" = "Hiba a tag eltávolításakor"; @@ -2251,7 +2287,7 @@ "Error saving settings" = "Hiba a beállítások mentésekor"; /* No comment provided by engineer. */ -"Error saving user password" = "Hiba a felhasználói jelszó mentésekor"; +"Error saving user password" = "Hiba a felhasználó jelszavának mentésekor"; /* No comment provided by engineer. */ "Error scanning code: %@" = "Hiba a kód beolvasásakor: %@"; @@ -2278,11 +2314,14 @@ "Error switching profile" = "Hiba a profilváltáskor"; /* alertTitle */ -"Error switching profile!" = "Hiba a profilváltásakor!"; +"Error switching profile!" = "Hiba a profilváltáskor!"; /* No comment provided by engineer. */ "Error synchronizing connection" = "Hiba a kapcsolat szinkronizálásakor"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Hiba a kiszolgáló kapcsolatának tesztelésekor"; + /* No comment provided by engineer. */ "Error updating group link" = "Hiba a csoporthivatkozás frissítésekor"; @@ -2337,6 +2376,9 @@ /* No comment provided by engineer. */ "expired" = "lejárt"; +/* token status text */ +"Expired" = "Lejárt"; + /* No comment provided by engineer. */ "Export database" = "Adatbázis exportálása"; @@ -2383,13 +2425,13 @@ "File not found - most likely file was deleted or cancelled." = "A fájl nem található – valószínűleg a fájlt törölték vagy visszavonták."; /* file error text */ -"File server error: %@" = "Fájlkiszolgáló hiba: %@"; +"File server error: %@" = "Fájlkiszolgáló-hiba: %@"; /* No comment provided by engineer. */ -"File status" = "Fájlállapot"; +"File status" = "Fájl állapota"; /* copied message info */ -"File status: %@" = "Fájlállapot: %@"; +"File status: %@" = "Fájl állapota: %@"; /* No comment provided by engineer. */ "File will be deleted from servers." = "A fájl törölve lesz a kiszolgálókról."; @@ -2413,7 +2455,7 @@ "Files and media" = "Fájlok és médiatartalmak"; /* No comment provided by engineer. */ -"Files and media are prohibited." = "A fájlok- és a médiatartalmak le vannak tiltva ebben a csoportban."; +"Files and media are prohibited." = "A fájlok- és a médiatartalmak küldése le van tiltva."; /* No comment provided by engineer. */ "Files and media not allowed" = "A fájlok- és médiatartalmak nincsenek engedélyezve"; @@ -2454,6 +2496,9 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Csoporttag általi javítás nem támogatott"; +/* No comment provided by engineer. */ +"For all moderators" = "Az összes moderátor számára"; + /* No comment provided by engineer. */ "for better metadata privacy." = "a metaadatok jobb védelme érdekében."; @@ -2466,6 +2511,9 @@ /* No comment provided by engineer. */ "For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Például, ha az Ön ismerőse egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni."; +/* No comment provided by engineer. */ +"For me" = "Csak magamnak"; + /* No comment provided by engineer. */ "For private routing" = "A privát útválasztáshoz"; @@ -2497,7 +2545,7 @@ "Forwarded" = "Továbbított"; /* No comment provided by engineer. */ -"Forwarded from" = "Továbbítva innen:"; +"Forwarded from" = "Továbbítva innen"; /* No comment provided by engineer. */ "Forwarding %lld messages" = "%lld üzenet továbbítása"; @@ -2512,7 +2560,7 @@ "Forwarding server version is incompatible with network settings: %@." = "A továbbító-kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@."; /* snd error text */ -"Forwarding server: %@\nDestination server error: %@" = "Továbbító-kiszolgáló: %1$@\nCélkiszolgáló hiba:%2$@"; +"Forwarding server: %@\nDestination server error: %@" = "Továbbító-kiszolgáló: %1$@\nCélkiszolgáló-hiba: %2$@"; /* snd error text */ "Forwarding server: %@\nError: %@" = "Továbbító-kiszolgáló: %1$@\nHiba: %2$@"; @@ -2587,7 +2635,7 @@ "Group message:" = "Csoport üzenet:"; /* No comment provided by engineer. */ -"Group moderation" = "Csoport moderáció"; +"Group moderation" = "Csoport moderálása"; /* No comment provided by engineer. */ "Group preferences" = "Csoport-beállítások"; @@ -2662,10 +2710,10 @@ "How to" = "Hogyan"; /* No comment provided by engineer. */ -"How to use it" = "Hogyan használja"; +"How to use it" = "Használati útmutató"; /* No comment provided by engineer. */ -"How to use your servers" = "Saját kiszolgálók használata"; +"How to use your servers" = "Hogyan használja a saját kiszolgálóit"; /* No comment provided by engineer. */ "Hungarian interface" = "Magyar kezelőfelület"; @@ -2698,7 +2746,7 @@ "Immediately" = "Azonnal"; /* No comment provided by engineer. */ -"Immune to spam" = "Spam és visszaélések elleni védelem"; +"Immune to spam" = "Védett a kéretlen tartalommal szemben"; /* No comment provided by engineer. */ "Import" = "Importálás"; @@ -2734,7 +2782,7 @@ "In order to continue, chat should be stopped." = "A folytatáshoz a csevegést meg kell szakítani."; /* No comment provided by engineer. */ -"In reply to" = "Válasz neki"; +"In reply to" = "Válaszul erre"; /* No comment provided by engineer. */ "In-call sounds" = "Bejövő hívás csengőhangja"; @@ -2767,7 +2815,7 @@ "incognito via group link" = "inkognitó a csoporthivatkozáson keresztül"; /* chat list item description */ -"incognito via one-time link" = "inkognitó egy egyszer használható meghívó-hivatkozáson keresztül"; +"incognito via one-time link" = "inkognitó egy egyszer használható meghívási hivatkozáson keresztül"; /* notification */ "Incoming audio call" = "Bejövő hanghívás"; @@ -2803,7 +2851,7 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "A [SimpleX Chat terminálhoz] telepítése (https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant" = "Azonnal"; +"Instant" = "Azonnali"; /* No comment provided by engineer. */ "Instant push notifications will be hidden!\n" = "Az azonnali push-értesítések el lesznek rejtve!\n"; @@ -2814,6 +2862,21 @@ /* No comment provided by engineer. */ "Interface colors" = "Kezelőfelület színei"; +/* token status text */ +"Invalid" = "Érvénytelen"; + +/* token status text */ +"Invalid (bad token)" = "Érvénytelen (hibás token)"; + +/* token status text */ +"Invalid (expired)" = "Érvénytelen (lejárt)"; + +/* token status text */ +"Invalid (unregistered)" = "Érvénytelen (nincs regisztrálva)"; + +/* token status text */ +"Invalid (wrong topic)" = "Érvénytelen (rossz topic)"; + /* invalid chat data */ "invalid chat" = "érvénytelen csevegés"; @@ -2887,7 +2950,7 @@ "iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál – lehetővé teszi a push-értesítések fogadását."; /* No comment provided by engineer. */ -"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Az iOS kulcstartó biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat megváltoztatása után – lehetővé teszi a push-értesítések fogadását."; +"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Az iOS kulcstartó biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat módosítása után – lehetővé teszi a push-értesítések fogadását."; /* No comment provided by engineer. */ "IP address" = "IP-cím"; @@ -2899,7 +2962,7 @@ "Irreversible message deletion is prohibited in this chat." = "Az üzenetek végleges törlése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited." = "Az üzenetek végleges törlése le van tiltva ebben a csoportban."; +"Irreversible message deletion is prohibited." = "Az üzenetek végleges törlése le van tiltva."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Lehetővé teszi, hogy egyetlen csevegőprofilon belül több névtelen kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük."; @@ -2908,7 +2971,7 @@ "It can happen when you or your connection used the old database backup." = "Ez akkor fordulhat elő, ha Ön vagy az ismerőse régi adatbázis biztonsági mentést használt."; /* No comment provided by engineer. */ -"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Az üzenet visszafejtése sikertelen volt, mert vagy az ismerőse régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült."; +"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Az üzenet visszafejtése sikertelen volt, mert Ön, vagy az ismerőse régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült."; /* No comment provided by engineer. */ "It protects your IP address and connections." = "Védi az IP-címét és a kapcsolatait."; @@ -2929,7 +2992,7 @@ "Join" = "Csatlakozás"; /* No comment provided by engineer. */ -"join as %@" = "csatlakozás mint: %@"; +"join as %@" = "csatlakozás mint %@"; /* No comment provided by engineer. */ "Join group" = "Csatlakozás csoporthoz"; @@ -2953,7 +3016,7 @@ "Joining group" = "Csatlakozás a csoporthoz"; /* alert action */ -"Keep" = "Megtart"; +"Keep" = "Megtartás"; /* No comment provided by engineer. */ "Keep conversation" = "Beszélgetés megtartása"; @@ -3019,7 +3082,7 @@ "List" = "Lista"; /* No comment provided by engineer. */ -"List name and emoji should be different for all lists." = "Az összes lista nevének és emodzsiának különbözőnek kell lennie."; +"List name and emoji should be different for all lists." = "Az összes lista nevének és emodzsijának különbözőnek kell lennie."; /* No comment provided by engineer. */ "List name..." = "Listanév…"; @@ -3052,7 +3115,7 @@ "Make profile private!" = "Tegye priváttá a profilját!"; /* No comment provided by engineer. */ -"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva."; +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Győződjön meg arról, hogy a megadott WebRTC ICE-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva."; /* No comment provided by engineer. */ "Mark deleted for everyone" = "Jelölje meg az összes tag számára töröltként"; @@ -3073,7 +3136,7 @@ "Max 30 seconds, received instantly." = "Max. 30 másodperc, azonnal érkezett."; /* No comment provided by engineer. */ -"Media & file servers" = "Média és fájlkiszolgálók"; +"Media & file servers" = "Média- és fájlkiszolgálók"; /* blur media */ "Medium" = "Közepes"; @@ -3085,7 +3148,7 @@ "Member" = "Tag"; /* profile update event chat item */ -"member %@ changed to %@" = "%1$@ megváltoztatta a nevét a következőre: %2$@"; +"member %@ changed to %@" = "%1$@ a következőre módosította a nevét: %2$@"; /* rcv group event chat item */ "member connected" = "kapcsolódott"; @@ -3093,14 +3156,17 @@ /* item status text */ "Member inactive" = "Inaktív tag"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "A tag szerepeköre meg fog változni a következőre: \"%@\". A csevegés tagjai értesítést fognak kapni."; +/* chat feature */ +"Member reports" = "Tagok jelentései"; /* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni a következőre: „%@”. A csoportban az összes tag értesítve lesz."; +"Member role will be changed to \"%@\". All chat members will be notified." = "A tag szerepköre a következőre fog módosulni: „%@”. A csevegés összes tagja értesítést fog kapni."; /* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "A tag szerepköre meg fog változni a következőre: „%@”. A tag új meghívást fog kapni."; +"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre a következőre fog módosulni: „%@”. A csoport az összes tagja értesítést fog kapni."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "A tag szerepköre a következőre fog módosulni: „%@”. A tag új meghívást fog kapni."; /* No comment provided by engineer. */ "Member will be removed from chat - this cannot be undone!" = "A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza!"; @@ -3109,25 +3175,28 @@ "Member will be removed from group - this cannot be undone!" = "A tag eltávolítása a csoportból – ez a művelet nem vonható vissza!"; /* No comment provided by engineer. */ -"Members can add message reactions." = "Csoporttagok üzenetreakciókat adhatnak hozzá."; +"Members can add message reactions." = "A tagok reakciókat adhatnak hozzá az üzenetekhez."; /* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra)"; +"Members can irreversibly delete sent messages. (24 hours)" = "A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra)"; /* No comment provided by engineer. */ -"Members can send direct messages." = "A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket."; +"Members can report messsages to moderators." = "A tagok jelenthetik az üzeneteket a moderátorok felé."; /* No comment provided by engineer. */ -"Members can send disappearing messages." = "A csoport tagjai küldhetnek eltűnő üzeneteket."; +"Members can send direct messages." = "A tagok küldhetnek egymásnak közvetlen üzeneteket."; /* No comment provided by engineer. */ -"Members can send files and media." = "A csoport tagjai küldhetnek fájlokat és médiatartalmakat."; +"Members can send disappearing messages." = "A tagok küldhetnek eltűnő üzeneteket."; /* No comment provided by engineer. */ -"Members can send SimpleX links." = "A csoport tagjai küldhetnek SimpleX-hivatkozásokat."; +"Members can send files and media." = "A tagok küldhetnek fájlokat és médiatartalmakat."; /* No comment provided by engineer. */ -"Members can send voice messages." = "A csoport tagjai küldhetnek hangüzeneteket."; +"Members can send SimpleX links." = "A tagok küldhetnek SimpleX-hivatkozásokat."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "A tagok küldhetnek hangüzeneteket."; /* No comment provided by engineer. */ "Menus" = "Menük"; @@ -3154,16 +3223,16 @@ "Message may be delivered later if member becomes active." = "Az üzenet később is kézbesíthető, ha a tag aktívvá válik."; /* No comment provided by engineer. */ -"Message queue info" = "Üzenet-sorbaállítási információ"; +"Message queue info" = "Üzenetsorbaállítási információ"; /* chat feature */ "Message reactions" = "Üzenetreakciók"; /* No comment provided by engineer. */ -"Message reactions are prohibited in this chat." = "Az üzenetreakciók küldése le van tiltva ebben a csevegésben."; +"Message reactions are prohibited in this chat." = "A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Message reactions are prohibited." = "Az üzenetreakciók küldése le van tiltva ebben a csoportban."; +"Message reactions are prohibited." = "A reakciók hozzáadása az üzenetekhez le van tiltva."; /* notification */ "message received" = "üzenet érkezett"; @@ -3175,16 +3244,16 @@ "Message servers" = "Üzenetkiszolgálók"; /* No comment provided by engineer. */ -"Message shape" = "Üzenetbuborék formája"; +"Message shape" = "Üzenetbuborék alakja"; /* No comment provided by engineer. */ "Message source remains private." = "Az üzenet forrása titokban marad."; /* No comment provided by engineer. */ -"Message status" = "Üzenetállapot"; +"Message status" = "Üzenet állapota"; /* copied message info */ -"Message status: %@" = "Üzenetállapot: %@"; +"Message status: %@" = "Üzenet állapota: %@"; /* No comment provided by engineer. */ "Message text" = "Név és üzenet"; @@ -3199,7 +3268,7 @@ "Messages & files" = "Üzenetek és fájlok"; /* No comment provided by engineer. */ -"Messages from %@ will be shown!" = "A(z) %@ által írt üzenetek megjelennek!"; +"Messages from %@ will be shown!" = "Az összes %@ által írt üzenet meg fog jelenni!"; /* alert message */ "Messages in this chat will never be deleted." = "Az ebben a csevegésben lévő üzenetek soha nem lesznek törölve."; @@ -3268,10 +3337,10 @@ "moderated" = "moderált"; /* No comment provided by engineer. */ -"Moderated at" = "Moderálva ekkor:"; +"Moderated at" = "Moderálva"; /* copied message info */ -"Moderated at: %@" = "Moderálva ekkor: %@"; +"Moderated at: %@" = "Moderálva: %@"; /* marked deleted chat item preview text */ "moderated by %@" = "moderálva lett %@ által"; @@ -3303,6 +3372,9 @@ /* notification label action */ "Mute" = "Némítás"; +/* notification label action */ +"Mute all" = "Összes némítása"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Némítás, ha inaktív!"; @@ -3336,8 +3408,11 @@ /* delete after time */ "never" = "soha"; +/* token status text */ +"New" = "Új"; + /* No comment provided by engineer. */ -"New chat" = "Új beszélgetés"; +"New chat" = "Új csevegés"; /* No comment provided by engineer. */ "New chat experience 🎉" = "Új csevegési élmény 🎉"; @@ -3415,7 +3490,7 @@ "No delivery information" = "Nincs kézbesítési információ"; /* No comment provided by engineer. */ -"No device token!" = "Nincs kiszüléktoken!"; +"No device token!" = "Nincs készüléktoken!"; /* item status description */ "No direct connection yet, message is forwarded by admin." = "Még nincs közvetlen kapcsolat, az üzenetet az adminisztrátor továbbítja."; @@ -3463,10 +3538,10 @@ "No servers for private message routing." = "Nincsenek kiszolgálók a privát üzenet-útválasztáshoz."; /* servers error */ -"No servers to receive files." = "Nincsenek fájlfogadó-kiszolgálók."; +"No servers to receive files." = "Nincsenek fájlfogadási kiszolgálók."; /* servers error */ -"No servers to receive messages." = "Nincsenek üzenetfogadó-kiszolgálók."; +"No servers to receive messages." = "Nincsenek üzenetfogadási kiszolgálók."; /* servers error */ "No servers to send files." = "Nincsenek fájlküldő-kiszolgálók."; @@ -3474,6 +3549,9 @@ /* copied message info in history */ "no text" = "nincs szöveg"; +/* alert title */ +"No token!" = "Nincs token!"; + /* No comment provided by engineer. */ "No unread chats" = "Nincsenek olvasatlan csevegések"; @@ -3498,9 +3576,15 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Az értesítések le vannak tiltva!"; +/* alert title */ +"Notifications error" = "Értesítési hiba"; + /* No comment provided by engineer. */ "Notifications privacy" = "Értesítési adatvédelem"; +/* alert title */ +"Notifications status" = "Értesítések állapota"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat („megfigyelő” szerepkör)"; @@ -3519,7 +3603,7 @@ "offered %@" = "%@ ajánlotta"; /* feature offered item */ -"offered %@: %@" = "ajánlotta %1$@: %2$@-kor"; +"offered %@: %@" = "ajánlotta: %1$@, ekkor: %2$@"; /* alert button */ "Ok" = "Rendben"; @@ -3534,7 +3618,7 @@ "on" = "bekapcsolva"; /* No comment provided by engineer. */ -"One-time invitation link" = "Egyszer használható meghívó-hivatkozás"; +"One-time invitation link" = "Egyszer használható meghívási hivatkozás"; /* No comment provided by engineer. */ "Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Onion-kiszolgálók **szükségesek** a kapcsolódáshoz.\nKompatibilis VPN szükséges."; @@ -3543,10 +3627,10 @@ "Onion hosts will be used when available.\nRequires compatible VPN." = "Onion-kiszolgálók használata, ha azok rendelkezésre állnak.\nVPN engedélyezése szükséges."; /* No comment provided by engineer. */ -"Onion hosts will not be used." = "Onion-kiszolgálók nem lesznek használva."; +"Onion hosts will not be used." = "Az onion-kiszolgálók nem lesznek használva."; /* No comment provided by engineer. */ -"Only chat owners can change preferences." = "Csak a csevegés tulajdonosai változtathatják meg a beállításokat."; +"Only chat owners can change preferences." = "Csak a csevegés tulajdonosai módosíthatják a csevegés-beállításokat."; /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages." = "A felhasználói profilok, ismerősök, csoportok és üzenetek csak az eszközön vannak tárolva a kliensen belül."; @@ -3555,13 +3639,13 @@ "Only delete conversation" = "Csak a beszélgetés törlése"; /* No comment provided by engineer. */ -"Only group owners can change group preferences." = "Csak a csoport-tulajdonosok változtathatják meg a csoport-beállításokat."; +"Only group owners can change group preferences." = "Csak a csoport tulajdonosai módosíthatják a csoport-beállításokat."; /* No comment provided by engineer. */ -"Only group owners can enable files and media." = "Csak a csoport-tulajdonosok engedélyezhetik a fájlok- és a médiatartalmak küldését."; +"Only group owners can enable files and media." = "Csak a csoport tulajdonosai engedélyezhetik a fájlok- és a médiatartalmak küldését."; /* No comment provided by engineer. */ -"Only group owners can enable voice messages." = "Csak a csoport-tulajdonosok engedélyezhetik a hangüzenetek küldését."; +"Only group owners can enable voice messages." = "Csak a csoport tulajdonosai engedélyezhetik a hangüzenetek küldését."; /* No comment provided by engineer. */ "Only sender and moderators see it" = "Csak a küldő és a moderátorok látják"; @@ -3570,7 +3654,7 @@ "Only you and moderators see it" = "Csak Ön és a moderátorok látják"; /* No comment provided by engineer. */ -"Only you can add message reactions." = "Csak Ön adhat hozzá üzenetreakciókat."; +"Only you can add message reactions." = "Csak Ön adhat hozzá reakciókat az üzenetekhez."; /* No comment provided by engineer. */ "Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Véglegesen csak Ön törölhet üzeneteket (ismerőse csak törlésre jelölheti meg őket ). (24 óra)"; @@ -3585,7 +3669,7 @@ "Only you can send voice messages." = "Csak Ön tud hangüzeneteket küldeni."; /* No comment provided by engineer. */ -"Only your contact can add message reactions." = "Csak az ismerőse tud üzenetreakciókat küldeni."; +"Only your contact can add message reactions." = "Csak az ismerőse adhat hozzá reakciókat az üzenetekhez."; /* No comment provided by engineer. */ "Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Csak az ismerőse tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra)"; @@ -3603,7 +3687,7 @@ "Open" = "Megnyitás"; /* No comment provided by engineer. */ -"Open changes" = "Változások megnyitása"; +"Open changes" = "Módosítások megtekintése"; /* No comment provided by engineer. */ "Open chat" = "Csevegés megnyitása"; @@ -3639,7 +3723,7 @@ "Operator" = "Üzemeltető"; /* alert title */ -"Operator server" = "Kiszolgáló üzemeltető"; +"Operator server" = "Kiszolgáló-üzemeltető"; /* No comment provided by engineer. */ "Or import archive file" = "Vagy archívumfájl importálása"; @@ -3681,13 +3765,13 @@ "Passcode" = "Jelkód"; /* No comment provided by engineer. */ -"Passcode changed!" = "A jelkód megváltozott!"; +"Passcode changed!" = "A jelkód módosult!"; /* No comment provided by engineer. */ "Passcode entry" = "Jelkód bevitele"; /* No comment provided by engineer. */ -"Passcode not changed!" = "A jelkód nem változott meg!"; +"Passcode not changed!" = "A jelkód nem módosult!"; /* No comment provided by engineer. */ "Passcode set!" = "A jelkód beállítva!"; @@ -3696,7 +3780,7 @@ "Password" = "Jelszó"; /* No comment provided by engineer. */ -"Password to show" = "Jelszó megjelenítése"; +"Password to show" = "Jelszó a megjelenítéshez"; /* past/unknown group member */ "Past member %@" = "(Már nem tag) %@"; @@ -3714,13 +3798,13 @@ "Paste the link you received" = "Kapott hivatkozás beillesztése"; /* No comment provided by engineer. */ -"peer-to-peer" = "ponttól-pontig"; +"peer-to-peer" = "egyenrangú"; /* No comment provided by engineer. */ "Pending" = "Függőben"; /* No comment provided by engineer. */ -"Periodic" = "Rendszeresen"; +"Periodic" = "Időszakos"; /* message decrypt error item */ "Permanent decryption error" = "Végleges visszafejtési hiba"; @@ -3783,7 +3867,16 @@ "Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Tárolja el biztonságosan jelmondát, mert ha elveszti azt, akkor NEM férhet hozzá a csevegéshez."; /* No comment provided by engineer. */ -"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja megváltoztatni."; +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja módosítani."; + +/* token info */ +"Please try to disable and re-enable notfications." = "Próbálja meg letiltani és újra engedélyezni az értesítéseket."; + +/* token info */ +"Please wait for token activation to complete." = "Várjon, amíg a token aktiválása befejeződik."; + +/* token info */ +"Please wait for token to be registered." = "Várjon a token regisztrálására."; /* No comment provided by engineer. */ "Polish interface" = "Lengyel kezelőfelület"; @@ -3798,7 +3891,7 @@ "Preserve the last message draft, with attachments." = "Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt."; /* No comment provided by engineer. */ -"Preset server address" = "Előre beállított kiszolgáló címe"; +"Preset server address" = "Az előre beállított kiszolgáló címe"; /* No comment provided by engineer. */ "Preset servers" = "Előre beállított kiszolgálók"; @@ -3834,7 +3927,7 @@ "Private routing" = "Privát útválasztás"; /* No comment provided by engineer. */ -"Private routing error" = "Privát útválasztáshiba"; +"Private routing error" = "Privát útválasztási hiba"; /* No comment provided by engineer. */ "Profile and server connections" = "Profil és kiszolgálókapcsolatok"; @@ -3858,13 +3951,16 @@ "Prohibit audio/video calls." = "A hívások kezdeményezése le van tiltva."; /* No comment provided by engineer. */ -"Prohibit irreversible message deletion." = "Az üzenetek véglegesen való törlése le van tiltva."; +"Prohibit irreversible message deletion." = "Az elküldött üzenetek végleges törlése le van tiltva."; /* No comment provided by engineer. */ -"Prohibit message reactions." = "Az üzenetreakciók küldése le van tiltva."; +"Prohibit message reactions." = "A reakciók hozzáadása az üzenethez le van tiltva."; /* No comment provided by engineer. */ -"Prohibit messages reactions." = "Az üzenetreakciók tiltása."; +"Prohibit messages reactions." = "A reakciók hozzáadása az üzenetekhez le van tiltva."; + +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Az üzenetek a moderátorok felé történő jelentésének megtiltása."; /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "A közvetlen üzenetek küldése a tagok között le van tiltva."; @@ -3873,7 +3969,7 @@ "Prohibit sending disappearing messages." = "Az eltűnő üzenetek küldése le van tiltva."; /* No comment provided by engineer. */ -"Prohibit sending files and media." = "Fájlok- és a médiatartalmak küldésének letiltása."; +"Prohibit sending files and media." = "A fájlok- és a médiatartalmak küldése le van tiltva."; /* No comment provided by engineer. */ "Prohibit sending SimpleX links." = "A SimpleX-hivatkozások küldése le van tiltva."; @@ -3891,7 +3987,7 @@ "Protect your chat profiles with a password!" = "Védje meg a csevegési profiljait egy jelszóval!"; /* No comment provided by engineer. */ -"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Védje IP-címét az ismerősei által kiválasztott üzenetváltó-továbbító-kiszolgálókkal szemben.\nEngedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben."; +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Védje IP-címét az ismerősei által kiválasztott üzenetváltó-továbbító-kiszolgálókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben."; /* No comment provided by engineer. */ "Protocol timeout" = "Protokoll időtúllépése"; @@ -3957,10 +4053,10 @@ "received answer…" = "válasz fogadása…"; /* No comment provided by engineer. */ -"Received at" = "Fogadva ekkor:"; +"Received at" = "Fogadva"; /* copied message info */ -"Received at: %@" = "Fogadva ekkor: %@"; +"Received at: %@" = "Fogadva: %@"; /* No comment provided by engineer. */ "received confirmation…" = "visszaigazolás fogadása…"; @@ -3981,13 +4077,13 @@ "Received total" = "Összes fogadott üzenet"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "A fogadócím egy másik kiszolgálóra változik. A cím megváltoztatása a feladó online állapotba kerülése után fejeződik be."; +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "A fogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása a feladó online állapotba kerülése után fejeződik be."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "A fájl fogadása le fog állni."; /* No comment provided by engineer. */ -"Receiving via" = "Fogadás a"; +"Receiving via" = "Fogadás a következőn keresztül:"; /* No comment provided by engineer. */ "Recent history and improved [directory bot](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)." = "Legutóbbi előzmények és továbbfejlesztett [könyvtárbot](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)."; @@ -4020,14 +4116,23 @@ "Reconnect servers?" = "Újrakapcsolódás a kiszolgálókhoz?"; /* No comment provided by engineer. */ -"Record updated at" = "A bejegyzés frissítve"; +"Record updated at" = "Bejegyzés frissítve"; /* copied message info */ -"Record updated at: %@" = "A bejegyzés frissítve: %@"; +"Record updated at: %@" = "Bejegyzés frissítve: %@"; /* No comment provided by engineer. */ "Reduced battery usage" = "Csökkentett akkumulátor-használat"; +/* No comment provided by engineer. */ +"Register" = "Regisztrálás"; + +/* token info */ +"Register notification token?" = "Értesítési token regisztrálása?"; + +/* token status text */ +"Registered" = "Regisztrálva"; + /* reject incoming call via notification swipe action */ "Reject" = "Elutasítás"; @@ -4081,7 +4186,7 @@ "removed you" = "eltávolította Önt"; /* No comment provided by engineer. */ -"Renegotiate" = "Újraegyzetetés"; +"Renegotiate" = "Újraegyeztetés"; /* No comment provided by engineer. */ "Renegotiate encryption" = "Titkosítás újraegyeztetése"; @@ -4128,6 +4233,12 @@ /* report reason */ "Report violation: only group moderators will see it." = "Szabálysértés jelentése: csak a csoport moderátorai látják."; +/* report in notification */ +"Report: (text.isEmpty ? reason.text : text)" = "Jelentés: (text.isEmpty ? reason.text : text)"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Az üzenetek jelentése a moderátorok felé le van tiltva."; + /* No comment provided by engineer. */ "Reports" = "Jelentések"; @@ -4268,7 +4379,7 @@ "Saved" = "Mentett"; /* No comment provided by engineer. */ -"Saved from" = "Elmentve innen:"; +"Saved from" = "Elmentve innen"; /* No comment provided by engineer. */ "saved from %@" = "elmentve innen: %@"; @@ -4310,7 +4421,7 @@ "Search" = "Keresés"; /* No comment provided by engineer. */ -"Search bar accepts invitation links." = "A keresősáv elfogadja a meghívó-hivatkozásokat."; +"Search bar accepts invitation links." = "A keresősáv elfogadja a meghívási hivatkozásokat."; /* No comment provided by engineer. */ "Search or paste SimpleX link" = "Keresés vagy SimpleX-hivatkozás beillesztése"; @@ -4328,7 +4439,7 @@ "secret" = "titok"; /* server test step */ -"Secure queue" = "Biztonságos sorbaállítás"; +"Secure queue" = "Biztonságos sorba állítás"; /* No comment provided by engineer. */ "Secured" = "Biztosítva"; @@ -4340,7 +4451,7 @@ "Security code" = "Biztonsági kód"; /* chat item text */ -"security code changed" = "a biztonsági kód megváltozott"; +"security code changed" = "a biztonsági kód módosult"; /* chat item action */ "Select" = "Kijelölés"; @@ -4352,7 +4463,7 @@ "Selected %lld" = "%lld kijelölve"; /* No comment provided by engineer. */ -"Selected chat preferences prohibit this message." = "A kijelölt csevegési beállítások tiltják ezt az üzenetet."; +"Selected chat preferences prohibit this message." = "A kijelölt csevegés-beállítások tiltják ezt az üzenetet."; /* No comment provided by engineer. */ "Self-destruct" = "Önmegsemmisítés"; @@ -4361,7 +4472,7 @@ "Self-destruct passcode" = "Önmegsemmisítő-jelkód"; /* No comment provided by engineer. */ -"Self-destruct passcode changed!" = "Az önmegsemmisítő-jelkód megváltozott!"; +"Self-destruct passcode changed!" = "Az önmegsemmisítő-jelkód módosult!"; /* No comment provided by engineer. */ "Self-destruct passcode enabled!" = "Az önmegsemmisítő-jelkód engedélyezve!"; @@ -4388,7 +4499,7 @@ "Send errors" = "Üzenetküldési hibák"; /* No comment provided by engineer. */ -"Send link previews" = "Hivatkozás előnézetek küldése"; +"Send link previews" = "Hivatkozás előnézete"; /* No comment provided by engineer. */ "Send live message" = "Élő üzenet küldése"; @@ -4406,7 +4517,7 @@ "Send notifications" = "Értesítések küldése"; /* No comment provided by engineer. */ -"Send questions and ideas" = "Ötletek és kérdések beküldése"; +"Send questions and ideas" = "Ötletek és javaslatok"; /* No comment provided by engineer. */ "Send receipts" = "Kézbesítési jelentések küldése"; @@ -4448,10 +4559,10 @@ "Sending via" = "Küldés a következőn keresztül:"; /* No comment provided by engineer. */ -"Sent at" = "Elküldve ekkor:"; +"Sent at" = "Elküldve"; /* copied message info */ -"Sent at: %@" = "Elküldve ekkor: %@"; +"Sent at: %@" = "Elküldve: %@"; /* No comment provided by engineer. */ "Sent directly" = "Közvetlenül küldött"; @@ -4493,22 +4604,22 @@ "Server address is incompatible with network settings." = "A kiszolgáló címe nem kompatibilis a hálózati beállításokkal."; /* alert title */ -"Server operator changed." = "A kiszolgáló üzemeltetője megváltozott."; +"Server operator changed." = "A kiszolgáló üzemeltetője módosult."; /* No comment provided by engineer. */ "Server operators" = "Kiszolgáló-üzemeltetők"; /* alert title */ -"Server protocol changed." = "A kiszolgáló-protokoll megváltozott."; +"Server protocol changed." = "A kiszolgáló-protokoll módosult."; /* queue info */ -"server queue info: %@\n\nlast received msg: %@" = "a kiszolgáló üzenet-sorbaállítási információi: %1$@\n\nutoljára fogadott üzenet: %2$@"; +"server queue info: %@\n\nlast received msg: %@" = "a kiszolgáló sorbaállítási információi: %1$@\n\nutoljára fogadott üzenet: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "A kiszolgálónak engedélyre van szüksége a sorbaállítás létrehozásához, ellenőrizze jelszavát"; +"Server requires authorization to create queues, check password" = "A kiszolgálónak engedélyre van szüksége a sorba állítás létrehozásához, ellenőrizze a jelszavát"; /* server test error */ -"Server requires authorization to upload, check password" = "A kiszolgálónak engedélyre van szüksége a várólisták feltöltéséhez, ellenőrizze jelszavát"; +"Server requires authorization to upload, check password" = "A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát"; /* No comment provided by engineer. */ "Server test failed!" = "Sikertelen kiszolgáló teszt!"; @@ -4550,10 +4661,10 @@ "Set group preferences" = "Csoport-beállítások megadása"; /* No comment provided by engineer. */ -"Set it instead of system authentication." = "Rendszerhitelesítés helyetti beállítás."; +"Set it instead of system authentication." = "Beállítás a rendszer-hitelesítés helyett."; /* profile update event chat item */ -"set new contact address" = "új kapcsolattartási cím beállítása"; +"set new contact address" = "új kapcsolattartási címet állított be"; /* profile update event chat item */ "set new profile picture" = "új profilképet állított be"; @@ -4568,7 +4679,7 @@ "Set passphrase to export" = "Jelmondat beállítása az exportáláshoz"; /* No comment provided by engineer. */ -"Set the message shown to new members!" = "Megjelenő üzenet beállítása az új tagok számára!"; +"Set the message shown to new members!" = "Megjelenítendő üzenet beállítása az új tagok számára!"; /* No comment provided by engineer. */ "Set timeouts for proxy/VPN" = "Időtúllépések beállítása a proxy/VPN számára"; @@ -4577,7 +4688,7 @@ "Settings" = "Beállítások"; /* alert message */ -"Settings were changed." = "A beállítások megváltoztak."; +"Settings were changed." = "A beállítások módosultak."; /* No comment provided by engineer. */ "Shape profile images" = "Profilkép alakzata"; @@ -4587,10 +4698,10 @@ "Share" = "Megosztás"; /* No comment provided by engineer. */ -"Share 1-time link" = "Egyszer használható hivatkozás megosztása"; +"Share 1-time link" = "Egyszer használható meghívási hivatkozás megosztása"; /* No comment provided by engineer. */ -"Share 1-time link with a friend" = "Egyszer használható meghívó-hivatkozás megosztása egy baráttal"; +"Share 1-time link with a friend" = "Egyszer használható meghívási hivatkozás megosztása egy baráttal"; /* No comment provided by engineer. */ "Share address" = "Cím megosztása"; @@ -4605,7 +4716,7 @@ "Share from other apps." = "Megosztás más alkalmazásokból."; /* No comment provided by engineer. */ -"Share link" = "Hivatkozás megosztása"; +"Share link" = "Megosztás"; /* No comment provided by engineer. */ "Share profile" = "Profil megosztása"; @@ -4614,10 +4725,10 @@ "Share SimpleX address on social media." = "SimpleX-cím megosztása a közösségi médiában."; /* No comment provided by engineer. */ -"Share this 1-time invite link" = "Egyszer használható meghívó-hivatkozás megosztása"; +"Share this 1-time invite link" = "Ennek az egyszer használható meghívási hivatkozásnak a megosztása"; /* No comment provided by engineer. */ -"Share to SimpleX" = "Megosztás a SimpleX-ben"; +"Share to SimpleX" = "Megosztás a SimpleXben"; /* No comment provided by engineer. */ "Share with contacts" = "Megosztás az ismerősökkel"; @@ -4635,7 +4746,7 @@ "Show last messages" = "Legutóbbi üzenet előnézetének megjelenítése"; /* No comment provided by engineer. */ -"Show message status" = "Üzenetállapot megjelenítése"; +"Show message status" = "Üzenet állapotának megjelenítése"; /* No comment provided by engineer. */ "Show percentage" = "Százalék megjelenítése"; @@ -4659,10 +4770,10 @@ "SimpleX Address" = "SimpleX-cím"; /* No comment provided by engineer. */ -"SimpleX address and 1-time links are safe to share via any messenger." = "A SimpleX-cím és az egyszer használható meghívó-hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül."; +"SimpleX address and 1-time links are safe to share via any messenger." = "A SimpleX-cím és az egyszer használható meghívási hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül."; /* No comment provided by engineer. */ -"SimpleX address or 1-time link?" = "SimpleX-cím vagy egyszer használható meghívó-hivatkozás?"; +"SimpleX address or 1-time link?" = "SimpleX-cím vagy egyszer használható meghívási hivatkozás?"; /* No comment provided by engineer. */ "SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "A SimpleX Chat és a Flux megállapodást kötött arról, hogy a Flux által üzemeltetett kiszolgálókat beépítik az alkalmazásba."; @@ -4683,7 +4794,7 @@ "SimpleX links" = "SimpleX-hivatkozások"; /* No comment provided by engineer. */ -"SimpleX links are prohibited." = "A SimpleX-hivatkozások küldése le van tiltva ebben a csoportban."; +"SimpleX links are prohibited." = "A SimpleX-hivatkozások küldése le van tiltva."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "A SimpleX-hivatkozások küldése le van tiltva"; @@ -4701,7 +4812,7 @@ "SimpleX Lock turned on" = "SimpleX-zár bekapcsolva"; /* simplex link type */ -"SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívó-hivatkozás"; +"SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívási hivatkozás"; /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva."; @@ -4768,7 +4879,7 @@ "Start migration" = "Átköltöztetés indítása"; /* No comment provided by engineer. */ -"Starting from %@." = "Kezdve ettől %@."; +"Starting from %@." = "Statisztikagyűjtés kezdete: %@."; /* No comment provided by engineer. */ "starting…" = "indítás…"; @@ -4837,13 +4948,13 @@ "Switch audio and video during the call." = "Hang/Videó váltása hívás közben."; /* No comment provided by engineer. */ -"Switch chat profile for 1-time invitations." = "Csevegési profilváltás az egyszer használható meghívó-hivatkozásokhoz."; +"Switch chat profile for 1-time invitations." = "Csevegési profilváltás az egyszer használható meghívási hivatkozásokhoz."; /* No comment provided by engineer. */ "System" = "Rendszer"; /* No comment provided by engineer. */ -"System authentication" = "Rendszerhitelesítés"; +"System authentication" = "Rendszer-hitelesítés"; /* No comment provided by engineer. */ "Tail" = "Farok"; @@ -4867,13 +4978,13 @@ "Tap to join" = "Koppintson ide a csatlakozáshoz"; /* No comment provided by engineer. */ -"Tap to join incognito" = "Koppintson ide az inkognitóban való csatlakozáshoz"; +"Tap to join incognito" = "Koppintson ide az inkognitóban való kapcsolódáshoz"; /* No comment provided by engineer. */ "Tap to paste link" = "Koppintson ide a hivatkozás beillesztéséhez"; /* No comment provided by engineer. */ -"Tap to scan" = "Koppintson ide a QR-kód beolvasáshoz"; +"Tap to scan" = "Koppintson ide a QR-kód beolvasásához"; /* No comment provided by engineer. */ "TCP connection" = "TCP kapcsolat"; @@ -4881,6 +4992,9 @@ /* No comment provided by engineer. */ "TCP connection timeout" = "TCP kapcsolat időtúllépése"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "TCP-port az üzenetváltáshoz"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -4891,10 +5005,13 @@ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; /* file error alert title */ -"Temporary file error" = "Ideiglenesfájl-hiba"; +"Temporary file error" = "Ideiglenes fájlhiba"; /* server test failure */ -"Test failed at step %@." = "A teszt sikertelen volt a(z) %@ lépésnél."; +"Test failed at step %@." = "A teszt a(z) %@ lépésnél sikertelen volt."; + +/* No comment provided by engineer. */ +"Test notifications" = "Értesítések tesztelése"; /* No comment provided by engineer. */ "Test server" = "Kiszolgáló tesztelése"; @@ -4918,13 +5035,13 @@ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatkéréseket kap – beállítások megnyitása az engedélyezéshez."; /* No comment provided by engineer. */ -"The app protects your privacy by using different operators in each conversation." = "Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetésben más-más üzemeltetőket használ."; +"The app protects your privacy by using different operators in each conversation." = "Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ."; /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról (kivéve .onion) történő letöltések megerősítését."; /* No comment provided by engineer. */ -"The attempt to change database passphrase was not completed." = "Az adatbázis jelmondatának megváltoztatására tett kísérlet nem fejeződött be."; +"The attempt to change database passphrase was not completed." = "Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be."; /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás."; @@ -4936,7 +5053,7 @@ "The connection you accepted will be cancelled!" = "Az Ön által elfogadott kérelem vissza lesz vonva!"; /* No comment provided by engineer. */ -"The contact you shared this link with will NOT be able to connect!" = "Ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni!"; +"The contact you shared this link with will NOT be able to connect!" = "Az ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni!"; /* No comment provided by engineer. */ "The created archive is available via app Settings / Database / Old database archive." = "A létrehozott archívum a „Beállítások / Adatbázis / Régi adatbázis-archívum” menüben érhető el."; @@ -4972,13 +5089,10 @@ "The profile is only shared with your contacts." = "A profilja csak az ismerőseivel van megosztva."; /* No comment provided by engineer. */ -"The report will be archived for you." = "A jelentés archiválva lesz az Ön számára."; +"The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára is: **%@**."; /* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltetőre is: **%@**."; - -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**."; +"The same conditions will apply to operator(s): **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k) számára is: **%@**."; /* No comment provided by engineer. */ "The second preset operator in the app!" = "A második előre beállított üzemeltető az alkalmazásban!"; @@ -5029,7 +5143,7 @@ "This chat is protected by end-to-end encryption." = "Ez a csevegés végpontok közötti titkosítással védett."; /* E2EE info chat item */ -"This chat is protected by quantum resistant end-to-end encryption." = "Ez a csevegés végpontok közötti kvantumálló tikosítással védett."; +"This chat is protected by quantum resistant end-to-end encryption." = "Ez a csevegés végpontok közötti kvantumálló titkosítással védett."; /* notification title */ "this contact" = "ez az ismerős"; @@ -5047,7 +5161,7 @@ "This group no longer exists." = "Ez a csoport már nem létezik."; /* No comment provided by engineer. */ -"This is your own one-time link!" = "Ez az Ön egyszer használható meghívó-hivatkozása!"; +"This is your own one-time link!" = "Ez az Ön egyszer használható meghívási hivatkozása!"; /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Ez az Ön SimpleX-címe!"; @@ -5086,7 +5200,7 @@ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Az IP-cím védelmének érdekében a privát útválasztás az SMP-kiszolgálókat használja az üzenetek kézbesítéséhez."; /* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Az adatvédelem érdekében (a más csevegési platformokon megszokott felhasználó-azonosítók helyett) a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, az összes ismerőséhez különbözőt."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Az Ön adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához."; /* No comment provided by engineer. */ "To receive" = "A fogadáshoz"; @@ -5101,7 +5215,7 @@ "To record voice message please grant permission to use Microphone." = "Hangüzenet rögzítéséhez adjon engedélyt a mikrofon használathoz."; /* No comment provided by engineer. */ -"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Rejtett profilja megjelenítéséhez adja meg a teljes jelszót a keresőmezőben, a **Csevegési profilok** menüben."; +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Rejtett profilja felfedéséhez adja meg a teljes jelszót a keresőmezőben, a **Csevegési profilok** menüben."; /* No comment provided by engineer. */ "To send" = "A küldéshez"; @@ -5121,6 +5235,9 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Inkognitóra váltás kapcsolódáskor."; +/* token status */ +"Token status: %@." = "Token állapota: %@."; + /* No comment provided by engineer. */ "Toolbar opacity" = "Eszköztár átlátszatlansága"; @@ -5128,7 +5245,7 @@ "Total" = "Összes kapcsolat"; /* No comment provided by engineer. */ -"Transport isolation" = "Átvitel-izoláció módja"; +"Transport isolation" = "Átvitel-izoláció"; /* No comment provided by engineer. */ "Transport sessions" = "Munkamenetek átvitele"; @@ -5155,7 +5272,7 @@ "Unblock" = "Feloldás"; /* No comment provided by engineer. */ -"Unblock for all" = "Letiltás feloldása az összes tag számára"; +"Unblock for all" = "Feloldás"; /* No comment provided by engineer. */ "Unblock member" = "Tag feloldása"; @@ -5221,7 +5338,7 @@ "Unlink" = "Szétkapcsolás"; /* No comment provided by engineer. */ -"Unlink desktop?" = "Számítógép szétkapcsolása?"; +"Unlink desktop?" = "Számítógép leválasztása?"; /* No comment provided by engineer. */ "Unlock" = "Feloldás"; @@ -5245,10 +5362,10 @@ "Update" = "Frissítés"; /* No comment provided by engineer. */ -"Update database passphrase" = "Az adatbázis jelmondatának megváltoztatása"; +"Update database passphrase" = "Az adatbázis jelmondatának módosítása"; /* No comment provided by engineer. */ -"Update network settings?" = "Hálózati beállítások megváltoztatása?"; +"Update network settings?" = "Hálózati beállítások módosítása?"; /* No comment provided by engineer. */ "Update settings?" = "Beállítások frissítése?"; @@ -5290,7 +5407,7 @@ "Use %@" = "%@ használata"; /* No comment provided by engineer. */ -"Use chat" = "Simplex Chat használata"; +"Use chat" = "SimpleX Chat használata"; /* No comment provided by engineer. */ "Use current profile" = "Jelenlegi profil használata"; @@ -5334,12 +5451,18 @@ /* No comment provided by engineer. */ "Use SOCKS proxy" = "SOCKS-proxy használata"; +/* No comment provided by engineer. */ +"Use TCP port %@ when no port is specified." = "A következő TCP-port használata, amikor nincs port megadva: %@."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Használja az alkalmazást hívás közben."; /* No comment provided by engineer. */ "Use the app with one hand." = "Használja az alkalmazást egy kézzel."; +/* No comment provided by engineer. */ +"Use web port" = "Webport használata"; + /* No comment provided by engineer. */ "User selection" = "Felhasználó kijelölése"; @@ -5386,7 +5509,7 @@ "via group link" = "a csoporthivatkozáson keresztül"; /* chat list item description */ -"via one-time link" = "egy egyszer használható meghívó-hivatkozáson keresztül"; +"via one-time link" = "egy egyszer használható meghívási hivatkozáson keresztül"; /* No comment provided by engineer. */ "via relay" = "egy továbbító-kiszolgálón keresztül"; @@ -5434,13 +5557,13 @@ "Voice messages are prohibited in this chat." = "A hangüzenetek küldése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Voice messages are prohibited." = "A hangüzenetek küldése le van tiltva ebben a csoportban."; +"Voice messages are prohibited." = "A hangüzenetek küldése le van tiltva."; /* No comment provided by engineer. */ "Voice messages not allowed" = "A hangüzenetek küldése le van tiltva"; /* No comment provided by engineer. */ -"Voice messages prohibited!" = "A hangüzenetek le vannak tilva!"; +"Voice messages prohibited!" = "A hangüzenetek le vannak tiltva!"; /* No comment provided by engineer. */ "waiting for answer…" = "várakozás a válaszra…"; @@ -5470,7 +5593,7 @@ "wants to connect to you!" = "kapcsolatba akar lépni Önnel!"; /* No comment provided by engineer. */ -"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, továbbá üzenetkézbesítési hibákat okozhat"; +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, mert üzenetkézbesítési hibákat okoz"; /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Figyelmeztetés: néhány adat elveszhet!"; @@ -5482,7 +5605,7 @@ "weeks" = "hét"; /* No comment provided by engineer. */ -"Welcome %@!" = "Üdvözöllek %@!"; +"Welcome %@!" = "Üdvözöljük %@!"; /* No comment provided by engineer. */ "Welcome message" = "Üdvözlőüzenet"; @@ -5503,7 +5626,7 @@ "when IP hidden" = "ha az IP-cím rejtett"; /* No comment provided by engineer. */ -"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Amikor egynél több hálózati üzemeltető van engedélyezve, egyikük sem rendelkezik olyan metaadatokkal ahhoz, hogy felderítse, ki kommunikál kivel."; +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Amikor egynél több üzemeltető van engedélyezve, akkor egyik sem rendelkezik olyan metaadatokkal, amelyekből megtudható, hogy ki kivel kommunikál."; /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott."; @@ -5569,16 +5692,16 @@ "You already have a chat profile with the same display name. Please choose another name." = "Már van egy csevegési profil ugyanezzel a megjelenített névvel. Válasszon egy másik nevet."; /* No comment provided by engineer. */ -"You are already connected to %@." = "Ön már kapcsolódva van a következőhöz: %@."; +"You are already connected to %@." = "Ön már kapcsolódott a következőhöz: %@."; /* No comment provided by engineer. */ "You are already connected with %@." = "Ön már kapcsolódva van vele: %@."; /* No comment provided by engineer. */ -"You are already connecting to %@." = "Már folyamatban van a kapcsolódás a következőhöz: %@."; +"You are already connecting to %@." = "A kapcsolódás már folyamatban van a következőhöz: %@."; /* No comment provided by engineer. */ -"You are already connecting via this one-time link!" = "A kapcsolódás már folyamatban van ezen az egyszer használható meghívó-hivatkozáson keresztül!"; +"You are already connecting via this one-time link!" = "A kapcsolódás már folyamatban van ezen az egyszer használható meghívási hivatkozáson keresztül!"; /* No comment provided by engineer. */ "You are already in group %@." = "Ön már a(z) %@ nevű csoport tagja."; @@ -5593,16 +5716,16 @@ "You are already joining the group via this link." = "A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül."; /* No comment provided by engineer. */ -"You are already joining the group!\nRepeat join request?" = "Csatlakozás folyamatban!\nCsatlakozáskérés megismétlése?"; +"You are already joining the group!\nRepeat join request?" = "A csatlakozás már folyamatban van a csoporthoz!\nCsatlakozáskérés megismétlése?"; /* No comment provided by engineer. */ "You are connected to the server used to receive messages from this contact." = "Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál."; /* No comment provided by engineer. */ -"you are invited to group" = "meghívást kapott a csoportba"; +"you are invited to group" = "Ön meghívást kapott a csoportba"; /* No comment provided by engineer. */ -"You are invited to group" = "Meghívást kapott a csoportba"; +"You are invited to group" = "Ön meghívást kapott a csoportba"; /* No comment provided by engineer. */ "You are not connected to these servers. Private routing is used to deliver messages to them." = "Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál."; @@ -5617,10 +5740,10 @@ "You can accept calls from lock screen, without device and app authentication." = "Hívásokat fogadhat a lezárási képernyőről, eszköz- és alkalmazás-hitelesítés nélkül."; /* No comment provided by engineer. */ -"You can change it in Appearance settings." = "Ezt a „Megjelenés” menüben változtathatja meg."; +"You can change it in Appearance settings." = "Ezt a „Megjelenés” menüben módosíthatja."; /* No comment provided by engineer. */ -"You can configure operators in Network & servers settings." = "Az üzemeltetőket a „Hálózat és kiszolgálók” beállításaban konfigurálhatja."; +"You can configure operators in Network & servers settings." = "Az üzemeltetőket a „Hálózat és kiszolgálók” menüben konfigurálhatja."; /* No comment provided by engineer. */ "You can configure servers via settings." = "A kiszolgálókat a „Hálózat és kiszolgálók” menüben konfigurálhatja."; @@ -5632,7 +5755,7 @@ "You can enable later via Settings" = "Később engedélyezheti a „Beállításokban”"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Később engedélyezheti őket az alkalmazás „Adatvédelem és biztonság” menüjében."; +"You can enable them later via app Privacy & Security settings." = "Később engedélyezheti őket az „Adatvédelem és biztonság” menüben."; /* No comment provided by engineer. */ "You can give another try." = "Megpróbálhatja még egyszer."; @@ -5659,7 +5782,7 @@ "You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Megoszthat egy hivatkozást vagy QR-kódot – így bárki csatlakozhat a csoporthoz. Ha a csoportot Ön később törli, akkor nem fogja elveszíteni annak tagjait."; /* No comment provided by engineer. */ -"You can share this address with your contacts to let them connect with **%@**." = "Megoszthatja ezt a címet az ismerőseivel, hogy kapcsolatba léphessenek Önnel a(z) **%@** nevű profilján keresztül."; +"You can share this address with your contacts to let them connect with **%@**." = "Megoszthatja ezt a SimpleX-címet az ismerőseivel, hogy kapcsolatba léphessenek vele: **%@**."; /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "A csevegést az alkalmazás „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával indíthatja el"; @@ -5674,22 +5797,22 @@ "You can use markdown to format messages:" = "Üzenetek formázása a szövegbe szúrt speciális karakterekkel:"; /* alert message */ -"You can view invitation link again in connection details." = "A meghívó-hivatkozást újra megtekintheti a kapcsolat részleteinél."; +"You can view invitation link again in connection details." = "A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél."; /* No comment provided by engineer. */ "You can't send messages!" = "Nem lehet üzeneteket küldeni!"; /* chat item text */ -"you changed address" = "Ön megváltoztatta a címet"; +"you changed address" = "Ön módosította a címet"; /* chat item text */ -"you changed address for %@" = "Ön megváltoztatta a címet %@ számára"; +"you changed address for %@" = "Ön módosította a címet %@ számára"; /* snd group event chat item */ -"you changed role for yourself to %@" = "Ön megváltoztatta a saját szerepkörét a következőre: %@"; +"you changed role for yourself to %@" = "Ön a következőre módosította a saját szerepkörét: „%@”"; /* snd group event chat item */ -"you changed role of %@ to %@" = "Ön megváltoztatta %1$@ szerepkörét a következőre: %@"; +"you changed role of %@ to %@" = "Ön a következőre módosította %1$@ szerepkörét: „%2$@”"; /* No comment provided by engineer. */ "You could not be verified; please try again." = "Nem sikerült hitelesíteni; próbálja meg újra."; @@ -5707,7 +5830,7 @@ "You have to enter passphrase every time the app starts - it is not stored on the device." = "A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul – nem az eszközön van tárolva."; /* No comment provided by engineer. */ -"You invited a contact" = "Meghívta egy ismerősét"; +"You invited a contact" = "Ön meghívta egy ismerősét"; /* No comment provided by engineer. */ "You joined this group" = "Ön csatlakozott ehhez a csoporthoz"; @@ -5743,10 +5866,13 @@ "You sent group invitation" = "Csoportmeghívó elküldve"; /* chat list item description */ -"you shared one-time link" = "Ön egy egyszer használható meghívó-hivatkozást osztott meg"; +"you shared one-time link" = "Ön egy egyszer használható meghívási hivatkozást osztott meg"; /* chat list item description */ -"you shared one-time link incognito" = "Ön egy egyszer használható meghívó-hivatkozást osztott meg inkognitóban"; +"you shared one-time link incognito" = "Ön egy egyszer használható meghívási hivatkozást osztott meg inkognitóban"; + +/* token info */ +"You should receive notifications." = "Ön megkapja az értesítéseket."; /* snd group event chat item */ "you unblocked %@" = "Ön feloldotta %@ letiltását"; @@ -5764,7 +5890,7 @@ "You will be connected when your contact's device is online, please wait or check later!" = "Akkor lesz kapcsolódva, amikor az ismerősének eszköze online lesz, várjon, vagy ellenőrizze később!"; /* No comment provided by engineer. */ -"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Az alkalmazás indításakor, vagy 30 másodpercnyi háttérben töltött idő után az alkalmazáshoz visszatérve hitelesítés szükséges."; +"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Az alkalmazás elindításához vagy 30 másodpercnyi háttérben töltött idő után, az alkalmazáshoz való visszatéréshez hitelesítésre lesz szükség."; /* No comment provided by engineer. */ "You will connect to all group members." = "Kapcsolódni fog a csoport összes tagjához."; @@ -5800,7 +5926,7 @@ "Your chat database is not encrypted - set passphrase to encrypt it." = "A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz."; /* alert title */ -"Your chat preferences" = "Csevegési beállítások"; +"Your chat preferences" = "Az Ön csevegés-beállításai"; /* No comment provided by engineer. */ "Your chat profiles" = "Csevegési profilok"; @@ -5809,7 +5935,7 @@ "Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "A kapcsolata át lett helyezve ide: %@, de egy váratlan hiba történt a profilra való átirányításkor."; /* No comment provided by engineer. */ -"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Az ismerőse olyan fájlt küldött, amely meghaladja a jelenleg támogatott maximális méretet (%@)."; +"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Az ismerőse a jelenleg megengedett maximális méretű (%@) fájlnál nagyobbat küldött."; /* No comment provided by engineer. */ "Your contacts can allow full message deletion." = "Az ismerősei engedélyezhetik a teljes üzenet törlést."; @@ -5845,7 +5971,7 @@ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "A profilja az eszközén van tárolva és csak az ismerőseivel van megosztva. A SimpleX-kiszolgálók nem láthatják a profilját."; /* alert message */ -"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "A profilja megváltozott. Ha elmenti, a profilfrissítés el lesz küldve az Ön ismerősei számára."; +"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "A profilja módosult. Ha elmenti, a profilfrissítés el lesz küldve az Ön ismerősei számára."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "A profilja, az ismerősei és az elküldött üzenetei az Ön eszközén vannak tárolva."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index b016babfc1..98854fefac 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -58,6 +58,12 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Dai una stella su GitHub](https://github.com/simplex-chat/simplex-chat)"; +/* No comment provided by engineer. */ +"@'%@'" = "@'%@'"; + +/* No comment provided by engineer. */ +"@%@" = "@%@"; + /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Aggiungi contatto**: per creare un nuovo link di invito."; @@ -397,6 +403,9 @@ /* No comment provided by engineer. */ "Acknowledgement errors" = "Errori di riconoscimento"; +/* token status text */ +"Active" = "Attivo"; + /* No comment provided by engineer. */ "Active connections" = "Connessioni attive"; @@ -523,6 +532,9 @@ /* profile dropdown */ "All profiles" = "Tutti gli profili"; +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Tutte le segnalazioni verranno archiviate per te."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Tutti i tuoi contatti resteranno connessi."; @@ -568,6 +580,9 @@ /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Permetti di eliminare irreversibilmente i messaggi inviati. (24 ore)"; +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Consenti di segnalare messaggi ai moderatori."; + /* No comment provided by engineer. */ "Allow to send files and media." = "Consenti l'invio di file e contenuti multimediali."; @@ -673,6 +688,12 @@ /* No comment provided by engineer. */ "Archive" = "Archivia"; +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Archiviare %lld segnalazioni?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Archiviare tutte le segnalazioni?"; + /* No comment provided by engineer. */ "Archive and upload" = "Archivia e carica"; @@ -685,6 +706,9 @@ /* No comment provided by engineer. */ "Archive report?" = "Archiviare la segnalazione?"; +/* swipe action */ +"Archive reports" = "Archivia segnalazioni"; + /* No comment provided by engineer. */ "Archived contacts" = "Contatti archiviati"; @@ -1080,6 +1104,9 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Svuotare la conversazione?"; +/* No comment provided by engineer. */ +"Clear group?" = "Svuotare il gruppo?"; + /* No comment provided by engineer. */ "Clear or delete group?" = "Svuotare o eliminare il gruppo?"; @@ -1173,6 +1200,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Conferma caricamento"; +/* token status text */ +"Confirmed" = "Confermato"; + /* server test step */ "Connect" = "Connetti"; @@ -2127,6 +2157,9 @@ /* No comment provided by engineer. */ "Error changing to incognito!" = "Errore nel passaggio a incognito!"; +/* No comment provided by engineer. */ +"Error checking token status" = "Errore di controllo dello stato del token"; + /* No comment provided by engineer. */ "Error connecting to forwarding server %@. Please try later." = "Errore di connessione al server di inoltro %@. Riprova più tardi."; @@ -2220,6 +2253,9 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Errore di riconnessione ai server"; +/* alert title */ +"Error registering for notifications" = "Errore di registrazione per le notifiche"; + /* No comment provided by engineer. */ "Error removing member" = "Errore nella rimozione del membro"; @@ -2283,6 +2319,9 @@ /* No comment provided by engineer. */ "Error synchronizing connection" = "Errore nella sincronizzazione della connessione"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Errore provando la connessione al server"; + /* No comment provided by engineer. */ "Error updating group link" = "Errore nell'aggiornamento del link del gruppo"; @@ -2337,6 +2376,9 @@ /* No comment provided by engineer. */ "expired" = "scaduto"; +/* token status text */ +"Expired" = "Scaduto"; + /* No comment provided by engineer. */ "Export database" = "Esporta database"; @@ -2454,6 +2496,9 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Correzione non supportata dal membro del gruppo"; +/* No comment provided by engineer. */ +"For all moderators" = "Per tutti i moderatori"; + /* No comment provided by engineer. */ "for better metadata privacy." = "per una migliore privacy dei metadati."; @@ -2466,6 +2511,9 @@ /* No comment provided by engineer. */ "For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Ad esempio, se il tuo contatto riceve messaggi tramite un server di SimpleX Chat, la tua app li consegnerà tramite un server Flux."; +/* No comment provided by engineer. */ +"For me" = "Per me"; + /* No comment provided by engineer. */ "For private routing" = "Per l'instradamento privato"; @@ -2814,6 +2862,21 @@ /* No comment provided by engineer. */ "Interface colors" = "Colori dell'interfaccia"; +/* token status text */ +"Invalid" = "Non valido"; + +/* token status text */ +"Invalid (bad token)" = "Non valido (token corrotto)"; + +/* token status text */ +"Invalid (expired)" = "Non valido (scaduto)"; + +/* token status text */ +"Invalid (unregistered)" = "Non valido (non registrato)"; + +/* token status text */ +"Invalid (wrong topic)" = "Non valido (argomento sbagliato)"; + /* invalid chat data */ "invalid chat" = "chat non valida"; @@ -3093,6 +3156,9 @@ /* item status text */ "Member inactive" = "Membro inattivo"; +/* chat feature */ +"Member reports" = "Segnalazioni dei membri"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All chat members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Verranno notificati tutti i membri della chat."; @@ -3114,6 +3180,9 @@ /* No comment provided by engineer. */ "Members can irreversibly delete sent messages. (24 hours)" = "I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore)"; +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "I membri possono segnalare messaggi ai moderatori."; + /* No comment provided by engineer. */ "Members can send direct messages." = "I membri del gruppo possono inviare messaggi diretti."; @@ -3303,6 +3372,9 @@ /* notification label action */ "Mute" = "Silenzia"; +/* notification label action */ +"Mute all" = "Silenzia tutto"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Silenzioso quando inattivo!"; @@ -3336,6 +3408,9 @@ /* delete after time */ "never" = "mai"; +/* token status text */ +"New" = "Nuovo"; + /* No comment provided by engineer. */ "New chat" = "Nuova chat"; @@ -3474,6 +3549,9 @@ /* copied message info in history */ "no text" = "nessun testo"; +/* alert title */ +"No token!" = "Nessun token!"; + /* No comment provided by engineer. */ "No unread chats" = "Nessuna chat non letta"; @@ -3498,9 +3576,15 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Le notifiche sono disattivate!"; +/* alert title */ +"Notifications error" = "Errore delle notifiche"; + /* No comment provided by engineer. */ "Notifications privacy" = "Privacy delle notifiche"; +/* alert title */ +"Notifications status" = "Stato delle notifiche"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Ora gli amministratori possono:\n- eliminare i messaggi dei membri.\n- disattivare i membri (ruolo \"osservatore\")"; @@ -3785,6 +3869,15 @@ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Conserva la password in modo sicuro, NON potrai cambiarla se la perdi."; +/* token info */ +"Please try to disable and re-enable notfications." = "Prova a disattivare e riattivare le notifiche."; + +/* token info */ +"Please wait for token activation to complete." = "Attendi il completamento dell'attivazione del token."; + +/* token info */ +"Please wait for token to be registered." = "Attendi la registrazione del token."; + /* No comment provided by engineer. */ "Polish interface" = "Interfaccia polacca"; @@ -3866,6 +3959,9 @@ /* No comment provided by engineer. */ "Prohibit messages reactions." = "Proibisci le reazioni ai messaggi."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Vieta di segnalare messaggi ai moderatori."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Proibisci l'invio di messaggi diretti ai membri."; @@ -4028,6 +4124,15 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Consumo di batteria ridotto"; +/* No comment provided by engineer. */ +"Register" = "Registra"; + +/* token info */ +"Register notification token?" = "Registrare il token di notifica?"; + +/* token status text */ +"Registered" = "Registrato"; + /* reject incoming call via notification swipe action */ "Reject" = "Rifiuta"; @@ -4128,6 +4233,12 @@ /* report reason */ "Report violation: only group moderators will see it." = "Segnala violazione: solo i moderatori del gruppo lo vedranno."; +/* report in notification */ +"Report: (text.isEmpty ? reason.text : text)" = "Segnalazione: (text.isEmpty ? reason.text : text)"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "È vietato segnalare messaggi ai moderatori."; + /* No comment provided by engineer. */ "Reports" = "Segnalazioni"; @@ -4881,6 +4992,9 @@ /* No comment provided by engineer. */ "TCP connection timeout" = "Scadenza connessione TCP"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "Porta TCP per i messaggi"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -4896,6 +5010,9 @@ /* server test failure */ "Test failed at step %@." = "Test fallito al passo %@."; +/* No comment provided by engineer. */ +"Test notifications" = "Prova le notifiche"; + /* No comment provided by engineer. */ "Test server" = "Prova server"; @@ -4971,9 +5088,6 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Il profilo è condiviso solo con i tuoi contatti."; -/* No comment provided by engineer. */ -"The report will be archived for you." = "La segnalazione verrà archiviata per te."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Le stesse condizioni si applicheranno all'operatore **%@**."; @@ -5121,6 +5235,9 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Attiva/disattiva l'incognito quando ti colleghi."; +/* token status */ +"Token status: %@." = "Stato del token: %@."; + /* No comment provided by engineer. */ "Toolbar opacity" = "Opacità barra degli strumenti"; @@ -5334,12 +5451,18 @@ /* No comment provided by engineer. */ "Use SOCKS proxy" = "Usa proxy SOCKS"; +/* No comment provided by engineer. */ +"Use TCP port %@ when no port is specified." = "Usa la porta TCP %@ quando non è specificata alcuna porta."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Usa l'app mentre sei in chiamata."; /* No comment provided by engineer. */ "Use the app with one hand." = "Usa l'app con una mano sola."; +/* No comment provided by engineer. */ +"Use web port" = "Usa porta web"; + /* No comment provided by engineer. */ "User selection" = "Selezione utente"; @@ -5748,6 +5871,9 @@ /* chat list item description */ "you shared one-time link incognito" = "hai condiviso un link incognito una tantum"; +/* token info */ +"You should receive notifications." = "Dovresti ricevere le notifiche."; + /* snd group event chat item */ "you unblocked %@" = "hai sbloccato %@"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 0f03f40496..587074589c 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -58,6 +58,12 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)"; +/* No comment provided by engineer. */ +"@'%@'" = "@'%@'"; + +/* No comment provided by engineer. */ +"@%@" = "@%@"; + /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Contact toevoegen**: om een nieuwe uitnodigingslink aan te maken, of verbinding te maken via een link die u heeft ontvangen."; @@ -397,6 +403,9 @@ /* No comment provided by engineer. */ "Acknowledgement errors" = "Bevestigingsfouten"; +/* token status text */ +"Active" = "actief"; + /* No comment provided by engineer. */ "Active connections" = "Actieve verbindingen"; @@ -523,6 +532,9 @@ /* profile dropdown */ "All profiles" = "Alle profielen"; +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Alle rapporten worden voor u gearchiveerd."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Al uw contacten blijven verbonden."; @@ -568,6 +580,9 @@ /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Sta toe om verzonden berichten definitief te verwijderen. (24 uur)"; +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Hiermee kunt u berichten rapporteren aan moderators."; + /* No comment provided by engineer. */ "Allow to send files and media." = "Sta toe om bestanden en media te verzenden."; @@ -673,6 +688,12 @@ /* No comment provided by engineer. */ "Archive" = "Archief"; +/* No comment provided by engineer. */ +"Archive %lld reports?" = "%lld rapporten archiveren?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Alle rapporten archiveren?"; + /* No comment provided by engineer. */ "Archive and upload" = "Archiveren en uploaden"; @@ -685,6 +706,9 @@ /* No comment provided by engineer. */ "Archive report?" = "Rapport archiveren?"; +/* swipe action */ +"Archive reports" = "Rapporten archiveren"; + /* No comment provided by engineer. */ "Archived contacts" = "Gearchiveerde contacten"; @@ -1080,6 +1104,9 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Gesprek wissen?"; +/* No comment provided by engineer. */ +"Clear group?" = "Groep wissen?"; + /* No comment provided by engineer. */ "Clear or delete group?" = "Groep wissen of verwijderen?"; @@ -1173,6 +1200,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Bevestig het uploaden"; +/* token status text */ +"Confirmed" = "Bevestigd"; + /* server test step */ "Connect" = "Verbind"; @@ -2127,6 +2157,9 @@ /* No comment provided by engineer. */ "Error changing to incognito!" = "Fout bij het overschakelen naar incognito!"; +/* No comment provided by engineer. */ +"Error checking token status" = "Fout bij het controleren van de tokenstatus"; + /* No comment provided by engineer. */ "Error connecting to forwarding server %@. Please try later." = "Fout bij het verbinden met doorstuurserver %@. Probeer het later opnieuw."; @@ -2220,6 +2253,9 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Fout bij opnieuw verbinden van servers"; +/* alert title */ +"Error registering for notifications" = "Fout bij registreren voor meldingen"; + /* No comment provided by engineer. */ "Error removing member" = "Fout bij verwijderen van lid"; @@ -2283,6 +2319,9 @@ /* No comment provided by engineer. */ "Error synchronizing connection" = "Fout bij het synchroniseren van de verbinding"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Fout bij het testen van de serververbinding"; + /* No comment provided by engineer. */ "Error updating group link" = "Fout bij bijwerken van groep link"; @@ -2337,6 +2376,9 @@ /* No comment provided by engineer. */ "expired" = "verlopen"; +/* token status text */ +"Expired" = "Verlopen"; + /* No comment provided by engineer. */ "Export database" = "Database exporteren"; @@ -2454,6 +2496,9 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Herstel wordt niet ondersteund door groepslid"; +/* No comment provided by engineer. */ +"For all moderators" = "Voor alle moderators"; + /* No comment provided by engineer. */ "for better metadata privacy." = "voor betere privacy van metagegevens."; @@ -2466,6 +2511,9 @@ /* No comment provided by engineer. */ "For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Als uw contactpersoon bijvoorbeeld berichten ontvangt via een SimpleX Chat-server, worden deze door uw app via een Flux-server verzonden."; +/* No comment provided by engineer. */ +"For me" = "Voor mij"; + /* No comment provided by engineer. */ "For private routing" = "Voor privé-routering"; @@ -2814,6 +2862,21 @@ /* No comment provided by engineer. */ "Interface colors" = "Interface kleuren"; +/* token status text */ +"Invalid" = "Ongeldig"; + +/* token status text */ +"Invalid (bad token)" = "Ongeldig (ongeldig token)"; + +/* token status text */ +"Invalid (expired)" = "Ongeldig (verlopen)"; + +/* token status text */ +"Invalid (unregistered)" = "Ongeldig (niet geregistreerd)"; + +/* token status text */ +"Invalid (wrong topic)" = "Ongeldig (verkeerd onderwerp)"; + /* invalid chat data */ "invalid chat" = "ongeldige gesprek"; @@ -3093,6 +3156,9 @@ /* item status text */ "Member inactive" = "Lid inactief"; +/* chat feature */ +"Member reports" = "Ledenrapporten"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All chat members will be notified." = "De rol van het lid wordt gewijzigd naar \"%@\". Alle chatleden worden op de hoogte gebracht."; @@ -3114,6 +3180,9 @@ /* No comment provided by engineer. */ "Members can irreversibly delete sent messages. (24 hours)" = "Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "Leden kunnen berichten melden bij moderators."; + /* No comment provided by engineer. */ "Members can send direct messages." = "Groepsleden kunnen directe berichten sturen."; @@ -3303,6 +3372,9 @@ /* notification label action */ "Mute" = "Dempen"; +/* notification label action */ +"Mute all" = "Alles dempen"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Gedempt wanneer inactief!"; @@ -3336,6 +3408,9 @@ /* delete after time */ "never" = "nooit"; +/* token status text */ +"New" = "Nieuw"; + /* No comment provided by engineer. */ "New chat" = "Nieuw gesprek"; @@ -3474,6 +3549,9 @@ /* copied message info in history */ "no text" = "geen tekst"; +/* alert title */ +"No token!" = "Geen token!"; + /* No comment provided by engineer. */ "No unread chats" = "Geen ongelezen chats"; @@ -3498,9 +3576,15 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Meldingen zijn uitgeschakeld!"; +/* alert title */ +"Notifications error" = "Meldingsfout"; + /* No comment provided by engineer. */ "Notifications privacy" = "Privacy van meldingen"; +/* alert title */ +"Notifications status" = "Meldingsstatus"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Nu kunnen beheerders: \n- berichten van leden verwijderen.\n- schakel leden uit (\"waarnemer\" rol)"; @@ -3785,6 +3869,15 @@ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Bewaar het wachtwoord veilig, u kunt deze NIET wijzigen als u het kwijtraakt."; +/* token info */ +"Please try to disable and re-enable notfications." = "Probeer meldingen uit en weer in te schakelen."; + +/* token info */ +"Please wait for token activation to complete." = "Wacht tot de tokenactivering voltooid is."; + +/* token info */ +"Please wait for token to be registered." = "Wacht tot het token is geregistreerd."; + /* No comment provided by engineer. */ "Polish interface" = "Poolse interface"; @@ -3866,6 +3959,9 @@ /* No comment provided by engineer. */ "Prohibit messages reactions." = "Berichten reacties verbieden."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Het melden van berichten aan moderators is niet toegestaan."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Verbied het sturen van directe berichten naar leden."; @@ -4028,6 +4124,15 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Verminderd batterijgebruik"; +/* No comment provided by engineer. */ +"Register" = "Register"; + +/* token info */ +"Register notification token?" = "Meldingstoken registreren?"; + +/* token status text */ +"Registered" = "Geregistreerd"; + /* reject incoming call via notification swipe action */ "Reject" = "Afwijzen"; @@ -4128,6 +4233,12 @@ /* report reason */ "Report violation: only group moderators will see it." = "Rapporteer overtreding: alleen groepsmoderators kunnen dit zien."; +/* report in notification */ +"Report: (text.isEmpty ? reason.text : text)" = "Rapport: (text.isEmpty ? reason.text : text)"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Het is niet toegestaan om berichten aan moderators te melden."; + /* No comment provided by engineer. */ "Reports" = "Rapporten"; @@ -4556,7 +4667,7 @@ "set new contact address" = "nieuw contactadres instellen"; /* profile update event chat item */ -"set new profile picture" = "nieuwe profielfoto instellen"; +"set new profile picture" = "nieuwe profielfoto"; /* No comment provided by engineer. */ "Set passcode" = "Toegangscode instellen"; @@ -4881,6 +4992,9 @@ /* No comment provided by engineer. */ "TCP connection timeout" = "Timeout van TCP-verbinding"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "TCP-poort voor berichtenuitwisseling"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -4896,6 +5010,9 @@ /* server test failure */ "Test failed at step %@." = "Test mislukt bij stap %@."; +/* No comment provided by engineer. */ +"Test notifications" = "Testmeldingen"; + /* No comment provided by engineer. */ "Test server" = "Server test"; @@ -4971,9 +5088,6 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Het profiel wordt alleen gedeeld met uw contacten."; -/* No comment provided by engineer. */ -"The report will be archived for you." = "Het rapport wordt voor u gearchiveerd."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Dezelfde voorwaarden gelden voor operator **%@**."; @@ -5121,6 +5235,9 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Schakel incognito in tijdens het verbinden."; +/* token status */ +"Token status: %@." = "Tokenstatus: %@."; + /* No comment provided by engineer. */ "Toolbar opacity" = "De transparantie van de werkbalk"; @@ -5334,12 +5451,18 @@ /* No comment provided by engineer. */ "Use SOCKS proxy" = "Gebruik SOCKS proxy"; +/* No comment provided by engineer. */ +"Use TCP port %@ when no port is specified." = "Gebruik TCP-poort %@ als er geen poort is opgegeven."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Gebruik de app tijdens het gesprek."; /* No comment provided by engineer. */ "Use the app with one hand." = "Gebruik de app met één hand."; +/* No comment provided by engineer. */ +"Use web port" = "Gebruik een webpoort"; + /* No comment provided by engineer. */ "User selection" = "Gebruikersselectie"; @@ -5748,6 +5871,9 @@ /* chat list item description */ "you shared one-time link incognito" = "je hebt een eenmalige link incognito gedeeld"; +/* token info */ +"You should receive notifications." = "U zou meldingen moeten ontvangen."; + /* snd group event chat item */ "you unblocked %@" = "je hebt %@ gedeblokkeerd"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 592194f19a..4c88e3dcaf 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -55,6 +55,12 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Daj gwiazdkę na GitHub](https://github.com/simplex-chat/simplex-chat)"; +/* No comment provided by engineer. */ +"@'%@'" = "@'%@'"; + +/* No comment provided by engineer. */ +"@%@" = "@%@"; + /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Dodaj kontakt**: aby utworzyć nowy link z zaproszeniem lub połączyć się za pomocą otrzymanego linku."; @@ -391,6 +397,9 @@ /* No comment provided by engineer. */ "Acknowledgement errors" = "Błędy potwierdzenia"; +/* token status text */ +"Active" = "Aktywne"; + /* No comment provided by engineer. */ "Active connections" = "Aktywne połączenia"; @@ -502,6 +511,9 @@ /* feature role */ "all members" = "wszyscy członkowie"; +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Wszystkie wiadomości i pliki są wysyłane **z szyfrowaniem end-to-end**, z bezpieczeństwem postkwantowym w wiadomościach bezpośrednich."; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Wszystkie wiadomości zostaną usunięte – nie można tego cofnąć!"; @@ -514,6 +526,9 @@ /* profile dropdown */ "All profiles" = "Wszystkie profile"; +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Wszystkie raporty zostaną dla Ciebie zarchiwizowane."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Wszystkie Twoje kontakty pozostaną połączone."; @@ -559,6 +574,9 @@ /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Zezwól na nieodwracalne usunięcie wysłanych wiadomości. (24 godziny)"; +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Zezwól na zgłaszanie wiadomości moderatorom."; + /* No comment provided by engineer. */ "Allow to send files and media." = "Pozwól na wysyłanie plików i mediów."; @@ -613,6 +631,9 @@ /* No comment provided by engineer. */ "and %lld other events" = "i %lld innych wydarzeń"; +/* report reason */ +"Another reason" = "Inny powód"; + /* No comment provided by engineer. */ "Answer call" = "Odbierz połączenie"; @@ -628,6 +649,9 @@ /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Aplikacja szyfruje nowe lokalne pliki (bez filmów)."; +/* No comment provided by engineer. */ +"App group:" = "Grupa aplikacji:"; + /* No comment provided by engineer. */ "App icon" = "Ikona aplikacji"; @@ -655,18 +679,39 @@ /* No comment provided by engineer. */ "Apply to" = "Zastosuj dla"; +/* No comment provided by engineer. */ +"Archive" = "Archiwizuj"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Archiwizować %lld reports?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Archiwizować wszystkie zgłoszenia?"; + /* No comment provided by engineer. */ "Archive and upload" = "Archiwizuj i prześlij"; /* No comment provided by engineer. */ "Archive contacts to chat later." = "Archiwizuj kontakty aby porozmawiać później."; +/* No comment provided by engineer. */ +"Archive report" = "Archiwizuj zgłoszenie"; + +/* No comment provided by engineer. */ +"Archive report?" = "Archiwizować zgłoszenie?"; + +/* swipe action */ +"Archive reports" = "Archiwizuj zgłoszenia"; + /* No comment provided by engineer. */ "Archived contacts" = "Zarchiwizowane kontakty"; /* No comment provided by engineer. */ "Archiving database" = "Archiwizowanie bazy danych"; +/* No comment provided by engineer. */ +"Ask" = "Zapytaj"; + /* No comment provided by engineer. */ "Attach" = "Dołącz"; @@ -5357,6 +5402,9 @@ /* No comment provided by engineer. */ "Your server address" = "Twój adres serwera"; +/* No comment provided by engineer. */ +"Your servers" = "Twoje serwery"; + /* No comment provided by engineer. */ "Your settings" = "Twoje ustawienia"; diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 7075b9b185..28b2398018 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -174,7 +174,7 @@ تغيير عنوان الاستلام؟ نٌسخت إلى الحافظة مسح - مسح الدردشة + امسح الدردشة إنشاء عنوان الدردشات تأكيد عبارة المرور الجديدة… @@ -196,7 +196,7 @@ إنشاء مجموعة سرية خطأ في إحباط تغيير العنوان تفعيل قفل SimpleX - تأكد من بيانات الاعتماد الخاصة بك + تأكد من بيانات اعتمادك أنشئ عنوان SimpleX متابعة تحدث مع المطورين @@ -308,7 +308,7 @@ تواصل عبر الرابط / رمز QR إنشاء رابط دعوة لمرة واحدة تحقق من عنوان الخادم وحاول مرة أخرى. - مسح التَحَقُّق + امسح التحقُّق أنشئ عنوانًا للسماح للأشخاص بالتواصل معك. أدخل الخادم يدويًا ملون @@ -585,7 +585,7 @@ انتهت صلاحية الدعوة! انضمام إلى المجموعة؟ الانضمام المتخفي - الانضمام إلى المجموعة + جارِ الانضمام إلى المجموعة غادِر مغادرة المجموعة؟ غادر @@ -695,7 +695,7 @@ خطأ في إرسال الرسالة خطأ في الانضمام إلى المجموعة خطأ في مزامنة الاتصال - تسجيل الدخول باستخدام بيانات الاعتماد الخاصة بك + سجّل الدخول باستخدام بيانات اعتمادك خطأ في حفظ الملف علّم غير مقروءة تأكد من أن عناوين خادم WebRTC ICE بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة. @@ -712,7 +712,7 @@ فتح في تطبيق الجوال، ثم انقر فوق اتصال في التطبيق.]]> تحت الإشراف في: %s ردود الفعل الرسائل ممنوعة في هذه الدردشة. - مُشرف بواسطة %s + أُشرف بواسطة %s من المرجح أن جهة الاتصال هذه قد حذفت الاتصال بك. ردود فعل الرسائل مكالمة فائتة @@ -775,8 +775,7 @@ يمكن إرسال 10 فيديوهات فقط في نفس الوقت رابط دعوة لمرة واحدة لا - سوف تكون مضيفات البصل مطلوبة للاتصال. -\nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion. + ستكون مضيفات البصل مطلوبة للاتصال. \nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion. اسم عرض جديد: عبارة مرور جديدة… قيد الانتظار @@ -905,7 +904,7 @@ منع إرسال الرسائل التي تختفي. الاحتفاظ بمسودة الرسالة الأخيرة، مع المرفقات. أسماء ملفات خاصة - حماية ملفات تعريف الدردشة الخاصة بك بكلمة مرور! + احمِ ملفات تعريف دردشتك بكلمة مرور! رُفضت المكالمة حماية شاشة التطبيق أُزيلت @@ -1098,7 +1097,7 @@ العنوان الرئيسي سيتم وضع علامة على الرسالة على أنها تحت الإشراف لجميع الأعضاء. انقر للانضمام - للكشف عن ملف تعريفك المخفي، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف الدردشة الخاصة بك. + للكشف عن ملف تعريفك المخفي، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف دردشتك. انقر للانضمام إلى وضع التخفي النظام السمات @@ -1142,8 +1141,8 @@ أنت لقد شاركت رابط لمرة واحدة سيتم إرسال ملف التعريفك إلى جهة الاتصال التي استلمت منها هذا الرابط. - سوف تتصل بجميع أعضاء المجموعة. - ملفات تعريف الدردشة الخاصة بك + ستتصل بجميع أعضاء المجموعة. + ملفات تعريف دردشتك عنوان SimpleX الخاص بك خوادم SMP الخاصة بك عندما يكون التطبيق قيد التشغيل @@ -1158,8 +1157,8 @@ تحقق من رمز الأمان رسائل صوتية عندما يطلب الأشخاص الاتصال، يمكنك قبوله أو رفضه. - سوف تكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! - سوف تكون متصلاً عندما يتم قبول طلب اتصالك، يُرجى الانتظار أو التحقق لاحقًا! + ستكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! + ستكون متصلاً عندما يتم قبول طلب اتصالك، يُرجى الانتظار أو التحقق لاحقًا! تستخدم خوادم SimpleX Chat. استخدم وكيل SOCKS استخدم مضيفي onion. @@ -1201,14 +1200,14 @@ عبر رابط لمرة واحدة مكالمة الفيديو ليست مُعمّاة بين الطريفين غيّرتَ العنوان - سوف تكون متصلاً عندما يكون جهاز جهة اتصالك متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! + ستكون متصلاً عندما يكون جهاز جهة اتصالك متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! غادرت - يجب عليك استخدام أحدث إصدار من قاعدة بيانات الدردشة الخاصة بك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال. + يجب عليك استخدام أحدث إصدار من قاعدة بيانات دردشتك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال. سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! يمكنك مشاركة هذا العنوان مع جهات اتصالك للسماح لهم بالاتصال بـ%s. أُزيلت %1$s حدّث - قاعدة بيانات الدردشة الخاصة بك غير مُعمّاة - عيّن عبارة مرور لحمايتها. + قاعدة بيانات دردشتك غير مُعمّاة - عيّن عبارة مرور لحمايتها. عبارة مرور قاعدة بيانات خاطئة سيتم إرسال ملف تعريف الدردشة الخاص بك إلى أعضاء المجموعة مرحبًا! %1$s @@ -1233,7 +1232,7 @@ سيتم حذف قاعدة بيانات الدردشة الحالية واستبدالها بالقاعدة المستوردة. \nلا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي. تحديث عبارة مرور قاعدة البيانات - سوف تتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ بسجل الدردشة. + ستتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ بسجل الدردشة. أسابيع يمكنك إخفاء أو كتم ملف تعريف المستخدم - اضغط مطولاً للقائمة. ما هو الجديد @@ -1258,9 +1257,9 @@ استخدم للاتصالات الجديدة استخدم الخادم عنوان خادمك - قاعدة بيانات الدردشة الخاصة بك + قاعدة بيانات دردشتك أنت مدعو إلى المجموعة. انضم للتواصل مع أعضاء المجموعة. - لقد انضممت إلى هذه المجموعة. الاتصال بدعوة عضو المجموعة. + لقد انضممت إلى هذه المجموعة. جارِ الاتصال بدعوة عضو المجموعة. غيّرتَ العنوان ل%s إلغاء إخفاء ملف تعريف الدردشة الرسائل الصوتية ممنوعة في هذه الدردشة. @@ -1351,7 +1350,7 @@ عيّن عبارة مرور قاعدة البيانات افتح مجلد قاعدة البيانات سيتم تخزين عبارة المرور في الإعدادات كنص عادي بعد تغييرها أو إعادة تشغيل التطبيق. - يُخزين عبارة المرور في الإعدادات كنص عادي. + عبارة المرور مخزنة في الإعدادات كنص عادي. يُرجى الملاحظة: يتم توصيل مرحلات الرسائل والملفات عبر وكيل SOCKS. تستخدم المكالمات وإرسال معاينات الروابط الاتصال المباشر.]]> عَمِّ الملفات المحلية عَمِّ الملفات والوسائط المخزنة @@ -1438,7 +1437,7 @@ مجموعات التخفي %s و%s و%d عضو هذا الجهاز - %1$d من الرسائل يُشرف عليها بواسطة %2$s + %1$d من الرسائل أُشرف عليها بواسطة %2$s إلغاء حظر العضو %s قُطع اتصاله]]> في انتظار سطح المكتب… @@ -2049,7 +2048,7 @@ شارك ملف التعريف استخدم بيانات اعتماد الوكيل المختلفة لكل اتصال. اسم المستخدم - قد يتم إرسال بيانات الاعتماد الخاصة بك غير مُعمَّاة. + قد يتم إرسال بيانات اعتمادك غير مُعمَّاة. خطأ في حفظ الوكيل إزالة الأرشيف؟ وضع النظام @@ -2058,7 +2057,7 @@ استخدم بيانات اعتماد عشوائية قاعدة بيانات الدردشة حُذف %1$d ملف/ات. - لا يزال يتم تنزيل %1$d ملفًا. + لا يزال يتم تنزيل %1$d ملف/ات. لا تستخدم بيانات الاعتماد مع الوكيل. خطأ في تحويل الرسائل خطأ في تبديل ملف التعريف @@ -2146,7 +2145,7 @@ %s.]]> أُضيفت خوادم الوسائط والملفات الشروط المفتوحة - الخوادم الخاصة بالملفات الجديدة لملف الدردشة الحالي الخاص بك + الخوادم الخاصة بالملفات الجديدة لملف دردشتك الحالي لإرسال خطأ في إضافة الخادم خطأ في تحديث الخادم @@ -2172,7 +2171,7 @@ لا يمكن تحميل نص الشروط الحالية، يمكنك مراجعة الشروط عبر هذا الرابط: خطأ في قبول الشروط خطأ في حفظ الخوادم - على سبيل المثال، إذا تلقى أحد جهات اتصالك رسائل عبر خادم SimpleX Chat، فسوف يقوم تطبيقك بتسليمها عبر خادم Flux. + على سبيل المثال، إذا تلقى أحد جهات اتصالك رسائل عبر خادم SimpleX Chat، فسيقوم تطبيقك بتسليمها عبر خادم Flux. لا يوجد خوادم لتوجيه الرسائل الخاصة. لا يوجد خوادم رسائل. لا يوجد خوادم لاستقبال الملفات. @@ -2231,7 +2230,7 @@ عندما يتم تفعيل أكثر من مُشغل واحد، لن يكون لدى أي منهم بيانات تعريفية لمعرفة مَن يتواصل مع مَن. سيتم تغيير الدور إلى %s. وسيتم إشعار الجميع في الدردشة. سيتم إرسال ملف تعريفك للدردشة إلى أعضاء الدردشة - سوف تتوقف عن تلقي الرسائل من هذه الدردشة. سيتم حفظ سجل الدردشة. + ستتوقف عن تلقي الرسائل من هذه الدردشة. سيتم حفظ سجل الدردشة. عن المُشغلين توصلت SimpleX Chat وFlux إلى اتفاق لتضمين الخوادم التي تديرها Flux في التطبيق. جارِ إعادة التفاوض على التعمية. @@ -2270,7 +2269,7 @@ تغيير القائمة تغيير الترتيب خطأ في حفظ الإعدادات - خطأ في إنشاء التقرير + خطأ في إنشاء بلاغ أنت والمشرفون فقط هم من يرون ذلك بلاغ مؤرشف لا يراه إلا المُرسِل والمُشرفين @@ -2324,4 +2323,27 @@ اكتم الكل ذّكورات غير مقروءة يمكنك ذكر ما يصل إلى %1$s من الأعضاء في الرسالة الواحدة! + السماح بالإبلاغ عن الرسائل إلى المشرفين. + منع الإبلاغ عن الرسائل للمشرفين. + أرشفة كافة البلاغات؟ + أرشف البلاغات + لكل المشرفين + لي + بلاغ: %s + يمكن للأعضاء الإبلاغ عن الرسائل إلى المشرفين. + سيتم أرشفة كافة البلاغات لك. + أرشفة %d بلاغ؟ + يُمنع الإبلاغ عن الرسائل في هذه المجموعة. + لا تفوت رسائل مهمة. + مساعدة المُدراء على إشراف مجموعاتهم. + أذكر الأعضاء 👋 + نظّم الدردشات في القوائم + أرسل بلاغات خاصة + اضبط انتهاء صلاحية الرسالة في الدردشات. + حذف أسرع من المجموعات. + أسماء ملفات الوسائط خاصة. + استلم إشعارًا عند ذكرك. + أداء مجموعات أفضل + خصوصية وأمان أفضل + إرسال أسرع للرسائل. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml index dd2f9a764c..de4042b8f5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -2297,4 +2297,30 @@ Port TCP per a missatgeria Emprar port web Emprar el port TCP %1$s quan no se n\'especifica cap. + Silenciar tot + Mencions no llegides + Podeu mencionar fins a %1$s membres per missatge! + Permetre denunciar missatges a moderació. + No permetre denunciar missatges a moderació. + Arxivar totes les denúncies? + Arxivar %d denúncies? + Arxivar denúncies + Per a totes les moderadores + Per a mi + Denúncia: %s + En aquest grup no es permet denunciar missatges. + Els/les membres poden denunciar els missatges a moderació. + Totes les denúncies s\'arxivaran. + Mencionar els membres 👋 + Enviar denúncies privades + Millor privadesa i seguretat + No us perdeu missatges importants. + Enviament de missatges més ràpid. + Organitzar els xats en llistes + Noms de fitxers multimèdia privats. + Establir la caducitat del missatge als xats. + Rebeu notificació quan se us menciona. + Millor rendiment dels grups + Supressió més ràpida de grups. + Ajudar els administradors a moderar els seus grups. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index 3d9b4a4f87..75ecc38515 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -2042,4 +2042,173 @@ Přijmout podmínky Přijaté podmínky Přidat seznam + Vidí to pouze odesílatelé a moderátoři + Vidite to pouze vy a moderátor + archivovaná hlášení + Chyba v konfiguraci serverů. + Chyba při příjímání podmínek + Žádné servery pro odesílání souborů. + Pokračovat + Otevřít změny + Žádné chaty nebyly nalezeny + archivovaná hlášení podle %s + přijmuté pozvání + Porušení pokynů komunity + Nevhodný obsah + Jiný důvod + Chyba změny serverů + Nevhodný profil + Pro profil chatu %s: + Žádné chatovací servery. + Žádné servery pro soukromé směřování chatů. + Žádné servery pro příjem souborů. + Žádné servery pro příjem zpráv. + Všechny chaty budou ze seznamu odebrány %s, a seznam bude smazán + Pro sociální sítě + Vzdálené telefony + %s.]]> + %s.]]> + %s.]]> + Pro soukromé směrování + Otevřít podmínky + Nebo importovat soubor archivu + Obsah porušuje podmínky používání + Spojení blokováno + Připojení je blokováno serverovým operátorem:\n%1$s. + Archivovat hlášení? + Žádná zpráva + Archivovat + Archivovat hlášení + Vymazat hlášení + Soubor je blokován operátorem serveru:\n%1$s. + Nahlásit + Žádné nepřečtené chaty + Žádné chaty + Žádné chaty v seznamu %s. + Kontakty + Oblíbené + Vše + Nahlásit obsah: uvidí ho pouze moderátoři skupiny. + Otevřít pomocí %s + Vytvořit seznam + Přidat prátele + Ztlumit vše + Název seznamu a emoji by mělo být různé pro všechny seznamy. + Změnit seznam + Název seznamu... + Povolit logování + Síťoví operátoři + Otevřít odkaz ze seznamu chatu + Otevřít webový odkaz? + Zeptat se + Ne + Otevřít odkaz + Chyba ukládání databáze + Přidat členy týmu + Chat bude smazán pro všechny členy - tato akce je nevratná! + Chat bude pro vás smazán - tato akce je nevratná! + Opustit chat + Člen bude odstraněn z chatu - tato akce je nevratná! + Připojení není připraveno. + Podmínky budou automaticky přijaty pro povolené operátory dne: %s. + Síťový operátor + %s.]]> + Přednastavené servery + Text aktuálních podmínek se nepodařilo načíst, podmínky si můžete prohlédnout prostřednictvím tohoto odkazu: + Podmínky budou přijaty dne: %s. + %s.]]> + %s.]]> + Přidány servery pro média & soubory + Povolit flux + Servery přes proxy + Soukromí pro vaše zákazníky. + moderátor + Přidány chatovací servery + Operátor + Zakázat automatické mazání zpráv? + Vymazat zprávy chatu z tohoto zařízení. + Zakázat mazání zpráv + %s, přijměte podmínky používání.]]> + Xiaomi zařízení: aby fungovaly notifikace, povolte prosím Autostart v systémovém nastavení.]]> + Zpráva je moc velká! + Zmenšete prosím velikost zprávy a odešlete ji znovu. + Zmenšete prosím velikost zprávy nebo odeberte média a odešlete ji znovu. + pouze s jedním kontaktem - sdílejte osobně nebo přes jakoukoliv chatovací službu.]]> + Zabezpečení připojení + Aplikace vždy poběží na pozadí + Upozornění a baterie + Například, pokud váš kontakt dostane zprávu pres Chat server SimpleX, vaše aplikace ji doručí pomocí Flux serveru. + Chyba načtení seznamů chatu + Chyba vytváření seznamu chatu + Chyba aktualizace seznamu chatů + Firmy + Dosažitelný panel nástrojů chatu + Pozvat do chatu + Pouze vlastníci chatu mohou upravit předvolby. + Chat + Podmínky použití + Chyba přidání serveru + Panel nástrojů aplikace + Rozmazání + Přímé zprávy mezi členy jsou zakázány. + pro lepší ochranu metadat. + Chat již existuje + Přes proxy + Firemní chaty + Vylepšená navigace chatu + - Otevřít chat na první nepřečtené zprávě.\n- Přejít na citované zprávy. + Archivovat všechna hlášení? + Archivovat %d hlášení? + Pro všechny moderátory + Pro mě + %d hlášení + Skupiny + Seznam + Hlášení členů + Poznámky + Zprávy v tomto chatu nebudou nikdy smazány. + Změnit řazení + Vymazat + Vymazat seznam? + Upravit + Chyba ukládání nastavení + Nebo sdílet soukromě + Firemní adresa + Podmínky přijaté dne: %s. + Přímé zprávy mezi členy jsou v tomto chatu zakázány. + Členové mohou zprávy nahlásit moderátorům. + Zakázat nahlašování zpráv moderátorům. + Chyba aktualizace serveru + Operátor server + Decentralizace sítě + Archivována hlášení členů + Archivovat hlášení + Opustit chat? + Nastavení adres + Adresa nebo jednorázový odkaz? + Přidat na seznam + Přidat členy týmu ke konverzaci + Všechna hlášení vám budou archivována + Povolit nahlašování zpráv moderátorům + Změnit automatické mazání zpráv? + Vytvořit jednorázový odkaz + Jak to pomáhá soukromí + Vymazat chat + Vymazat chat? + %s.]]> + %s.]]> + %1$s.]]> + Relace aplikace + Podmínky budou přijaty pro povolené operátory po 30 dnech. + Připojení vyžaduje opětovné vyjednání šifrování. + Probíhá opětovné vyjednávání o šifrování. + Opravit + Opravit připojení? + Nový server + koncovým šifrováním, s post-quantovým zabezpečením v přímých zprávách.]]> + Žádné služba na pozadí + Kontrolovat zprávy každých 10 minut + Chyba vytváření hlášení + 1 rok + výchozí (%s) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index fc4c421e3b..e79746c8f1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -603,9 +603,9 @@ nie Älter als ein Tag Älter als eine Woche - Älter als einen Monat + Älter als ein Monat %s Sekunde(n) - Löschen der Nachrichten + Nachrichten löschen Automatisches Löschen von Nachrichten aktivieren? Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern. Diese Aktion kann nicht rückgängig gemacht werden! Nachrichten löschen @@ -2401,12 +2401,23 @@ Automatisches Löschen von Nachrichten deaktivieren? Löschen von Nachrichten deaktivieren Älter als ein Jahr - Voreinstellung (%s) + Default (%s) Nachrichten in diesem Chat werden nie gelöscht. - Solange kein Port konfiguriert ist, wird der TCP-Port %1$s genutzt. + Solange kein Port konfiguriert ist, wird TCP-Port %1$s genutzt. Web-Port nutzen - TCP-Port für die Nachrichtenübermittlung + TCP-Port für Nachrichtenübermittlung Sie können bis zu %1$s Mitglieder pro Nachricht erwähnen! Alle stummschalten Ungelesene Erwähnungen + Melden von Nachrichten ist in dieser Gruppe nicht erlaubt. + Alle Meldungen archivieren? + Archiviere %d Meldungen? + Für alle Moderatoren + Für mich + Meldungen archivieren + Mitglieder können Nachrichten an Moderatoren melden. + Melden von Nachrichten an Moderatoren nicht erlauben. + Melden von Nachrichten an Moderatoren erlauben. + Alle Meldungen werden für Sie archiviert. + Meldung: %s diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 48d9969d06..cce810b10c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -87,7 +87,7 @@ ¿Eliminar contacto\? ¿Eliminar mensaje\? ¿Eliminar perfil? - grupo eliminado + ha eliminado el grupo ¿Eliminar grupo\? Eliminar en Autenticación de dispositivo desactivada. Puedes habilitar Bloqueo SimpleX en Configuración, después de activar la autenticación de dispositivo. @@ -110,7 +110,7 @@ mensaje duplicado Herramientas desarrollo Eliminar los archivos de todos los perfiles - Eliminar mensajes + Activar ¡Base de datos cifrada! La base de datos está cifrada con una contraseña aleatoria, puedes cambiarla. Error en base de datos @@ -169,7 +169,7 @@ conectando llamada… Activar llamadas desde la pantalla de bloqueo en Configuración La contraseña de cifrado de la base de datos será actualizada y almacenada en Keystore. - conectando + conectando... creador %d min Tiempo de conexión agotado @@ -355,7 +355,7 @@ Ignorar Error al eliminar base de datos Base de datos cifrada - Error al eliminar miembro + Error al expulsar miembro Los miembros pueden enviar mensajes de voz. en modo incógnito mediante enlace de dirección del contacto ¡Error al crear perfil! @@ -1474,14 +1474,14 @@ Ordenador encontrado ¡No compatible! Esperando conexión móvil: - Eliminar miembro + Expulsar miembro ¿Desbloquear miembro? Para permitir que la aplicación móvil se conecte al ordenador, abre este puerto en el firewall si está habilitado Usar desde ordenador Código de sesión ¿Repetir solicitud de admisión? Crear perfil de chat - ¿Eliminar miembro? + ¿Expulsar miembro? ¡Ya estás conectando mediante este enlace de un solo uso! Desenlazar El nombre del dispositivo será compartido con el cliente móvil conectado. @@ -2065,8 +2065,7 @@ ¿Reenviar mensajes sin los archivos? Asegúrate de que la configuración del proxy es correcta. %1$d archivo(s) ha(n) sido eliminado(s). - %1$d error(es) de archivo -\n%2$s + %1$d error(es) de archivo:\n%2$s La descarga ha fallado para %1$d archivo(s). %1$d archivo(s) no se ha(n) descargado. %1$s mensajes no enviados @@ -2328,13 +2327,36 @@ ¿Modificar la eliminación automática de mensajes? Elimina los mensajes del dispositivo Los mensajes de esta conversación nunca se borran. - Todos los mensajes previos a la selección serán eliminados del chat. ¡No podrá deshacerse! + Todos los mensajes previos al período seleccionado serán eliminados del chat. ¡No podrá deshacerse! Un año predefinido (%s) Puerto TCP para mensajes - Usar puerto TCP %1$s cuando no se especifica. + Se usa el puerto TCP %1$s cuando no se ha especificado otro. Usar puerto web Silenciar todo Menciones sin leer ¡Puedes mencionar hasta %1$s miembros por mensaje! + Los miembros pueden informar de mensajes a los moderadores. + No se permite informar de mensajes a los moderadores. + Se permite informar de mensajes a los moderadores. + Informe: %s + ¿Archivar %d informes? + Archivar informes + Para todos los moderadores + para mí + No se permite informar de mensajes en este grupo. + Todos los informes serán archivados para ti. + ¿Archivar todos los informes? + Las menciones ahora notifican. + Mencionar miembros 👋 + No pierdas los mensajes importantes. + Organiza los chats en listas + Nombres privados de archivos de media. + Eliminación más rápida de grupos. + Envío más rápido de mensajes. + Ayuda a los admins moderar sus grupos. + Mejor rendimiento de los grupos + Mejor privacidad y seguridad + Envia informes privados + Establece la expiración de mensajes en chats. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index 376b3e2d36..b8e99587f7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -1850,4 +1850,5 @@ \nلطفا هر مشکل دیگری را با توسعه‌دهندگان به اشتراک بگذارید. این لینک توسط موبایل دیگری استفاده شده است، لطفا لینک جدیدی در کامپیوتر بسازید. خطای سرور پرونده:%1$s + %1$d خطای پرونده:\n%2$s diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 0721617b2e..6e23eca6b6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -2321,4 +2321,9 @@ Vous pouvez définir les opérateurs dans les paramètres Réseau et serveurs. Transparence Vous pouvez définir un nom de connexion pour vous rappeler avec qui le lien a été partagé. + Autoriser le signalement des messages aux modérateurs. + 1 an + Tous les signalements seront archivés pour vous. + Archiver tous les signalements ? + Archiver %d signalements ? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml index 7f5b73a1d2..f434feb4cd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml @@ -255,7 +255,7 @@ povezivanje Povezan telefon VI - Zamagljenost za bolju privatnost. + Zamućeno za bolju privatnost. %d meseca(i) Poziv završen Poništiti @@ -291,7 +291,7 @@ Blokiraj člana Blokirano administratorom Blokiraj člana? - Zamagljenje + Zamućeno Otkači %d minut(a) Proveri ažuriranje @@ -358,7 +358,7 @@ Onemogućeno za sve Razgovor migriran! %s, %s i %d članovi - Prekinuti + Prekinuti vezu Razgovor već postoji! Baza podataka razgovora odstranjena Proveri ažuriranje @@ -386,7 +386,7 @@ Ili uvesti arhiviranu datoteku Razgovor će biti odstranjen za sve članove - ovo se ne može vratiti! Direktne poruke između članova su zabranjene u ovim porukama. - Prekinuti + Prekinuti vezu %s preuzeto Već ste povezani na %1$s. Glasovne poruke nisu dozvoljenje @@ -704,7 +704,7 @@ Informacije o serveru Popraviti vezu? Arhivirani kontakti - Arhiviraj izveštaj + Arhivirati izveštaj Skenirati QR kod sa radne površine Napustiti razgovor? Dolazni video poziv @@ -925,7 +925,7 @@ Greška: %s Greška u zaustavljanju razgovora Doprinesite - upravio %s + moderirao %s Trenutan Pin kod Čekanje na video Čekanje na dokument @@ -938,7 +938,7 @@ Omogućiti pin kod za samouništenje Pin kod za samouništenje Isključiti - Upravljen u + Moderirano u Vidljiva istorija Prekinuta veza iz razloga: %s Mobilna @@ -1013,7 +1013,7 @@ Promeniti ulogu grupe? Odstraniti datoteku SimpleX Tim - upravljen + moderirano Operator servera blokira vezu:\n%1$s. Promeniti ulogu Ime ovog uređaja @@ -1044,7 +1044,7 @@ Vaš nasumičan profil Molimo pokušajte kasnije. Pošaljite nam email - Upravljen u: %s + Moderirano u: %s Potvrditi otpremanje Popuniti Greške u dešifrovanju @@ -1207,4 +1207,209 @@ Nema poruka Nema istorije Reakcije na poruku su zabranjene. + Bezbedan redosled + Napraviti redosled + Koristiti SOCKS proxy + Greška pri šifrovanju baze podataka + Koristiti za poruke + Greška pri kreiranju profila! + Greška pri učitavanju detalja + Greška pri čuvanju XFTP servera + Otpremljene datoteke + Otvoriti podešavanja servera + Greška pri odstranjivanju privatnih beleški + Greška pri prosleđivanju poruka + Poruke su odstranjene nakon što ste ih odabrali. + NE koristiti privatno usmeravanje. + Kada je IP skrivena + Izgled + Pozvati prijatelje + Potvrditi šifru + Potvrditi pin kod + Odrediti pin kod + Ako unesete Vaš pin kod za samouništenje prilikom otvaranja aplikacije: + Onemogućiti potvrde? + povezivanje (uvedeno) + Uneti šifru u pretragu + Greška pri učitavanju XFTP servera + Greška pri odstranjivanju grupe + Nije moguće poslati poruku + Poslati poruku koja nestaje + Serveri za poruke + Pročitati više + XFTP server + Greška pri pravljenju adrese + neovlašćeno slanje + Verifikovati bezbednostni kod + Dugme zatvoriti + Greška pri pristupanju grupi + Opcije za programere + Alati za programere + Preference razgovora + Potvrdite da su mrežna podešavanja za ovaj uređaj ispravna. + Sadržaj krši uslove korišćenja + Nije moguće primiti datoteku + Greška pri kreiranju poruke + Otvoriti podešavanja aplikacije + Omogućiti SimpleX Zaključavanje + Podeliti datoteku… + Prilagođeno vreme + Povezani serveri + Vaš profil će biti poslan kontaktu od koga ste primili ovu vezu. + Pokrenuti periodično + Preskočene poruke + Pin kod postavljen! + Svi podaci u aplikaciji su odstranjeni. + Pin kod aplikacije je zamenjen pin kodom za samouništenje. + POTPORI SIMPLEX CHAT + Oblik poruke + IKONA APLIKACIJE + Pristupna fraza baze podataka + Odrediti pristupnu frazu + Baza podataka će biti šifrovana. + Neispravna pristupna fraza! + Uneti pristupnu frazu… + Uslovi korišćenja + Greška pri dodavanju servera + Primljen odgovor + Uvećanje + Dozvoliti slanje glasovnih poruka Vašim kontaktima. + Dozvoliti glasovne poruke samo ako ih Vaš kontakt dozvoljava. + Dozvoli svojim kontaktima dodavanje reakcija na poruke. + Dozvoliti slanje direktnih poruka članovima. + Zabraniti slanje direktnih poruka članovima. + Dozvoliti slanje glasovnih poruka. + Dozvoliti slanje SimpleX veza. + SimpleX veze su zabranjene. + Napraviti grupu koristeći nasumičan profil. + Koristiti aplikaciju jednom rukom. + Potvrditi mrežna podešavanja + Greška pri čuvanju baze podataka + Poslan odgovor + Isključiti zvuk svima + Greška pri čuvanju proxy + Ili deliti privatno + Greška pri čuvanju korisničke šifre + Vaše preference + Prethodno povezani serveri + Komadi su preuzeti + Onemogućiti automatsko brisanje poruka? + Hvala Vam što ste instalirali SimpleX Chat! + Neki serveri nisu prošli test: + Mrežni operator + Možete pokušati ponovo. + Poslano putem proxy + pozvan za povezivanje + pomoću %1$s + Greška pri učitavanju SMP servera + Greška pri čuvanju SMP servera + Greška pri promeni adrese + Pristupnu frazu je potrebna + Spomenuti članove 👋 + Bolja privatnost i bezbednost + Ne propustiti bitne poruke. + prilagođen + Otkriti pomoću lokalne mreže + Uneti pristupnu frazu + Proverite internet vezu i pokušajte ponovo + Bez Informacija, pokušajte ponovo da učitate + Komadi su odstranjeni + SimpleX Chat pozivi + XFTP serveri + Mreža i serveri + Podesite ICE servere + Koristiti direktnu internet vezu? + Instalirati ažuriranje + Vaši pozivi + Omogućiti zaključavanje + ID Baze podataka + Zapis ažuriran u + Koristiti za datoteke + Optimizacija baterije je aktivna, isključeni su usluga u pozadini i periodični zahtevi za nove poruke. Možete ih ponovo omogućiti putem podešavanja. + Potvrda identiteta + Test servera nije uspeo! + Greška pri izvoženju baze podataka razgovora + Vremensko ograničenje protokola + Greška pri slanju poruke + Greška pri odstranjivanju kontakta + Napraviti jednokratnu pozivnicu + Markdown pomoć + Dozvoliti slanje datoteka i medijskog sadržaji. + Pin kod promenjen! + ID Baze podataka: %d + SimpleX veze + Dodani serveri za medije i datoteke + Vas profil %1$s će biti deljen. + primljen odgovor… + Mrežni operatori + Privatno usmeravanje + Nevažeća putanja datoteke + Moderirati + %1$d poruke moderirane %2$s + Poruke od %s biće prikazane! + Pristupna fraza baze podataka i izvoz + Koristite privatno usmeravanje sa nepoznatim serverima. + Intenzivno + Poruka će biti označena za ostranjivanje. Primaoci će moći da otkriju ovu poruku. + Privatnost i bezbednost + SimpleX Chat poruke + Možete pokušati ponovo. + Greška pri čuvanju podešavanja + Postavi poruku koja će biti prikazana novim članovima! + Greška pri čuvanju servera + Član će biti uklonjen iz razgovora – ovo se ne može poništiti! + ažuriran profil + povezivanje (najavljeno) + Greška u promeni uloge + Prilagođena tema + Pronaći razgovore brže + Vaša privatnost + Bivši član %1$s + Vremensko ograničenje protokola po KB + Blago + Podeliti medije… + NE šaljite poruke direktno, čak i ako vaš ili odredišni serveri ne podržavaju privatno usmeravanje. + Promeniti pin kod za samouništenje + Ažurirati pristupna frazu baze podataka + Za mene + Podeliti poruku… + Arhivirati izveštaje + SimpleX veze nisu dozvoljene + Promeniti automatsko brisanje poruka? + Glasovne poruke su zabranjene! + Ili prikazati ovaj kod + Koristiti za nove konekcije + Vaši ICE serveri + pokretanje… + Otvoriti podešavanje + Za odvijanje poziva, dozvoliti upotrebu mikrofona. Završite razgovor i pokušajte ponovo. + Baza podataka je šifrovana! + Zapis ažuriran u: %s + Član će biti uklonjen iz grupe – ovo se ne može poništiti! + Pozivi zabranjeni! + \nDostupno u v5.1 + SimpleX veze + Odstraniti za sve + Dozvoliti slanje poruka koje nestaju. + Pin kod aplikacije + Za sakrivanje neželjenih poruka. + Greška pri čuvanju podešavanja + Nevažeće ime! + Komadi su otpremljeni + Greške u otpremanju + Uvek koristiti privatno usmeravanje. + Markdown u porukama + Kada je omogućeno više od jednog operatera, nijedan od njih nema metapodatke da nauči ko komunicira sa kim. + Dozvoliti vašim kontaktima da vas zovu. + Prijem poruke + Greška pri izvoženju baze podataka razgovora + Možete učiniti vidljivim vašim SimpleX kontaktima putem Podešavanja. + Greška pri slanju pozivnice + Preskočiti pozivanje članova + Za privatno usmeravanje + Dodatni sekundarni + Pokretanje iz %s. + Vaši ICE serveri + Koristiti nasumičnu pristupnu frazu + Onemogućiti SimpleX Zaključavanje diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 5cfb9fb8da..9100c029b7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -10,18 +10,18 @@ 5 perc 1 perc A SimpleX-címről - Címváltoztatás megszakítása? + Cím módosításának megszakítása? Megszakítás 30 másodperc - Egyszer használható meghívó-hivatkozás + Egyszer használható meghívási hivatkozás %1$s szeretne kapcsolatba lépni Önnel a következőn keresztül: A SimpleX Chat névjegye 1 nap - Címváltoztatás megszakítása + Cím módosításának megszakítása A SimpleXről Kiemelés fogadott hívás - Hozzáférés a kiszolgálókhoz SOCKS-proxyn keresztül a(z) %d porton? A proxyt el kell indítani, mielőtt engedélyezné ezt az opciót. + Hozzáférés a kiszolgálókhoz SOCKS-proxyn a következő porton keresztül: %d? A proxyt el kell indítani, mielőtt engedélyezné ezt az opciót. Elfogadás Elfogadás gombra fent, majd: @@ -34,7 +34,7 @@ híváshiba Csoporttagok letiltása Hitelesítés - Egy üres csevegési profil jön létre a megadott névvel, és az alkalmazás a szokásos módon megnyílik. + Egy üres csevegési profil lesz létrehozva a megadott névvel, és az alkalmazás a szokásos módon megnyílik. %s visszavonva Előre beállított kiszolgálók hozzáadása A hívások kezdeményezése le van tiltva ebben a csevegésben. @@ -50,7 +50,7 @@ Az adatbázis előkészítése sikertelen Az ismerőseivel kapcsolatban marad. A profilfrissítés el lesz küldve az Ön ismerősei számára. A csevegési profillal (alapértelmezett), vagy a kapcsolattal (BÉTA). - Egy új véletlenszerű profil lesz megosztva. + Egy új, véletlenszerű profil lesz megosztva. A hangüzenetek küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. Az alkalmazás összeállítási száma: %s Hang- és videóhívások @@ -74,15 +74,15 @@ HÍVÁSOK és további %d esemény Cím - Csatlakozás folyamatban! + A csatlakozás folyamatban van a csoporthoz! Automatikus elfogadás A háttérszolgáltatás mindig fut – az értesítések megjelennek, amint az üzenetek elérhetővé válnak. Az elküldött üzenetek végleges törlése engedélyezve van. (24 óra) Mindkét fél küldhet hangüzeneteket. Téves üzenet ID - Az üzenetreakciók küldése engedélyezve van az ismerősei számára. + A reakciók hozzáadása az üzenetekhez engedélyezve van az ismerősei számára. A hangüzenetek küldése engedélyezve van. - Az üzenetreakciók küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. + A reakciók hozzáadása az üzenetekhez csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. Vissza Kikapcsolható a beállításokban – az értesítések továbbra is meg lesznek jelenítve amíg az alkalmazás fut.]]> Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz. @@ -91,11 +91,11 @@ Nem lehet meghívni az ismerőst! téves üzenet ID Kapcsolatkérések automatikus elfogadása - Megjegyzés: NEM fogja tudni helyreállítani, vagy megváltoztatni a jelmondatot abban az esetben, ha elveszíti.]]> + Megjegyzés: NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti.]]> hívás… További másodlagos Hozzáadás egy másik eszközhöz - Az üzenetreakciók küldése engedélyezve van. + A reakciók hozzáadása az üzenetekhez engedélyezve van. Fájlelőnézet visszavonása Az összes csoporttag kapcsolatban marad. Több akkumulátort használ! Az alkalmazás mindig fut a háttérben – az értesítések azonnal megjelennek.]]> @@ -112,7 +112,7 @@ hanghívás félkövér Az alkalmazás-jelkód helyettesítve lesz egy önmegsemmisítő-jelkóddal. - Arab, bulgár, finn, héber, thai és ukrán – köszönet a felhasználóknak és a Weblate-nek. + Arab, bolgár, finn, héber, thai és ukrán – köszönet a felhasználóknak és a Weblate-nek. Hangüzenetek engedélyezése? Mindig használjon továbbító-kiszolgálót mindig @@ -124,20 +124,20 @@ Hang- és videóhívások érvénytelen az üzenet hasítóértéke Mindig fut - Az Android Keystore biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat megváltoztatása után – lehetővé teszi az értesítések fogadását. + Az Android Keystore biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat módosítása után – lehetővé teszi az értesítések fogadását. Az összes alkalmazásadat törölve. Legjobb akkumulátoridő. Csak akkor kap értesítéseket, amikor az alkalmazás meg van nyitva. (NINCS háttérszolgáltatás.)]]> Megjelenés - Az akkumulátor-optimalizálás aktív, ez kikapcsolja a háttérszolgáltatást és az új üzenetek rendszeres lekérdezését. A beállításokban újraengedélyezheti. - Biztosan letiltja? + Az akkumulátor-optimalizálás aktív, ez kikapcsolja a háttérszolgáltatást és az új üzenetek időszakos lekérdezését. A beállításokban újraengedélyezheti. + Tag letiltása? %1$s hívása befejeződött Jó akkumulátoridő. Az alkalmazás 10 percenként ellenőrzi az új üzeneteket. Előfordulhat, hogy hívásokról, vagy a sürgős üzenetekről marad le.]]> szerző Az elküldött üzenetek végleges törlése engedélyezve van az ismerősei számára. (24 óra) Mégse - Az alkalmazás csak akkor tud értesítéseket fogadni, amikor meg van nyitva. A háttérszolgáltatás nem fog elndúlni + Az alkalmazás csak akkor tud értesítéseket fogadni, amikor meg van nyitva. A háttérszolgáltatás nem fog elindulni Továbbfejlesztett üzenetek - A cím megváltoztatása meg fog szakadni. A régi fogadási cím lesz használva. + A cím módosítása meg fog szakadni. A régi fogadási cím lesz használva. Engedélyezés Érvénytelen számítógépcím Profil hozzáadása @@ -157,7 +157,7 @@ A közvetlen üzenetek küldése a tagok között engedélyezve van. ALKALMAZÁS Hívás folyamatban - Mindkét fél küldhet üzenetreakciókat. + Mindkét fél hozzáadhat az üzenetekhez reakciókat. Mindkét fél tud hívásokat kezdeményezni. Sikertelen hitelesítés Az összes %s által írt új üzenet el lesz rejtve! @@ -167,29 +167,29 @@ Hang bekapcsolva hanghívás (nem e2e titkosított) letiltva - Az adatbázis jelmondatának megváltoztatása? + Az adatbázis jelmondatának módosítása? kapcsolódva - Jelkód megváltoztatása - %s szerepkörét megváltoztatta a következőre: %s - A fogadócím megváltoztatása - Megváltoztatás + Jelkód módosítása + a következőre módosította %s szerepkörét: „%s” + A fogadási cím módosítása + Módosítás Jelkód megerősítése Jelszó megerősítése - Tag szerepkörének megváltoztatása? - Zárolási mód megváltoztatása + Tag szerepkörének módosítása? + Zárolási mód módosítása Kapcsolódott kapcsolódott Kapcsolódás kapcsolódott Társított hordozható eszköz kapcsolódva - Szerepkör megváltoztatása + Szerepkör módosítása Kapcsolódva Hitelesítőadatok megerősítése - Megváltoztatja a fogadócímet? - megváltoztatta a címet az Ön számára - Önmegsemmisítő-mód megváltoztatása - megváltoztatta az Ön szerepkörét a következőre: %s + Módosítja a fogadási címet? + módosította a címet az Ön számára + Önmegsemmisítő-mód módosítása + a következőre módosította az Ön szerepkörét: „%s” Kapcsolódás Közvetlen kapcsolódás? Kapcsolódás @@ -199,7 +199,7 @@ Csoport létrehozása véletlenszerű profillal. Az ismerős és az összes üzenet törölve lesz – ez a művelet nem vonható vissza! Az ismerősei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat. - Kapcsolódás egyszer használható meghívó-hivatkozással? + Kapcsolódás egyszer használható meghívási hivatkozással? Kapcsolódás egy hivatkozáson vagy QR-kódon keresztül Kapcsolódási hiba (AUTH) Csak név @@ -224,7 +224,7 @@ Kapcsolat Kapcsolat megszakítva kapcsolat létrehozva - az ismerősnek nincs e2e titkosítása + az ismerős nem rendelkezik e2e titkosítással Ismerős engedélyezi Rejtett név: Társítás számítógéppel @@ -240,7 +240,7 @@ törölt ismerős Csoporttag üzenetének törlése? A csevegés fut - Egyszer használható meghívó-hivatkozás létrehozása + Egyszer használható meghívási hivatkozás létrehozása Törlés Új üzenetek ellenőrzése 10 percenként, legfeljebb 1 percen keresztül Adatbázis törlése @@ -248,8 +248,8 @@ Csevegési profil Profil létrehozása Társított számítógép - Törölve ekkor: %s - Törölve ekkor: + Törölve: %s + Törölve Kínai és spanyol kezelőfelület Nem lehet meghívni az ismerősöket! A csevegés megállt @@ -258,7 +258,7 @@ törölt csoport Törlés az összes tagnál Hivatkozás létrehozása - Csevegési beállítások + Csevegés-beállítások Profil törlése Jelenlegi jelkód kapcsolódás @@ -271,7 +271,7 @@ Jelenleg támogatott legnagyobb fájl méret: %1$s. Fájl törlése Hamarosan! - cím megváltoztatása %s számára… + cím módosítása %s számára… Csevegési adatbázis importálva Üzenetek törlése Kiürítés @@ -295,14 +295,14 @@ Csoport törlése? Adatbázis fejlesztésének megerősítése Saját profil létrehozása - cím megváltoztatása… + cím módosítása… kapcsolódás… Hívás kapcsolása A fájlok- és a médiatartalmak törlése? befejezett CSEVEGÉSI ADATBÁZIS - Önmegsemmisítő-jelkód megváltoztatása - Sorbaállítás létrehozása + Önmegsemmisítő-jelkód módosítása + Sorba állítás létrehozása színezett kapcsolódás… Sötét téma @@ -311,7 +311,7 @@ Csevegés a fejlesztőkkel Hivatkozás törlése? kapcsolódás - Egyéni időbeállítás + Egyéni időköz Kapcsolódás inkognitóban CSEVEGÉSEK Új profil létrehozása a számítógép alkalmazásban. 💻 @@ -322,9 +322,9 @@ Csoporthivatkozás létrehozása Csevegési konzol Fájlok törlése az összes csevegési profilból - Sorbaállítás törlése + Sorba állítás törlése Ismerős törlése - cím megváltoztatása… + cím módosítása… Társítva a hordozható eszközhöz Jelenlegi jelmondat… Fájl kiválasztása @@ -344,7 +344,7 @@ Üzenetek kiürítése? Adatbázis visszafejlesztése Üzenetek kiürítése - Az datbázis titkosítási jelmondata frissítve lesz. + Az adatbázis titkosítási jelmondata frissítve lesz. Kapcsolódás automatikusan Adatbázishiba Az adatbázis titkosítási jelmondata frissülni fog és a beállításokban lesz tárolva. @@ -367,7 +367,7 @@ Kézbesítési jelentések! Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén. Titkosítás visszafejtési hiba - Eltűnik ekkor: %s + Eltűnik: %s szerkesztve Törlés %d óra @@ -377,12 +377,12 @@ Az adatbázis-jelmondat eltér a Keystore-ban lévőtől. Közvetlen üzenetek E-mail - Letiltás az összes tag számára + Letiltás Fejlesztői eszközök Adatbázis-jelmondat %d nap Kapcsolat bontva - Az adatbázis egy véletlenszerű jelmondattal van titkosítva, amelyet megváltoztathat. + Az adatbázis egy véletlenszerű jelmondattal van titkosítva, amelyet szabadon módosíthat. %dó %dhét Felfedezés helyi hálózaton keresztül @@ -391,7 +391,7 @@ Eltűnő üzenet Ne hozzon létre címet Ne mutasd újra - SimpleX zár kikapcsolása + SimpleX-zár kikapcsolása e2e titkosított ESZKÖZ e2e titkosított videóhívás @@ -403,7 +403,7 @@ %dhónap A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban. %d perc - Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt változtassa meg. + Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt módosítsa. Kézbesítés jelentések letiltása a csoportok számára? nap %d nap @@ -411,14 +411,14 @@ Letiltás (felülírások megtartásával) Adatbázis fejlesztése %d üzenet letiltva - Eltűnik ekkor: + Eltűnik %d hét engedélyezve az Ön számára Eltűnő üzenetek Törlés Törlés, és az ismerős értesítése letiltva - %d másodperc + %d mp Az összes fájl törlése Az adatbázis titkosítva lesz. Adatbázis-jelmondat és -exportálás @@ -448,9 +448,9 @@ alapértelmezett (%s) duplikált üzenet Számítógép leválasztása? - Számítógép-alkalmazás verziója %s nem kompatibilis ezzel az alkalmazással. + A számítógép-alkalmazás verziója (%s) nem kompatibilis ezzel az alkalmazással. Kézbesítés - %d fájl %s összméretben + %d fájl, %s összméretben A csevegés megnyitásához adja meg az adatbázis jelmondatát. %dnap Engedélyezés az összes tag számára @@ -459,7 +459,7 @@ Hiba az üzenet küldésekor Adja meg a jelkódot Mindenkinél - Titkosítás-újraegyeztetési hiba + Hiba a titkosítás újraegyeztetésekor Hiba az adatbázis titkosításakor Hiba a csoport törlésekor Kilépés mentés nélkül @@ -483,7 +483,7 @@ Helyi fájlok titkosítása titkosítás elfogadva %s számára %d üzenet megjelölve törlésre - titkosítás újra egyeztetése engedélyezve + a titkosítás újraegyeztetése engedélyezve van Önmegsemmisítés engedélyezése Olvasatlan és kedvenc csevegésekre való szűrés. A csevegések betöltése sikertelen @@ -503,7 +503,7 @@ Gyors és nem kell várni, amíg a feladó online lesz! Hiba a csoporthoz való csatlakozáskor Kedvenc - Csoport moderáció + Csoport moderálása Fájl Csoporthivatkozás a titkosítás újraegyeztetése szükséges %s számára @@ -547,7 +547,7 @@ Engedélyezés (csoport felülírások megtartásával) Hiba az ismerős törlésekor A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra) - Hiba a szerepkör megváltoztatásakor + Hiba a szerepkör módosításakor Javítás A tagok küldhetnek eltűnő üzeneteket. Kapcsolat javítása @@ -555,8 +555,8 @@ Hiba a tag(ok) hozzáadásakor Fájl A tagok küldhetnek fájlokat és médiatartalmakat. - Törlés ennyi idő után: - Hiba a beállítás megváltoztatásakor + Törlés ennyi idő után + Hiba a beállítás módosításakor Hiba a csoporthivatkozás frissítésekor a csoport törölve csoportprofil frissítve @@ -580,7 +580,7 @@ súgó Önmegsemmisítő-jelkód engedélyezése KÍSÉRLETI - Hiba a cím megváltoztatásának megszakításakor + Hiba a cím módosításának megszakításakor Hiba a fájl fogadásakor titkosítás rendben Hiba a kapcsolatkérés törlésekor @@ -598,7 +598,7 @@ Titkosítás javítása az adatmentések helyreállítása után. Hiba a csevegési adatbázis törlésekor Teljes hivatkozás - Hiba a cím megváltoztatásakor + Hiba a cím módosításakor A tagok küldhetnek hangüzeneteket. Csoport-beállítások Hiba: %s @@ -610,7 +610,7 @@ Hiba a részletek betöltésekor Hiba történt a kapcsolatkérés elfogadásakor a titkosítás újraegyeztetése engedélyezve van %s számára - titkosítás-újraegyeztetés szükséges + a titkosítás újraegyeztetése szükséges Rejtett csevegési profilok Fájlok és médiatartalmak A kép elmentve a „Galériába” @@ -618,7 +618,7 @@ Azonnal A fájlok- és a médiatartalmak küldése le van tiltva! Profil elrejtése - Kiszolgálók használata + Hogyan használja a saját kiszolgálóit Csevegési üzenetek gyorsabb megtalálása Téma importálása Hiba a téma importálásakor @@ -661,20 +661,20 @@ Elrejtés: Hiba az ismerőssel történő kapcsolat létrehozásában ICE-kiszolgálók (soronként egy) - beolvashatja a QR-kódot a videohívásban, vagy az ismerőse megoszthat egy meghívó-hivatkozást.]]> + beolvashatja a QR-kódot a videohívásban, vagy az ismerőse megoszthat egy meghívási hivatkozást.]]> Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen el lesz távolítva! Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás közben, vagy ossza meg a hivatkozást. mutassa meg a QR-kódot a videohívásban, vagy ossza meg a hivatkozást.]]> Megerősítés esetén az üzenetváltó-kiszolgálók látni fogják az IP-címét és a szolgáltatóját – azt, hogy mely kiszolgálókhoz kapcsolódik. A kép akkor érkezik meg, amikor a küldője befejezte annak feltöltését. QR-kód beolvasásával.]]> - A kapott SimpleX Chat-meghívó-hivatkozását megnyithatja a böngészőjében: + A kapott SimpleX Chat-meghívási hivatkozását megnyithatja a böngészőjében: Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő-jelkódot: Megtalált számítógép Számítógépek A markdown használata Csevegési profil létrehozása - Levélszemét elleni védelem + Védett a kéretlen tartalommal szemben Hordozható eszközök leválasztása Különböző nevek, profilképek és átvitel-izoláció. Elutasítás esetén a feladó NEM kap értesítést. @@ -682,7 +682,7 @@ A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! meghíva Érvénytelen kapcsolattartási hivatkozás - Némítás + Csak említések nincsenek részletek Nem fogadott hívás Világos @@ -694,12 +694,12 @@ Nincs kézbesítési információ moderált A tag eltávolítása a csoportból – ez a művelet nem vonható vissza! - Győződjön meg arról, hogy az XFTP-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. + Győződjön meg arról, hogy a megadott XFTP-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva. Nincs ismerős kijelölve - Nincsenek fogadott, vagy küldött fájlok + Nincsenek fogadott vagy küldött fájlok Megnyitás hordozható eszköz-alkalmazásban, majd koppintson a Kapcsolódás gombra az alkalmazásban.]]> Markdown az üzenetekben - meghívás a %1$s csoportba + meghívás a(z) %1$s csoportba Zárolási mód Új hordozható eszköz Kapcsolatok megtartása @@ -714,13 +714,13 @@ Társítsa össze a hordozható eszköz- és számítógépes alkalmazásokat! 🔗 közvetett (%1$s) Hamarosan további fejlesztések érkeznek! - Az üzenetreakciók küldése le van tiltva ebben a csevegésben. + A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben. Helytelen biztonsági kód! Ez akkor fordulhat elő, ha Ön vagy az ismerőse régi adatbázis biztonsági mentést használt. Új számítógép-alkalmazás! Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat (megfigyelő szerepkör) meghívta őt: %1$s - A reakciók küldése az üzenetekre le van tiltva. + A reakciók hozzáadása az üzenetekhez le van tiltva. Nem nincs szöveg TAG @@ -733,7 +733,7 @@ k soha (új)]]> - Győződjön meg arról, hogy az SMP-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. + Győződjön meg arról, hogy a megadott SMP-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva. Az onion-kiszolgálók nem lesznek használva. perc Tudjon meg többet @@ -757,7 +757,7 @@ Egy üzenet eltüntetése Végleges üzenettörlés Egyszerre csak 10 videó küldhető el - Csak Ön adhat hozzá üzenetreakciókat. + Csak Ön adhat hozzá reakciókat az üzenetekhez. elhagyta a csoportot Az üzenetek végleges törlése le van tiltva ebben a csevegésben. Max 40 másodperc, azonnal fogadható. @@ -773,16 +773,16 @@ Rendben Nincsenek szűrt csevegések érvénytelen adat - Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. - Csak a csoport-tulajdonosok engedélyezhetik a fájlok- és a médiatartalmak küldését. + Győződjön meg arról, hogy a megadott WebRTC ICE-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva. + Csak a csoport tulajdonosai engedélyezhetik a fájlok- és a médiatartalmak küldését. A fájl betöltése… Nincs hozzáadandó ismerős Üzenetvázlat meghívta, hogy csatlakozzon - Egyszer használható meghívó-hivatkozás + Egyszer használható meghívási hivatkozás Értesítések Egyszerre csak 10 kép küldhető el - ajánlott %s: %2s + ajánlotta: %s, ekkor: %2s Nem kompatibilis! Tegye priváttá a profilját! Üzenetkézbesítési hiba @@ -791,10 +791,10 @@ Némítás Hordozható eszköz társítása Értesítési szolgáltatás - Csak a csoport-tulajdonosok engedélyezhetik a hangüzenetek küldését. + Csak a csoport tulajdonosai engedélyezhetik a hangüzenetek küldését. A felhasználói profilok, ismerősök, csoportok és üzenetek csak az eszközön vannak tárolva a kliensen belül. Érvénytelen átköltöztetési visszaigazolás - Csak a csoport-tulajdonosok változtathatják meg a csoport-beállításokat. + Csak a csoport tulajdonosai módosíthatják a csoport-beállításokat. Nincsenek előzmények Érvénytelen QR-kód Megjelölés olvasottként @@ -807,25 +807,25 @@ Az értesítések az alkalmazás elindításáig nem fognak működni kikapcsolva` (ez az eszköz: v%s)]]> - ajánlott %s + %s ajánlotta Csoport elhagyása - Az összes %s által írt üzenet megjelenik! + Az összes %s által írt üzenet meg fog jelenni! Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Az üzenet visszafejtése sikertelen volt, mert Ön, vagy az ismerőse régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült. megfigyelő inkognitó a csoporthivatkozáson keresztül Onion-kiszolgálók használata, ha azok rendelkezésre állnak. - Ismerősök meghívása + Barátok meghívása Menük és figyelmeztetések Tagok meghívása csatlakozás mint %s Nincs csevegés kijelölve Csak helyi profiladatok - inkognitó egy egyszer használható hivatkozáson keresztül - Moderálva lett ekkor: %s - Egyszer használható meghívó-hivatkozás + inkognitó egy egyszer használható meghívási hivatkozáson keresztül + Moderálva: %s + Egyszer használható meghívási hivatkozás Érvénytelen név! Beszélgessünk a SimpleX Chatben - Moderálva ekkor: + Moderálva Élő üzenetek Hitelesítés Üzenetkézbesítési jelentések! @@ -839,7 +839,7 @@ Új jelmondat… nem fogadott hívás Átköltöztetés: %s - Válaszul erre: + Válaszul erre Név és üzenet Az értesítések csak az alkalmazás bezárásáig érkeznek! Információ @@ -869,10 +869,10 @@ Csatlakozás inkognitóban Csevegés megnyitása elutasított hívás - Rendszeres + Időszakos fogadott, tiltott Kapcsolatkérés megismétlése? - Véglegesen csak Ön törölhet üzeneteket (ismerőse csak törlésre jelölheti őket ). (24 óra) + Véglegesen csak Ön törölhet üzeneteket (ismerőse csak törlésre jelölheti meg őket ). (24 óra) Szerepkör SimpleX kapcsolattartási cím Megállítás @@ -898,22 +898,22 @@ Csak Ön tud hangüzeneteket küldeni. Frissítés Videó elküldve - Az adatbázis jelmondatának megváltoztatása + Az adatbázis jelmondatának módosítása Alkalmazás beállítások megnyitása - A jelkód nem változott meg! + A jelkód nem módosult! Frissítés Kijelölés Csak Ön tud hívásokat indítani. - Biztonságos sorbaállítás + Biztonságos sorba állítás Értékelje az alkalmazást - Egyszer használható meghívó-hivatkozás megosztása + Egyszer használható meghívási hivatkozás megosztása Hiba az adatbázis visszaállításakor %s és %s Ön engedélyezi Csökkentett akkumulátor-használat Mentés és az ismerősök értesítése Előnézet - Simplex Chat használata + SimpleX Chat használata Megosztás Fogadott üzenet Üdvözlőüzenet @@ -926,13 +926,13 @@ (beolvasás, vagy beillesztés a vágólapról) Várakozás a videóra Válasz - Ez az Ön egyszer használható meghívó-hivatkozása! + Ez az Ön egyszer használható meghívási hivatkozása! SimpleX Chat hívások Új inkognitóprofil használata Frissítse az alkalmazást, és lépjen kapcsolatba a fejlesztőkkel. SimpleX Hivatkozás előnézete - a biztonsági kód megváltozott + a biztonsági kód módosult Csak az ismerős nevének megjelenítése Hangszóró bekapcsolva Importált csevegési adatbázis használatához indítsa újra az alkalmazást. @@ -958,7 +958,7 @@ Adatbázismentés visszaállítása Visszavonás Kérje meg az ismerősét, hogy engedélyezze a hangüzenetek küldését. - Ön egy egyszer használható meghívó-hivatkozást osztott meg + Ön egy egyszer használható meghívási hivatkozást osztott meg A hivatkozás megnyitása a böngészőben gyengítheti az adatvédelmet és a biztonságot. A megbízhatatlan SimpleX-hivatkozások pirossal vannak kiemelve. Saját ICE-kiszolgálók Kapcsolat létrehozása @@ -968,7 +968,7 @@ Profiljelszó mentése Fájlküldés megállítása? Számítógép leválasztása? - A hangüzenetek le vannak tilva! + A hangüzenetek le vannak tiltva! Közvetlen üzenet küldése a kapcsolódáshoz PING-ek száma Fejlesztői beállítások megjelenítése @@ -987,12 +987,12 @@ %s: %s A SimpleX nem tud a háttérben futni. Csak akkor fog értesítéseket kapni, amikor az alkalmazás meg van nyitva. Túl sok kép! - %s, %s és %d tag + %s, %s és további %d tag Csevegési szolgáltatás megállítása SimpleX-hivatkozások Az elküldött üzenetek törölve lesznek a beállított idő után. Némítás megszüntetése - Elküldve ekkor: %s + Elküldve: %s Jelenlegi profil használata Ez az eszköz Megosztja a címet az ismerőseivel? @@ -1016,7 +1016,7 @@ Biztonsági kód Adja meg a helyes, jelenlegi jelmondatot. Az elküldött üzenetek végleges törlése le van tiltva. - Az üzenetreakciók küldése le van tiltva. + A reakciók hozzáadása az üzenethez le van tiltva. Véletlenszerű jelmondat használata egyenrangú CSEVEGÉSI SZOLGÁLTATÁS INDÍTÁSA @@ -1028,7 +1028,7 @@ SimpleX Chat szolgáltatás Nem lehet üzeneteket küldeni! %s hitelesítve - Jelszó megjelenítése + Jelszó a megjelenítéshez Adatvédelem és biztonság Eltávolítás A jelkód beállítva! @@ -1043,12 +1043,12 @@ Üdvözlőüzenet mentése? Új csevegési fiók létrehozásához indítsa újra az alkalmazást. Engedély megtagadva! - Főggőben lévő hívás + Függőben lévő hívás Adatbázis megnyitása… Leállítás? Jelmondat szükséges Privát értesítések - Meghívta egy ismerősét + Ön meghívta egy ismerősét %s nincs hitelesítve Koppintson ide a kapcsolódáshoz Ennek az eszköznek a neve @@ -1070,7 +1070,7 @@ tulajdonos Bekapcsolás %s, %s és %s kapcsolódott - Egyszer használható SimpleX-meghívó-hivatkozás + Egyszer használható SimpleX-meghívási hivatkozás Hívások nem sikerült elküldeni KEZELŐFELÜLET SZÍNEI @@ -1082,7 +1082,7 @@ SMP-kiszolgálók Videó Automatikus elfogadási beállítások mentése - Újraegyzetetés + Újraegyeztetés Várakozás a videóra Saját XFTP-kiszolgálók Videó kikapcsolva @@ -1101,20 +1101,20 @@ Saját SMP-kiszolgálók A kézbesítési jelentések le vannak tiltva Adatbázismappa megnyitása - egy egyszer használható meghívó-hivatkozáson keresztül + egy egyszer használható meghívási hivatkozáson keresztül Csoport-beállítások megadása a következőn keresztül: %1$s igen Hangüzenet Társítás számítógéppel PROFIL - port %d + %d-s port Kapcsolódás egy hivatkozáson keresztül Cím megosztása A kiszolgáló QR-kódjának beolvasása Megállítás Címmegosztás megállítása? - Csevegési profilok megváltoztatása + Csevegési profilok módosítása Csatlakozáskérés megismétlése? Várakozás a képre Hangüzenetek @@ -1132,12 +1132,12 @@ Jelkód beállítása Újdonságok Csoport megnyitása - Elküldve ekkor: + Elküldve A hangüzenetek küldése le van tiltva. Legutóbbi üzenet előnézetének megjelenítése Az előre beállított kiszolgáló címe - Rendszeres értesítések letiltva! - A jelkód megváltozott! + Időszakos értesítések letiltva! + A jelkód módosult! Akkor fut, amikor az alkalmazás meg van nyitva Ez a QR-kód nem egy hivatkozás! Várakozás a fájlra @@ -1145,7 +1145,7 @@ Szétkapcsolás Véletlenszerű profil Érvénytelen jelmondat! - Az üzenetreakciók küldése le van tiltva. + A reakciók hozzáadása az üzenetekhez le van tiltva. Rendszer olvasatlan Függőben @@ -1156,7 +1156,7 @@ Videó Frissítés Megnyitás - Rendszeres értesítések + Időszakos értesítések Kihagyott üzenetek A hangüzenetek küldése le van tiltva. Ismerős nevének beállítása @@ -1169,7 +1169,7 @@ Váltás A kapott hivatkozás beillesztése az ismerőshöz való kapcsolódáshoz… Beolvasás - Port megnyitása a tűzfalon + Port nyitása a tűzfalban indítás… Leállítás elküldve @@ -1190,7 +1190,7 @@ QR-kód Titkosítás újraegyeztetése Eltávolítás - Onion kiszolgálók használata + Onion-kiszolgálók használata Felfedés Zárolási mód Fájl visszavonása @@ -1214,7 +1214,7 @@ Közvetlen üzenet küldése Elutasítás Küldés - Rendszerhitelesítés + Rendszer-hitelesítés Böngészőn keresztül Védje meg a csevegési profiljait egy jelszóval! Csak az ismerőse tud eltűnő üzeneteket küldeni. @@ -1228,11 +1228,11 @@ - értesíti az ismerősöket a törlésről (nem kötelező)\n- profil nevek szóközökkel\n- és még sok más! Lengyel kezelőfelület Kiszolgáló használata - Fogadva ekkor: %s + Fogadva: %s SimpleX-zár Mentés és a csoporttagok értesítése Visszaállítás - Csak az ismerőse tud üzenetreakciókat küldeni. + Csak az ismerőse adhat hozzá reakciókat az üzenetekhez. Hangüzenetek Ön elhagyta a csoportot Hangüzenet rögzítése @@ -1252,18 +1252,18 @@ Tudjon meg többet A fájl küldője visszavonta az átvitelt. Csevegési szolgáltatás megállítása? - Fogadva ekkor: + Fogadva Beállítva 1 nap Felfedés Fogadott üzenetbuborék színe Csak az ismerőse tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra) - Az önmegsemmisítő-jelkód megváltozott! + Az önmegsemmisítő-jelkód módosult! SimpleX Chat-kiszolgálók használatban. SimpleX Chat-kiszolgálók használata? Csevegési profil felfedése Videók és fájlok legfeljebb 1GB méretig TCP kapcsolat időtúllépése - A(z) %1$s nevű profiljának SimpleX-címe meg lesz osztva. + A(z) %1$s nevű profilja meg lesz osztva. Ön már kapcsolódott a következőhöz: %1$s. A jelenlegi csevegési adatbázis TÖRÖLVE és CSERÉLVE lesz az importáltra!\nEz a művelet nem vonható vissza – profiljai, ismerősei, csevegési üzenetei és fájljai véglegesen törölve lesznek. Ötletek és javaslatok @@ -1271,25 +1271,25 @@ Koppintson ide az új csevegés indításához Várakozás a számítógépre… Az üzenetváltás jövője - Hálózati beállítások megváltoztatása? + Hálózati beállítások módosítása? Várakozás a hordozható eszköz társítására: Biztonságos kapcsolat hitelesítése fájlok küldése egyelőre még nem támogatott - Ön megváltoztatta a címet %s számára + Ön módosította a címet %s számára fájlok fogadása egyelőre még nem támogatott Csoportprofil mentése Visszaállítás alapértelmezettre Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. videóhívás (nem e2e titkosított) Alkalmazás új kapcsolatokhoz - Az új üzeneteket az alkalmazás rendszeresen lekéri – naponta néhány százalékot használ az akkumulátorból. Az alkalmazás nem használ push-értesítéseket – az eszközről származó adatok nem lesznek elküldve a kiszolgálóknak. + Az új üzeneteket az alkalmazás időszakosan lekéri – naponta néhány százalékot használ az akkumulátorból. Az alkalmazás nem használ push-értesítéseket – az eszközről származó adatok nem lesznek elküldve a kiszolgálóknak. Számítógép címének beillesztése a kapcsolattartási címhivatkozáson keresztül a SimpleX a háttérben fut a push értesítések használata helyett.]]> Az ismerősének online kell lennie ahhoz, hogy a kapcsolat létrejöjjön.\nVisszavonhatja ezt az ismerőskérelmet és eltávolíthatja az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással). - A jelszó nem található a Keystore-ban, ezért kézzel szükséges megadni. Ez akkor történhetett meg, ha visszaállította az alkalmazás adatait egy biztonságimentési eszközzel. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. + A jelszó nem található a Keystore-ban, ezért kézzel szükséges megadni. Ez akkor történhetett meg, ha visszaállította az alkalmazás adatait egy biztonsági mentési eszközzel. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. Az ismerősei továbbra is kapcsolódva maradnak. - A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát + A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát Az adatbázis nem működik megfelelően. Koppintson ide a további információkért A fájl küldése le fog állni. Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál. @@ -1303,9 +1303,9 @@ Ez a szöveg a „Beállításokban” érhető el A profilja el lesz küldve az ismerőse számára, akitől ezt a hivatkozást kapta. Az alkalmazás 1 perc után bezárható a háttérben. - meghívást kapott a csoportba + Ön meghívást kapott a csoportba Engedélyezze a következő párbeszédpanelen az azonnali értesítések fogadásához.]]> - A kiszolgálónak engedélyre van szüksége a sorbaállítás létrehozásához, ellenőrizze jelszavát + A kiszolgálónak engedélyre van szüksége a sorba állítás létrehozásához, ellenőrizze a jelszavát Kapcsolódni fog a csoport összes tagjához. Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen A biztonsága érdekében kapcsolja be a SimpleX-zár funkciót.\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén. @@ -1321,7 +1321,7 @@ A fájl fogadása le fog állni. Ne felejtse el, vagy tárolja biztonságosan – az elveszett jelszót nem lehet visszaállítani! A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését. - Ön egy egyszer használható meghívó-hivatkozást osztott meg inkognitóban + Ön egy egyszer használható meghívási hivatkozást osztott meg inkognitóban Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál. Később engedélyezheti a „Beállításokban” Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! @@ -1332,16 +1332,16 @@ A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) az ismerőse eszközén lévő kóddal. A csevegési adatbázis legfrissebb verzióját CSAK egy eszközön kell használnia, ellenkező esetben előfordulhat, hogy az üzeneteket nem fogja megkapni valamennyi ismerősétől. Ez a beállítás csak az Ön jelenlegi csevegő-profiljában lévő üzenetekre vonatkozik - Meghívást kapott a csoportba. Csatlakozzon, hogy kapcsolatba léphessen a csoport tagjaival. + Ön meghívást kapott a csoportba. Csatlakozzon, hogy kapcsolatba léphessen a csoport tagjaival. Ez a csoport már nem létezik. A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül. - Meghívást kapott a csoportba + Ön meghívást kapott a csoportba Az ismerőse a jelenleg megengedett maximális méretű (%1$s) fájlnál nagyobbat küldött. Az ismerősei és az üzenetek (kézbesítés után) nem a SimpleX-kiszolgálókon vannak tárolva. Üzenetek formázása a szövegbe szúrt speciális karakterekkel: Megnyitás az alkalmazásban gombra.]]> A csevegési profilja el lesz küldve\naz ismerőse számára - Egy olyan ismerősét próbálja meghívni, akivel inkognitó-profilt osztott meg abban a csoportban, amelyben a saját fő profilja van használatban + Egy olyan ismerősét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a saját fő profilja van használatban %1$s nevű csoporthoz.]]> Amikor az alkalmazás fut Inkognitóprofilt használ ehhez a csoporthoz – fő profilja megosztásának elkerülése érdekében a meghívók küldése le van tiltva @@ -1355,11 +1355,11 @@ %s ekkor: %s Akkor lesz kapcsolódva, amikor az ismerősének eszköze online lesz, várjon, vagy ellenőrizze később! Kéretlen üzenetek elrejtése. - Onion kiszolgálók használata opciót „Nemre”, ha a SOCKS-proxy nem támogatja őket.]]> + Onion-kiszolgálók használata opciót „Nemre”, ha a SOCKS-proxy nem támogatja őket.]]> Megoszthatja a címét egy hivatkozásként vagy egy QR-kódként – így bárki kapcsolódhat Önhöz. Létrehozás később A profilja az eszközén van tárolva és csak az ismerőseivel van megosztva. A SimpleX-kiszolgálók nem láthatják a profilját. - Ön megváltoztatta %s szerepkörét a következőre: %s + Ön a következőre módosította %s szerepkörét: „%s” Csoportmeghívó elutasítva Az Ön adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához. (a megosztáshoz az ismerősével) @@ -1376,19 +1376,19 @@ Fejlesztés és a csevegés megnyitása Engedélyeznie kell a hangüzenetek küldését az ismerőse számára, hogy hangüzeneteket küldhessenek egymásnak. %1$s nevű csoport tagja.]]> - Ön megváltoztatta a címet + Ön módosította a címet Az ismerősei engedélyezhetik a teljes üzenet törlést. A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul – nem az eszközön van tárolva. Ha engedélyezni szeretné a hordozható eszköz-alkalmazás társítását a számítógéphez, akkor nyissa meg ezt a portot a tűzfalában, miután engedélyezte azt A profilja, az ismerősei és az elküldött üzenetei az Ön eszközén vannak tárolva. Alkalmazás akkumulátor-használata / Korlátlan módot az alkalmazás beállításaiban.]]> - Ez a karakterlánc nem egy meghívó-hivatkozás! + Ez a karakterlánc nem egy meghívási hivatkozás! Új csevegés indításához - A kapcsolódás már folyamatban van ezen az egyszer használható meghívó-hivatkozáson keresztül! + A kapcsolódás már folyamatban van ezen az egyszer használható meghívási hivatkozáson keresztül! Nem veszíti el az ismerőseit, ha később törli a címét. A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár. kapcsolatba akar lépni Önnel! - Ön megváltoztatta a saját szerepkörét a következőre: %s + Ön a következőre módosította a saját szerepkörét: „%s” A csevegési szolgáltatás elindítható a „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával. Kód hitelesítése a hordozható eszközön Ön csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz. @@ -1405,7 +1405,7 @@ Kód hitelesítése a számítógépen Az időzóna védelmének érdekében a kép-/hangfájlok UTC-t használnak. A kapcsolatkérés el lesz küldve ezen csoporttag számára. - Inkognitó-profil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott. + Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott. Már küldött egy kapcsolatkérést ezen a címen keresztül! Megoszthatja ezt a SimpleX-címet az ismerőseivel, hogy kapcsolatba léphessenek vele: %s. Amikor az emberek kapcsolatot kérnek, Ön elfogadhatja vagy elutasíthatja azokat. @@ -1413,7 +1413,7 @@ Köszönet a felhasználóknak – hozzájárulás a Weblate-en! A kézbesítési jelentések küldése az összes ismerőse számára engedélyezve lesz. Protokoll időtúllépése KB-onként - Az adatbázis-jelmondat megváltoztatására tett kísérlet nem fejeződött be. + Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be. Ez a művelet nem vonható vissza – a kijelöltnél korábban küldött és fogadott üzenetek törölve lesznek. Ez több percet is igénybe vehet. A profilja csak az ismerőseivel van megosztva. Néhány kiszolgáló megbukott a teszten: @@ -1425,19 +1425,19 @@ A kézbesítési jelentések küldése engedélyezve lesz az összes látható csevegési profilban lévő összes ismerőse számára. Bluetooth támogatás és további fejlesztések. Ez a funkció még nem támogatott. Próbálja meg a következő kiadásban. - A bejegyzés frissítve: %s + Bejegyzés frissítve: %s Tagok meghívásának kihagyása Ezek felülbírálhatók az ismerős- és csoport-beállításokban. Az ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni! - A véletlenszerű jelmondat egyszerű szövegként van tárolva a beállításokban.\nEzt később megváltoztathatja. + A véletlenszerű jelmondat egyszerű szövegként van tárolva a beállításokban.\nEzt később módosíthatja. Koppintson ide az inkognitóban való kapcsolódáshoz Jelmondat beállítása az exportáláshoz A kézbesítési jelentések le vannak tiltva %d csoportban Néhány nem végzetes hiba történt az importáláskor: Köszönet a felhasználóknak – hozzájárulás a Weblate-en! A továbbító-kiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címet. - Rendszerhitelesítés helyetti beállítás. - A fogadócím egy másik kiszolgálóra változik. A cím megváltoztatása a feladó online állapotba kerülése után fejeződik be. + Beállítás a rendszer-hitelesítés helyett. + A fogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása a feladó online állapotba kerülése után fejeződik be. A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni. Jelmondat mentése a Keystore-ba Köszönet a felhasználóknak – hozzájárulás a Weblate-en! @@ -1448,7 +1448,7 @@ Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. A mentett WebRTC ICE-kiszolgálók el lesznek távolítva. A kézbesítési jelentések engedélyezve vannak %d csoportban - A szerepkör meg fog változni a következőre: %s. A csoport tagjai értesítést fognak kapni. + A tag szerepköre a következőre fog módosulni: „%s”. A csoport összes tagja értesítést fog kapni. Profil és kiszolgálókapcsolatok Egy üzenetváltó- és alkalmazásplatform, amely védi az adatait és biztonságát. A profil aktiválásához koppintson az ikonra. @@ -1462,16 +1462,16 @@ A következő üzenet azonosítója érvénytelen (kisebb vagy egyenlő az előzővel).\nEz valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. Az eszköz neve meg lesz osztva a társított hordozható eszközön használt alkalmazással. A címzettek a beírás közben látják a szövegváltozásokat. - Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja megváltoztatni. - A jelmondat a beállításokban egyszerű szövegként lesz tárolva, miután megváltoztatta azt vagy újraindította az alkalmazást. + Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja módosítani. + A jelmondat a beállításokban egyszerű szövegként lesz tárolva, miután módosította azt vagy újraindította az alkalmazást. A jelenlegi csevegőprofiljához tartozó új kapcsolatok kiszolgálói Fogadás a következőn keresztül: Tárolja el biztonságosan jelmondát, mert ha elveszti azt, akkor NEM férhet hozzá a csevegéshez. - A szerepkör meg fog változni a következőre: %s. A tag új meghívót fog kapni. + A tag szerepköre a következőre fog módosulni: „%s”. A tag új meghívást fog kapni. profilkép helyőrzője A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet! Ez a művelet nem vonható vissza – profiljai, ismerősei, üzenetei és fájljai véglegesen törölve lesznek. - A bejegyzés frissítve + Bejegyzés frissítve Használati útmutatóban olvasható.]]> A jelmondat a beállításokban egyszerű szövegként van tárolva. Konzol megjelenítése új ablakban @@ -1485,14 +1485,14 @@ Legfeljebb az utolsó 100 üzenet lesz elküldve az új tagok számára. A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás. A beillesztett szöveg nem egy SimpleX-hivatkozás. - A meghívó-hivatkozását újra megtekintheti a kapcsolat részleteinél. + A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél. Csevegés indítása? Látható előzmények Alkalmazás jelkód Ismerős hozzáadása Koppintson ide a QR-kód beolvasásához Koppintson ide a hivatkozás beillesztéséhez - Ismerős hozzáadása: új meghívó-hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz.]]> + Ismerős hozzáadása: új meghívási hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz.]]> Csoport létrehozása: új csoport létrehozásához.]]> A csevegés megállt. Ha már használta ezt az adatbázist egy másik eszközön, úgy visszaállítás szükséges a csevegés elindítása előtt. Az előzmények nem lesznek elküldve az új tagok számára. @@ -1503,7 +1503,7 @@ Vagy mutassa meg ezt a kódot Kamera hozzáférés engedélyezése Fel nem használt meghívó megtartása? - Ennek az egyszer használható meghívó-hivatkozásnak a megosztása + Ennek az egyszer használható meghívási hivatkozásnak a megosztása Új csevegés A csevegések betöltése… Hivatkozás létrehozása… @@ -1523,7 +1523,7 @@ Kapcsolat bontva a számítógéppel A kapcsolat megszakadt A kapcsolat megszakadt - Számítógép kapcsolata rossz állapotban van + A kapcsolat a számítógéppel rossz állapotban van Jelentse a fejlesztőknek:\n%s\n\nAz alkalmazás újraindítása javasolt. Jelentse a fejlesztőknek: \n%s @@ -1543,28 +1543,28 @@ Fejlesztői beállítások A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s %s hordozható eszköz elfoglalt]]> - Már nem tag %1$s + (Már nem tag) %1$s ismeretlen állapot - %1$s megváltoztatta a nevét a következőre: %2$s + %1$s a következőre módosította a nevét: %2$s eltávolította a kapcsolattartási címet eltávolította a profilképét - új kapcsolattartási cím beállítása + új kapcsolattartási címet állított be új profilképet állított be frissített profil - %1$s megváltoztatta a nevét a következőre: %2$s + %1$s a következőre módosította a nevét: %2$s Privát jegyzetek Hiba a privát jegyzetek törlésekor Hiba az üzenet létrehozásakor Privát jegyzetek kiürítése? - Létrehozva ekkor: + Létrehozva Mentett üzenet - Megosztva ekkor: %s + Létrehozva: %s Az összes üzenet törölve lesz – ez a művelet nem vonható vissza! Továbbfejlesztett üzenetkézbesítés Csatlakozás csoportos beszélgetésekhez Hivatkozás beillesztése a kapcsolódáshoz! Privát jegyzetek - A keresősáv elfogadja a meghívó-hivatkozásokat. + A keresősáv elfogadja a meghívási hivatkozásokat. Titkosított fájlokkal és médiatartalmakkal. Csökkentett akkumulátor-használattal. Magyar és török kezelőfelület @@ -1575,10 +1575,10 @@ letiltva az adminisztrátor által Letiltva az adminisztrátor által letiltotta őt: %s - Letiltás az összes tag számára + Letiltás Az összes tag számára letiltja ezt a tagot? %d üzenetet letiltott az adminisztrátor - Letiltás feloldása az összes tag számára + Feloldás Az összes tag számára feloldja a tag letiltását? Ön letiltotta őt: %s Hiba a tag az összes csoporttag számára való letiltásakor @@ -1644,7 +1644,7 @@ %s feltöltve Sikertelen feltöltés Archívum feltöltése - Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, és üzenetkézbesítési hibákat okozhat + Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, mert üzenetkézbesítési hibákat okoz Importálás ismét szabványos végpontok közötti titkosítás Átköltöztetés ide @@ -1663,7 +1663,7 @@ végpontok közötti kvantumálló titkosítás Ez a csevegés végpontok közötti titkosítással védett. Átköltöztetési párbeszédablak megnyitása - Ez a csevegés végpontok közötti kvantumálló tikosítással védett. + Ez a csevegés végpontok közötti kvantumálló titkosítással védett. végpontok közötti titkosítással, sérülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi.]]> végpontok közötti kvantumálló titkosítással sérülés utáni titkosságvédelemmel, visszautasítással és feltörés utáni helyreállítással vannak védve.]]> Hiba az értesítés megjelenítésekor, lépjen kapcsolatba a fejlesztőkkel. @@ -1693,13 +1693,13 @@ A SimpleX-hivatkozások küldése le van tiltva A fájlok- és médiatartalmak nincsenek engedélyezve A SimpleX-hivatkozások küldése engedélyezve van. - Számukra engedélyezve: + Számukra engedélyezve mentett elmentve innen: %s - Továbbítva innen: + Továbbítva innen A címzett(ek) nem látja(k), hogy kitől származik ez az üzenet. Mentett - Elmentve innen: + Elmentve innen Letöltés Továbbítás Továbbított @@ -1721,11 +1721,11 @@ Profilkép alakzata Négyzet, kör vagy bármi a kettő között. Célkiszolgáló-hiba: %1$s - Továbbító kiszolgáló: %1$s\nHiba: %2$s + Továbbító-kiszolgáló: %1$s\nHiba: %2$s Hálózati problémák – az üzenet többszöri elküldési kísérlet után lejárt. A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal. Érvénytelen kulcs vagy ismeretlen kapcsolat – valószínűleg ez a kapcsolat törlődött. - Továbbító-kiszolgáló: %1$s\nCélkiszolgáló hiba: %2$s + Továbbító-kiszolgáló: %1$s\nCélkiszolgáló-hiba: %2$s Hiba: %1$s Kapacitás túllépés – a címzett nem kapta meg a korábban elküldött üzeneteket. Üzenetkézbesítési figyelmeztetés @@ -1733,7 +1733,7 @@ Soha Ismeretlen kiszolgálók Ha az IP-cím rejtett - Üzenetállapot megjelenítése + Üzenet állapotának megjelenítése Visszafejlesztés engedélyezése Mindig Nem @@ -1799,24 +1799,24 @@ Csökkentett akkumulátor-használattal. Hiba a WebView előkészítésekor. Frissítse rendszerét az új verzióra. Lépjen kapcsolatba a fejlesztőkkel.\nHiba: %s Felhasználó által létrehozott téma visszaállítása - Üzenet-sorbaállítási információ + Üzenetsorbaállítási információ nincs Kézbesítési hibák felderítése - a kiszolgáló sorbaállítási információi: %1$s\n\nutoljára kézbesített üzenet: %2$s + a kiszolgáló sorbaállítási információi: %1$s\n\nutoljára fogadott üzenet: %2$s Érvénytelen kulcs vagy ismeretlen fájltöredékcím – valószínűleg a fájl törlődött. - Ideiglenesfájl-hiba - Üzenetállapot - Üzenetállapot: %s + Ideiglenes fájlhiba + Üzenet állapota + Üzenet állapota: %s Fájlhiba A fájl nem található – valószínűleg a fájlt törölték vagy visszavonták. Fájlkiszolgáló-hiba: %1$s - Fájlállapot - Fájlállapot: %s + Fájl állapota + Fájl állapota: %s Másolási hiba Ezt a hivatkozást egy másik hordozható eszközön már használták, hozzon létre egy új hivatkozást a számítógépén. Ellenőrizze, hogy a hordozható eszköz és a számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint a számítógép tűzfalában engedélyezve van-e a kapcsolat.\nMinden további problémát osszon meg a fejlesztőkkel. Nem lehet üzenetet küldeni - A kijelölt csevegési beállítások tiltják ezt az üzenetet. + A kijelölt csevegés-beállítások tiltják ezt az üzenetet. Próbálja meg később. A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. Inaktív tag @@ -1905,7 +1905,7 @@ Munkamenetek átvitele Összes kapcsolat Statisztikák - Információk megjelenítése a következőhöz: + Információk megjelenítése a következőhöz A kiszolgáló verziója nem kompatibilis az alkalmazással: %1$s. Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál. Aktív kapcsolatok száma @@ -1987,14 +1987,14 @@ Kapcsolatok- és kiszolgálók állapotának megjelenítése. Kapcsolódjon gyorsabban az ismerőseihez. Folytatás - Ellenőrízze a hálózatát + Ellenőrizze a hálózatát Média- és fájlkiszolgálók Legfeljebb 20 üzenet egyszerre való törlése. Védi az IP-címét és a kapcsolatait. Könnyen elérhető eszköztár Üzenetkiszolgálók SOCKS-proxy - Néhány fájl nem lett exportálva: + Néhány fájl nem lett exportálva Az exportált adatbázist átköltöztetheti. Mentés és újrakapcsolódás Használja az alkalmazást egy kézzel. @@ -2003,7 +2003,7 @@ Az exportált archívumot elmentheti. Tippek visszaállítása Csevegőlista átváltása: - Ezt a „Megjelenés” menüben változtathatja meg. + Ezt a „Megjelenés” menüben módosíthatja. Új médiabeállítások Lejátszás a csevegési listából. Elhomályosítás a jobb adatvédelemért. @@ -2066,19 +2066,19 @@ Továbbfejlesztett hívásélmény Továbbfejlesztett üzenetdátumok. Továbbfejlesztett felhasználói élmény - Testreszabható üzenetbuborékok. + Testre szabható üzenetbuborékok. Legfeljebb 200 üzenet egyszerre való törlése, vagy moderálása. Legfeljebb 20 üzenet egyszerre való továbbítása. Hang/Videó váltása hívás közben. - Csevegési profilváltás az egyszer használható meghívó-hivatkozásokhoz. + Csevegési profilváltás az egyszer használható meghívási hivatkozásokhoz. Továbbfejlesztett biztonság ✅ A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva. Hiba a kiszolgálók mentésekor Nincsenek üzenet-kiszolgálók. - Nincsenek üzenetfogadó-kiszolgálók. + Nincsenek üzenetfogadási kiszolgálók. Nincsenek média- és fájlkiszolgálók. A(z) %s nevű csevegési profilhoz: - Cím vagy egyszer használható meghívó-hivatkozás? + Cím vagy egyszer használható meghívási hivatkozás? Új kiszolgáló Címbeállítások Előre beállított kiszolgálók @@ -2086,7 +2086,7 @@ Feltételek megtekintése Nincsenek kiszolgálók a privát üzenet-útválasztáshoz. Nincsenek fájlküldő-kiszolgálók. - Nincsenek fájlfogadó-kiszolgálók. + Nincsenek fájlfogadási kiszolgálók. Hibák a kiszolgálók konfigurációjában. Hiba a feltételek elfogadásakor Kézbesítetlen üzenetek @@ -2096,19 +2096,19 @@ Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz. Cím nyilvános megosztása SimpleX-cím megosztása a közösségi médiában. - Egyszer használható meghívó-hivatkozás megosztása egy baráttal - egyetlen ismerőssel használható – személyesen vagy bármilyen üzenetküldőn keresztül megosztható.]]> + Egyszer használható meghívási hivatkozás megosztása egy baráttal + egyetlen ismerőssel használható – személyesen vagy bármilyen üzenetküldőn keresztül megosztható.]]> Beállíthatja az ismerős nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást. Kapcsolatbiztonság - A SimpleX-cím és az egyszer használható meghívó-hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül. + A SimpleX-cím és az egyszer használható meghívási hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül. A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat az ismerősével. A közösségi médiához Vagy a privát megosztáshoz - SimpleX-cím vagy egyszer használható meghívó-hivatkozás? - Egyszer használható meghívó-hivatkozás létrehozása + SimpleX-cím vagy egyszer használható meghívási hivatkozás? + Egyszer használható meghívási hivatkozás létrehozása Kiszolgáló-üzemeltetők Hálózati üzemeltetők - Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetésben más-más üzemeltetőt használ. + Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ. Például, ha az Ön ismerőse egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni. Jelölje ki a használni kívánt hálózati üzemeltetőket. Felülvizsgálat később @@ -2122,24 +2122,24 @@ A feltételek automatikusan el lesznek fogadva az engedélyezett üzemeltetők számára: %s. Az Ön kiszolgálói %s.]]> - %s.]]> + %s.]]> %s kiszolgáló Hálózati üzemeltető Weboldal - Feltételek elfogadva ekkor: %s. + Feltételek elfogadva: %s. A feltételek ekkor lesznek elfogadva: %s. Kiszolgálók használata %s használata A jelenlegi feltételek szövegét nem lehetett betölteni, a feltételeket a következő hivatkozáson keresztül vizsgálhatja felül: %s.]]> - %s.]]> - %s.]]> + %s.]]> + %s.]]> %s.]]> %s.]]> %s.]]> Feltételek elfogadása Használati feltételek - %s kiszolgálók használatához fogadja el a használati feltételeket.]]> + %s kiszolgálóinak használatához fogadja el a használati feltételeket.]]> Használat az üzenetekhez A fogadáshoz A privát útválasztáshoz @@ -2148,10 +2148,10 @@ A küldéshez Hozzáadott média- és fájlkiszolgálók Feltételek megnyitása - Változások megnyitása + Módosítások megtekintése Hiba a kiszolgáló frissítésekor - A kiszolgáló-protokoll megváltozott. - A kiszolgáló üzemeltetője megváltozott. + A kiszolgáló-protokoll módosult. + A kiszolgáló üzemeltetője módosult. Kiszolgáló-üzemeltető Kiszolgáló hozzáadva a következő üzemeltetőhöz: %s. Hiba a kiszolgáló hozzáadásakor @@ -2187,11 +2187,11 @@ A csevegés törölve lesz az Ön számára – ez a művelet nem vonható vissza! Csevegés törlése? Csevegés elhagyása - Csak a csevegés tulajdonosai változtathatják meg a beállításokat. + Csak a csevegés tulajdonosai módosíthatják a csevegés-beállításokat. Könnyen elérhető csevegési eszköztár A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza! Csevegés - A szerepkör meg fog változni a következőre: %s. A csevegés tagjai értesítést fognak kapni. + A tag szerepköre a következőre fog módosulni: „%s”. A csevegés összes tagja értesítést fog kapni. A csevegési profilja el lesz küldve a csevegésben résztvevő tagok számára A tagok közötti közvetlen üzenetek le vannak tiltva. Üzleti csevegések @@ -2215,8 +2215,8 @@ Naplózás engedélyezése Hiba az adatbázis mentésekor áthúzott - A következő csevegési profil törlése: - Üzenetek törlése ennyi idő után: + A következő csevegési profil törlése + Üzenetek törlése ennyi idő után a + b A kapcsolat nem áll készen. Megnyitás a következővel: %s @@ -2241,11 +2241,11 @@ Lista törlése? Szerkesztés Listanév… - Az összes lista nevének és emodzsiának különbözőnek kell lennie. + Az összes lista nevének és emodzsijának különbözőnek kell lennie. Nincsenek csevegések a(z) %s nevű listában. Jegyzetek - Lista megváltoztatása - Elrendezés megváltoztatása + Lista módosítása + Elrendezés módosítása Hiba a jelentés létrehozásakor Hiba a beállítások mentésekor A jelentés archiválva lesz az Ön számára. @@ -2295,10 +2295,33 @@ Csevegési üzenetek törlése az Ön eszközéről. Automatikus üzenettörlés módosítása? Ez a művelet nem vonható vissza – a kijelölt üzenettől korábban küldött és fogadott üzenetek törölve lesznek a csevegésből. - A(z) %1$s port használata, amikor nincs port megadva. + A következő TCP-port használata, amikor nincs port megadva: %1$s. TCP-port az üzenetváltáshoz Webport használata - Olvasatlan megemlítések + Olvasatlan említések Összes némítása Legfeljebb %1$s tagot említhet meg egy üzenetben! + Lehetővé teszi az üzenetek jelentését a moderátorok felé. + Az üzenetek a moderátorok felé történő jelentésének megtiltása. + Az összes jelentés archiválása? + %d jelentés archiválása? + Csak magamnak + Jelentések archiválása + Az összes moderátor számára + Az üzenetek jelentése ebben a csoportban le van tiltva. + A tagok jelenthetik az üzeneteket a moderátorok felé. + Az összes jelentés archiválva lesz az Ön számára. + Jelentés: %s + Tagok említése 👋 + Privát jelentések küldése + Ne maradjon le a fontos üzenetekről. + Gyorsabb üzenetküldés. + Gyorsabb csoporttörlés. + Segítsen az adminisztrátoroknak a csoportjaik moderálásában. + Privát nevek a médiafájlokhoz. + Üzenetek lejárati idejének módosítása a csevegésekben. + Továbbfejlesztett, gyorsabb csoportok + Továbbfejlesztett adatvédelem és biztonság + Kapjon értesítést, ha megemlítik. + Csevegések listákba szervezése diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index fca0124b30..14fb1ad156 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -2306,4 +2306,15 @@ Gunakan port TCP %1$s jika tidak ada port yang ditentukan. Gunakan port peramban Anda dapat menyebut hingga %1$s anggota per pesan! + Semua laporan akan diarsipkan untuk Anda. + Arsipkan semua laporan? + Untuk semua moderator + Untuk saya + Arsip laporan + Laporan: %s + Izinkan untuk laporkan pesan ke moderator. + Anggota dapat melaporkan pesan ke moderator. + Laporkan pesan dilarang di grup ini. + Dilarang laporkan pesan ke moderator. + Arsipkan %d laporan? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index efbcb1d77a..3afc376154 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -2307,11 +2307,11 @@ Archivia la segnalazione Elimina la segnalazione Segnalazioni - Segnalazioni membro archiviate + Segnalazioni dei membri archiviate 1 segnalazione segnalazione archiviata da %s %d segnalazioni - Segnalazioni membro + Segnalazioni dei membri Spam Connessione bloccata Il file è bloccato dall\'operatore del server:\n%1$s. @@ -2338,4 +2338,27 @@ Silenzia tutto Menzioni non lette Puoi menzionare fino a %1$s membri per messaggio! + I membri possono segnalare messaggi ai moderatori. + Archiviare tutte le segnalazioni? + Archiviare %d segnalazioni? + Archivia segnalazioni + Per tutti i moderatori + Per me + Segnalazione: %s + In questo gruppo è vietato segnalare messaggi. + Consenti di segnalare messaggi ai moderatori. + Tutte le segnalazioni verranno archiviate per te. + Vieta di segnalare messaggi ai moderatori. + Non perdere messaggi importanti. + Eliminazione dei gruppi più veloce. + Ricevi una notifica quando menzionato. + Aiuta gli amministratori a moderare i loro gruppi. + Menziona i membri 👋 + Privacy e sicurezza migliorate + Invio dei messaggi più veloce. + Prestazioni dei gruppi migliorate + Organizza le chat in elenchi + Imposta la scadenza dei messaggi nelle chat. + Invia resoconti privati + Nomi privati dei file multimediali. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index 71613f2d8a..edfb18bc89 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -1596,7 +1596,7 @@ contactadres verwijderd profielfoto verwijderd nieuw contactadres instellen - nieuwe profielfoto instellen + nieuwe profielfoto Geblokkeerd door beheerder Blokkeren voor iedereen Lid voor iedereen blokkeren? @@ -2335,4 +2335,27 @@ Alles dempen Ongelezen vermeldingen Je kunt maximaal %1$s leden per bericht vermelden! + Hiermee kunt u berichten rapporteren aan moderators. + Alle rapporten worden voor u gearchiveerd. + Alle rapporten archiveren? + %d rapporten archiveren? + Voor mij + Leden kunnen berichten melden bij moderators. + Het melden van berichten in deze groep is niet toegestaan + Rapport: %s + Het melden van berichten aan moderators is niet toegestaan. + Voor alle moderators + Rapporten archiveren + Betere prestaties van groepen + Stel de berichtvervaldatum in chats in. + Betere privacy en veiligheid + ‐Mis geen belangrijke berichten. + Sneller verwijderen van groepen. + Ontvang een melding als u vermeld wordt. + Help beheerders bij het modereren van hun groepen. + Vermeld leden 👋 + Organiseer chats in lijsten + Namen van persoonlijke mediabestanden. + Privérapporten verzenden + Sneller verzenden van berichten. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index d4f9be50aa..92572516b9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -334,7 +334,7 @@ Echipa SimpleX Închizi? Adresă de contact SimpleX - Link de grup SimpleX + Link pentru grup SimpleX Link-uri SimpleX Invitație unică SimpleX Siglă SimpleX @@ -634,7 +634,7 @@ Adresă desktop Dispozitive desktop Desktop - Eroare la descifrare + Eroare de decriptare Șterge imagine Șterge după Șterge pentru toată lumea @@ -739,4 +739,70 @@ Apeluri îmbunătățite Apel audio primit Termină apelul - \ No newline at end of file + marcate şterse + %d mesaje marcate șterse + moderat de %s + %1$d alte erori de fișier. + %1$s mesaje netransmise + ai distribuit un link de unică folosință + ai distribuit un link ascuns de unică folosință + prin link pentru grup + ascuns prin link pentru grup + Prin browser + Reclamă + Conținut inadecvat + Încălcă normelor comunitare + Profil inadecvat + Alt motiv + Eroare la salvarea serverelor SMP + Asigurați-vă că adresele serverelor XFTP sunt în format corect, pe randuri separate și nu sunt duplicate. + Eroare la încărcarea serverlor XFTP + Eroare la actualizarea configurației de rețea + Eșec la încărcarea conversațiilor + Vă rugăm să actualizați aplicația și contactați dezvoltatorii. + Eroare la crearea profilului! + %d mesaje blocate + Eroare de renegociere a criptării + invitat să se conecteze + ascuns prin link pentru adresa de contact + printr-un link de unică folosință + 1 raport + 1 an + înaintat + eroare de afișare a mesajului + eroare de afișat conținutul + criptare cap-coadă cu secretizare înaintată perfecta, repudiere si recuperare în caz de spargere.]]> + criptare cap-coadă rezistentă la algoritmi cuantici cu secretizare înaintată perfecta, repudiere si recuperare în caz de spargere.]]> + Această conversație este protejată prin criptare cap-coadă. + Link întreg + ascuns printr-un link de unică folosință + a + b + prin %1$s + Eroare la încărcarea serverlor SMP + %1$d fișier(e) șterse. + raport arhivat de %s + invitație acceptată + Asigurați-vă că adresele serverelor SMP sunt în format corect, pe randuri separate și nu sunt duplicate. + Eroare la salvarea serverelor XFTP + %1$d fișier(e) încă în descărcare. + %1$d fișier(e) a eșuat să se descărcarce. + %1$d fișier(e) nu au fost descărcate. + prin link pentru adresa de contact + Deschiderea link-ului în browser poate reduce confidențialitatea și securitatea conexiunii. Link-urile SimpleX de neîncredere vor fi roșii. + solicitat să se conecteze + Eșec la încărcarea conversației + Doar tu și moderatorii vedeți asta + Doar expeditorul şi moderatorii văd asta + raport arhivat + %d mesaje blocate de admin + primirea de fișiere nu este acceptată încă + tu + format mesaj necunoscut + format mesaj invalid + LIVE + moderat + conversație nevalidă + date nevalide + Această conversație este protejată prin criptare cap-coadă, rezistentă la algoritmi cuantici. + Notițe private + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 361e126c10..62d3764940 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -704,7 +704,7 @@ Anlık bildirimler Anlık bildirimler Anlık bildirimler devre dışı! - SimpleX in arkaplanda çalışmasına izin ver seçeneğini işaretleyin. Aksi takdirde, bildirimler devre dışı bırakılacaktır.]]> + izin verin. ]]> Kişi ismi Cihaz doğrulaması devre dışı. SimpleX Kilidi Kapatılıyor. Cihaz doğrulaması etkin değil. Cihaz doğrulamasını etkinleştirdikten sonra SimpleX Kilidini Ayarlar üzerinden açabilirsiniz. @@ -2138,4 +2138,25 @@ Listeye ekle %s listesindeki bütün sohbetler kaldırılacak ve liste silinecek İşletmeler + arşivlenmiş rapor + Başka bir sebep + Raporu arşivle + %s tarafından arşivlenen rapor + 1 rapor + Arşivlenmiş üye raporları + %s.]]> + Sor + 1 yıl + Xiaomi cihazları: Bildirimlerin çalışması için lütfen sistem ayarlarında Otomatik Başlat\'ı etkinleştirin.]]> + Tüm raporlar sizin için arşivlenecektir. + Tüm raporlar arşivlensin mi? + Raporları arşivle + %s.]]> + %s operatörü için de geçerli olacaktır.]]> + %s operatörü(leri) için de geçerli olacaktır.]]> + Mesajların moderatörlere bildirilmesine izin verin. + %d raporu arşivleyelim mi? + Arşiv + Raporu arşivleyelim mi? + uçtan uca şifreli olarak gönderilir ve doğrudan mesajlarda kuantum sonrası güvenlik sağlanır.]]> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index 000517d28c..d1afbe4c38 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -58,7 +58,7 @@ Додати сервер адміністратор Додати привітання - Всі члени групи залишаться підключеними. + Всі учасники групи залишаться підключеними. Дозвольте вашим контактам надсилати повідомлення, які зникають. Всі повідомлення будуть видалені - цю дію неможливо скасувати! Повідомлення будуть видалені ЛИШЕ для вас. Версія додатку @@ -1553,7 +1553,7 @@ \n \nРекомендується перезапустити додаток. Надсилати до 100 останніх повідомлень новим користувачам. - До 100 останніх повідомлень надсилаються новим членам. + До 100 останніх повідомлень надсилаються новим учасникам. З\'єднання перервано Створено: %s Показати внутрішні помилки @@ -2332,4 +2332,18 @@ TCP-порт для повідомлень Використовувати TCP-порт %1$s, якщо порт не вказано. Використовувати веб-порт + Вимкнути звук + Непрочитані згадки + Ви можете згадувати до %1$s учасників у кожному повідомленні! + Усі звіти будуть архівовані для вас. + Архівувати всі звіти? + Архівувати %d звітів? + Архівувати звіти + Для всіх модераторів + Для мене + Звіт: %s + Заборонити повідомлення модераторам. + Повідомлення модераторам заборонено в цій групі. + Учасники можуть повідомляти повідомлення модераторам. + Дозволити повідомляти повідомлення модераторам. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index b7954bcea8..acc3508772 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -2309,4 +2309,15 @@ Bạn có thể nhắc đến tối đa là %1$s thành viên trong một tin nhắn! Tắt thông báo tất cả Những lời nhắc chưa đọc + Các thành viên có thể báo cáo tin nhắn tới các kiểm duyệt viên. + Tất cả các báo cáo sẽ được lưu trữ cho bạn. + Lưu trữ tất cả báo cáo? + Lưu trữ %d báo cáo? + Lưu trữ các báo cáo + Cho tất cả các kiểm duyệt viên + Cho tôi + Báo cáo: %s + Cho phép báo cáo tin nhắn tới các kiểm duyệt viên. + Cấm báo cáo tin nhắn tới các kiểm duyệt viên. + Báo cáo tin nhắn bị cấm trong nhóm này. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 8c9d0429d2..97f1ef9d2d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -2322,4 +2322,27 @@ 全部静音 取消提及的已读状态 每条消息最多提及 %1$s 名成员! + 将为你存档所有举报。 + 存档所有举报? + 存档 %d 份举报? + 存档举报 + 所有 moderators + 仅自己 + 举报:%s + 禁止向 moderators 举报消息。 + 此群禁止消息举报。 + 成员可以向 moderators 举报消息。 + 允许向 moderators 举报消息。 + 提及成员👋 + 更好的群性能 + 更好的隐私和安全 + 不错过重要消息。 + 更快地删除群。 + 更快死发送消息。 + 被提及时收到通知。 + 帮助管理员管理群组。 + 将聊天组织到列表 + 私密媒体文件名。 + 发送私下举报 + 在聊天中设置消息过期时间。 From 981901d587680852c346c82d1a3dd7ed7129c622 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 24 Feb 2025 22:06:52 +0000 Subject: [PATCH 07/24] 6.3-beta.6: ios 266, android 277, desktop 94 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 36 +++++++++---------- .../xcschemes/SimpleX SE.xcscheme | 1 + apps/multiplatform/gradle.properties | 8 ++--- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 8e9c47850a..8ac80ccbdc 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -174,8 +174,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -531,8 +531,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -688,8 +688,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -774,8 +774,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.5-8EACP2Rqr2fC85J6D8STdI.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.6-JqjWEhRZNfuBevXEobl047.a */, ); path = Libraries; sourceTree = ""; @@ -1963,7 +1963,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 265; + CURRENT_PROJECT_VERSION = 266; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2012,7 +2012,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 265; + CURRENT_PROJECT_VERSION = 266; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2053,7 +2053,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 265; + CURRENT_PROJECT_VERSION = 266; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2073,7 +2073,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 265; + CURRENT_PROJECT_VERSION = 266; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2098,7 +2098,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 265; + CURRENT_PROJECT_VERSION = 266; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2135,7 +2135,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 265; + CURRENT_PROJECT_VERSION = 266; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2172,7 +2172,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 265; + CURRENT_PROJECT_VERSION = 266; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2223,7 +2223,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 265; + CURRENT_PROJECT_VERSION = 266; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2274,7 +2274,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 265; + CURRENT_PROJECT_VERSION = 266; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2308,7 +2308,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 265; + CURRENT_PROJECT_VERSION = 266; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme index 1a1fa8b800..a2639eb263 100644 --- a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme +++ b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme @@ -74,6 +74,7 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" + askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 02771976c6..ea0516fdaa 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.3-beta.5 -android.version_code=276 +android.version_name=6.3-beta.6 +android.version_code=277 -desktop.version_name=6.3-beta.5 -desktop.version_code=93 +desktop.version_name=6.3-beta.6 +desktop.version_code=94 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 From 511ff1d35cf7a6ccf466cf288e8aa517fc04c454 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 25 Feb 2025 09:24:30 +0000 Subject: [PATCH 08/24] cli: remove multiple members (#5656) * cli: remove multiple members * accept all members joining via link as observers (do NOT release) * blocked words * blocked words * XGrpLinkReject * core: 6.3.0.6 (simplexmq 6.3.0.6) --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- apps/simplex-directory-service/Main.hs | 5 +- .../src/Directory/BlockedWords.hs | 64 +++++++++++++++++++ .../src/Directory/Options.hs | 60 ++++++++++++++++- .../src/Directory/Service.hs | 19 +++++- simplex-chat.cabal | 3 + src/Simplex/Chat.hs | 3 + src/Simplex/Chat/Controller.hs | 10 ++- src/Simplex/Chat/Library/Commands.hs | 13 +++- src/Simplex/Chat/Library/Subscriber.hs | 21 ++++-- src/Simplex/Chat/ProfileGenerator.hs | 11 ++++ src/Simplex/Chat/Protocol.hs | 1 + src/Simplex/Chat/Store/Connections.hs | 4 +- src/Simplex/Chat/Store/Groups.hs | 26 ++++++-- src/Simplex/Chat/Store/Messages.hs | 8 ++- .../SQLite/Migrations/chat_query_plans.txt | 29 +++++++-- src/Simplex/Chat/Store/Shared.hs | 7 +- src/Simplex/Chat/Types.hs | 4 +- tests/Bots/DirectoryTests.hs | 5 ++ tests/ChatClient.hs | 10 +-- 19 files changed, 267 insertions(+), 36 deletions(-) create mode 100644 apps/simplex-directory-service/src/Directory/BlockedWords.hs diff --git a/apps/simplex-directory-service/Main.hs b/apps/simplex-directory-service/Main.hs index 0c6464dbfe..b01f088483 100644 --- a/apps/simplex-directory-service/Main.hs +++ b/apps/simplex-directory-service/Main.hs @@ -6,7 +6,6 @@ import Directory.Options import Directory.Service import Directory.Store import Simplex.Chat.Core -import Simplex.Chat.Terminal (terminalChatConfig) main :: IO () main = do @@ -14,4 +13,6 @@ main = do st <- restoreDirectoryStore directoryLog if runCLI then directoryServiceCLI st opts - else simplexChatCore terminalChatConfig (mkChatOpts opts) $ directoryService st opts + else do + cfg <- directoryChatConfig opts + simplexChatCore cfg (mkChatOpts opts) $ directoryService st opts diff --git a/apps/simplex-directory-service/src/Directory/BlockedWords.hs b/apps/simplex-directory-service/src/Directory/BlockedWords.hs new file mode 100644 index 0000000000..7477226e53 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/BlockedWords.hs @@ -0,0 +1,64 @@ +module Directory.BlockedWords where + +import Data.Char (isMark, isPunctuation, isSpace) +import Data.List (isPrefixOf) +import Data.Maybe (fromMaybe) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Normalize as TN + +containsBlockedWords :: Map Char [Char] -> [String] -> Text -> Bool +containsBlockedWords spelling blockedWords s = + let normalizedWords = concatMap words $ normalizeText spelling s + -- Fully normalize the entire string (no spaces or punctuation) + fullNorm = normalizeText spelling $ T.filter (not . isSpace) s + -- Check if any individual word is a swear word + wordCheck = any (`elem` blockedWords) normalizedWords + -- Check if the full string, when normalized, matches a swear word exactly + fullCheck = any (\bw -> T.length s <= length bw * 2 && any (bw ==) fullNorm) blockedWords + -- Check if the string is a single word (no spaces) + isSingleWord = not $ T.any isSpace s + in wordCheck || (fullCheck && not isSingleWord) + +normalizeText :: Map Char [Char] -> Text -> [String] +normalizeText spelling = + filter (not . null) + . map (filter (\c -> not (isPunctuation c) && not (isMark c))) + . allSubstitutions spelling + . removeTriples + . T.unpack + . T.toLower + . TN.normalize TN.NFKD + +-- replaces triple and larger occurences with doubles +removeTriples :: String -> String +removeTriples xs = go xs '\0' False + where + go [] _ _ = [] + go (c : cs) prev samePrev + | prev /= c = c : go cs c False + | samePrev = go cs c True + | otherwise = c : go cs c True + +-- Generate all possible strings by substituting each character +allSubstitutions :: Map Char [Char] -> String -> [String] +allSubstitutions spelling = sequence . map substs + where + substs c = fromMaybe [c] $ M.lookup c spelling + +wordVariants :: [(String, [String])] -> String -> [String] +wordVariants [] s = [s] +wordVariants (sub : subs) s = concatMap (wordVariants subs) (replace sub) + where + replace (pat, tos) = go s + where + go [] = [""] + go s'@(c : rest) + | pat `isPrefixOf` s' = + let s'' = drop (length pat) s' + restVariants = go s'' + in map (pat <>) restVariants + <> concatMap (\to -> map (to <>) restVariants) tos + | otherwise = map (c :) (go rest) diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index a62939b6ac..277d332cf6 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -1,5 +1,6 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -12,9 +13,10 @@ module Directory.Options where import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Options.Applicative import Simplex.Chat.Bot.KnownContacts -import Simplex.Chat.Controller (updateStr, versionNumber, versionString) +import Simplex.Chat.Controller (AcceptAsObserver (..), updateStr, versionNumber, versionString) import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreChatOptsP) data DirectoryOpts = DirectoryOpts @@ -22,6 +24,11 @@ data DirectoryOpts = DirectoryOpts adminUsers :: [KnownContact], superUsers :: [KnownContact], ownersGroup :: Maybe KnownGroup, + blockedWordsFile :: Maybe FilePath, + blockedExtensionRules :: Maybe FilePath, + nameSpellingFile :: Maybe FilePath, + profileNameLimit :: Int, + acceptAsObserver :: Maybe AcceptAsObserver, directoryLog :: Maybe FilePath, serviceName :: T.Text, runCLI :: Bool, @@ -55,6 +62,43 @@ directoryOpts appDir defaultDbName = do <> metavar "OWNERS_GROUP" <> help "The group of group owners in the format GROUP_ID:DISPLAY_NAME - owners of listed groups will be invited automatically" ) + blockedWordsFile <- + optional $ + strOption + ( long "blocked-words-file" + <> metavar "BLOCKED_WORDS_FILE" + <> help "File with the basic forms of words not allowed in profiles and groups" + ) + blockedExtensionRules <- + optional $ + strOption + ( long "blocked-extenstion-rules" + <> metavar "BLOCKED_EXTENSION_RULES" + <> help "Substitions to extend the list of blocked words" + ) + nameSpellingFile <- + optional $ + strOption + ( long "name-spelling-file" + <> metavar "NAME_SPELLING_FILE" + <> help "File with the character substitions to match in profile names" + ) + profileNameLimit <- + option + auto + ( long "profile-name-limit" + <> metavar "PROFILE_NAME_LIMIT" + <> help "Max length of profile name that will be allowed to connect and to join groups" + <> value maxBound + ) + acceptAsObserver <- + optional $ + option + parseAcceptAsObserver + ( long "accept-as-observer" + <> metavar "ACCEPT_AS_OBSERVER" + <> help "Whether to accept all or some of the joining members without posting rights ('all', 'no-image', 'incognito')" + ) directoryLog <- Just <$> strOption @@ -80,6 +124,11 @@ directoryOpts appDir defaultDbName = do adminUsers, superUsers, ownersGroup, + blockedWordsFile, + blockedExtensionRules, + nameSpellingFile, + profileNameLimit, + acceptAsObserver, directoryLog, serviceName = T.pack serviceName, runCLI, @@ -116,3 +165,12 @@ mkChatOpts DirectoryOpts {coreOptions} = markRead = False, maintenance = False } + +parseAcceptAsObserver :: ReadM AcceptAsObserver +parseAcceptAsObserver = eitherReader $ decodeAAO . encodeUtf8 . T.pack + where + decodeAAO = \case + "all" -> Right AOAll + "name-only" -> Right AONameOnly + "incognito" -> Right AOIncognito + _ -> Left "bad AcceptAsObserver" diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index ed51371be3..543ba2c84e 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} @@ -10,6 +11,7 @@ module Directory.Service ( welcomeGetOpts, directoryService, directoryServiceCLI, + directoryChatConfig ) where @@ -18,7 +20,10 @@ import Control.Concurrent.Async import Control.Concurrent.STM import Control.Logger.Simple import Control.Monad +import Data.Composition ((.:)) +import Data.Containers.ListUtils (nubOrd) import Data.List (find, intercalate) +import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isJust, maybeToList) import Data.Set (Set) import qualified Data.Set as S @@ -26,6 +31,7 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (diffUTCTime, getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) +import Directory.BlockedWords import Directory.Events import Directory.Options import Directory.Search @@ -97,8 +103,9 @@ directoryServiceCLI st opts = do env <- newServiceState eventQ <- newTQueueIO let eventHook cc resp = atomically $ resp <$ writeTQueue eventQ (cc, resp) + cfg <- directoryChatConfig opts race_ - (simplexChatCLI' terminalChatConfig {chatHooks = defaultChatHooks {eventHook}} (mkChatOpts opts) Nothing) + (simplexChatCLI' cfg {chatHooks = defaultChatHooks {eventHook}} (mkChatOpts opts) Nothing) (processEvents eventQ env) where processEvents eventQ env = forever $ do @@ -114,6 +121,16 @@ directoryService st opts@DirectoryOpts {testing} user cc = do (_, _, resp) <- atomically . readTBQueue $ outputQ cc directoryServiceEvent st opts env user cc resp +directoryChatConfig :: DirectoryOpts -> IO ChatConfig +directoryChatConfig DirectoryOpts {blockedWordsFile, nameSpellingFile, blockedExtensionRules, profileNameLimit, acceptAsObserver} = do + blockedWords <- mapM (fmap lines . readFile) blockedWordsFile + spelling <- maybe (pure M.empty) (fmap (M.fromList . read) . readFile) nameSpellingFile + extensionRules <- maybe (pure []) (fmap read . readFile) blockedExtensionRules + let !bws = nubOrd . concatMap (wordVariants extensionRules) <$> blockedWords + !allowedProfileName = not .: containsBlockedWords spelling <$> bws + putStrLn $ "Blocked words: " <> show (maybe 0 length bws) <> ", spelling rules: " <> show (M.size spelling) + pure terminalChatConfig {allowedProfileName, profileNameLimit, acceptAsObserver} + directoryServiceEvent :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> ChatResponse -> IO () directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} ServiceState {searchRequests} user@User {userId} cc event = forM_ (crDirectoryEvent event) $ \case diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 2d7172133d..3f12ebd1af 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -407,6 +407,7 @@ executable simplex-directory-service default-extensions: StrictData other-modules: + Directory.BlockedWords Directory.Events Directory.Options Directory.Search @@ -427,6 +428,7 @@ executable simplex-directory-service , simplexmq >=6.3 , stm ==2.5.* , time ==1.12.* + , unicode-transforms ==0.4.* default-language: Haskell2010 if flag(client_postgres) cpp-options: -DdbPostgres @@ -517,6 +519,7 @@ test-suite simplex-chat-test , stm ==2.5.* , terminal ==0.2.* , time ==1.12.* + , unicode-transforms ==0.4.* , unliftio ==0.2.* default-language: Haskell2010 if flag(client_postgres) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index bf07e4ae51..89bd16b273 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -112,6 +112,9 @@ defaultChatConfig = ntf = _defaultNtfServers, netCfg = defaultNetworkConfig }, + allowedProfileName = Nothing, + profileNameLimit = maxBound, + acceptAsObserver = Nothing, tbqSize = 1024, fileChunkSize = 15780, -- do not change xftpDescrPartSize = 14000, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 31f158e780..5b1712b6b4 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -137,6 +137,9 @@ data ChatConfig = ChatConfig chatVRange :: VersionRangeChat, confirmMigrations :: MigrationConfirmation, presetServers :: PresetServers, + allowedProfileName :: Maybe (ContactName -> Bool), + profileNameLimit :: Int, + acceptAsObserver :: Maybe AcceptAsObserver, tbqSize :: Natural, fileChunkSize :: Integer, xftpDescrPartSize :: Int, @@ -158,6 +161,11 @@ data ChatConfig = ChatConfig chatHooks :: ChatHooks } +data AcceptAsObserver + = AOAll -- all members + | AONameOnly -- members without image + | AOIncognito -- members with incognito-style names and without image + data RandomAgentServers = RandomAgentServers { smpServers :: NonEmpty (ServerCfg 'PSMP), xftpServers :: NonEmpty (ServerCfg 'PXFTP) @@ -481,7 +489,7 @@ data ChatCommand | JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter} | MemberRole GroupName ContactName GroupMemberRole | BlockForAll GroupName ContactName Bool - | RemoveMember GroupName ContactName + | RemoveMembers GroupName (NonEmpty ContactName) | LeaveGroup GroupName | DeleteGroup GroupName | ClearGroup GroupName diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 79ff9333c6..23f4406b9c 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2116,7 +2116,16 @@ processChatCommand' vr = \case processChatCommand $ APIJoinGroup groupId enableNtfs MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMemberRole gId gMemberId memRole BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMemberForAll gId gMemberId blocked - RemoveMember gName gMemberName -> withMemberName gName gMemberName APIRemoveMember + RemoveMembers gName gMemberNames -> withUser $ \user -> do + (gId, gMemberIds) <- withStore $ \db -> do + gId <- getGroupIdByName db user gName + gMemberIds <- forM gMemberNames $ getGroupMemberIdByName db user gId + pure (gId, gMemberIds) + rs <- forM (L.zip (L.fromList [1..]) gMemberIds) $ \(i, memId) -> do + r <- processChatCommand (APIRemoveMember gId memId) + when (i < length gMemberIds) $ toView r + pure r + pure $ L.last rs LeaveGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APILeaveGroup groupId @@ -3873,7 +3882,7 @@ chatCommandP = ("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> memberRole), "/block for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure True), "/unblock for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure False), - ("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP), + ("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMembers <$> displayNameP <* A.space <*> (L.fromList <$> (char_ '@' *> displayNameP) `A.sepBy1'` A.char ',')), ("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayNameP), ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayNameP), ("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayNameP <*> chatDeleteMode), diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 1530f8e5c4..120e74cb7a 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -48,7 +48,7 @@ import Simplex.Chat.Library.Internal import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events -import Simplex.Chat.ProfileGenerator (generateRandomProfile) +import Simplex.Chat.ProfileGenerator (generateRandomProfile, isRandomName) import Simplex.Chat.Protocol import Simplex.Chat.Store import Simplex.Chat.Store.Connections @@ -1290,8 +1290,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> pure () where profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> CM () - profileContactRequest invId chatVRange p xContactId_ reqPQSup = do - withStore (\db -> createOrUpdateContactRequest db vr user userContactLinkId invId chatVRange p xContactId_ reqPQSup) >>= \case + profileContactRequest invId chatVRange p@Profile {displayName, image} xContactId_ reqPQSup = do + cfg <- asks config + withAllowedName cfg $ withStore (\db -> createOrUpdateContactRequest db vr user userContactLinkId invId chatVRange p xContactId_ reqPQSup) >>= \case CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact CORGroup gInfo -> toView $ CRBusinessRequestAlreadyAccepted user gInfo CORRequest cReq -> do @@ -1320,11 +1321,23 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo if v >= groupFastLinkJoinVersion then do - mem <- acceptGroupJoinRequestAsync user gInfo cReq gLinkMemRole profileMode + let useRole = userMemberRole gLinkMemRole $ acceptAsObserver cfg + mem <- acceptGroupJoinRequestAsync user gInfo cReq useRole profileMode createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing toView $ CRAcceptingGroupJoinRequestMember user gInfo mem else messageError "processUserContactRequest: chat version range incompatible for accepting group join request" _ -> toView $ CRReceivedContactRequest user cReq + where + withAllowedName ChatConfig {profileNameLimit, allowedProfileName} action + | T.length displayName <= profileNameLimit && maybe True ($ displayName) allowedProfileName = action + | otherwise = liftIO $ putStrLn $ "Joining of " <> T.unpack displayName <> " is blocked" -- TODO send response, maybe event to UI? + userMemberRole linkRole = \case + Just AOAll -> GRObserver + Just AONameOnly | noImage -> GRObserver + Just AOIncognito | noImage && isRandomName displayName -> GRObserver + _ -> linkRole + where + noImage = maybe True (\(ImageData i) -> i == "") image memberCanSend :: GroupMember -> CM () -> CM () memberCanSend GroupMember {memberRole} a diff --git a/src/Simplex/Chat/ProfileGenerator.hs b/src/Simplex/Chat/ProfileGenerator.hs index 95f5f16207..8380bb58f1 100644 --- a/src/Simplex/Chat/ProfileGenerator.hs +++ b/src/Simplex/Chat/ProfileGenerator.hs @@ -2,6 +2,8 @@ module Simplex.Chat.ProfileGenerator where +import qualified Data.Attoparsec.Text as A +import Data.Either (isRight) import Data.Text (Text) import Simplex.Chat.Types (Profile (..)) import System.Random (randomRIO) @@ -23,6 +25,15 @@ generateRandomProfile = do then pickNoun adjective (n - 1) else pure noun +-- This function does not check for exact match with this disctionary, +-- it only checks for the WordWord style. +isRandomName :: Text -> Bool +isRandomName = isRight . A.parseOnly randomNameP + where + randomNameP = A.satisfy upper >> A.takeWhile1 lower >> A.satisfy upper >> A.takeWhile1 lower >> A.endOfInput + upper c = c >= 'A' && c <= 'Z' + lower c = c >= 'a' && c <= 'z' + adjectives :: [Text] adjectives = [ "Abatic", diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 9c8c044630..c19e89a776 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -326,6 +326,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json XGrpAcpt :: MemberId -> ChatMsgEvent 'Json XGrpLinkInv :: GroupLinkInvitation -> ChatMsgEvent 'Json + -- XGrpLinkReject :: GroupProfile -> RejectionReason -> ChatMsgEvent 'Json XGrpLinkMem :: Profile -> ChatMsgEvent 'Json XGrpMemNew :: MemberInfo -> ChatMsgEvent 'Json XGrpMemIntro :: MemberInfo -> Maybe MemberRestrictions -> ChatMsgEvent 'Json diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index dce5ca0a79..d43f7169ea 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -141,9 +141,11 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at, -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 204689a7b9..bddbdbcce5 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -173,11 +173,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif -type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences)) +type MaybeGroupMemberRow = (Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime) toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences)) = - Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences)) +toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences) :. (Just createdAt, Just updatedAt)) = + Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences) :. (createdAt, updatedAt)) toMaybeGroupMember _ _ = Nothing createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO () @@ -281,9 +281,11 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = do mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at, -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -469,7 +471,9 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe memberContactId = Just $ contactId' userOrContact, memberContactProfileId = localProfileId (profile' userOrContact), activeConn = Nothing, - memberChatVRange + memberChatVRange, + createdAt, + updatedAt = createdAt } where memberChatVRange@(VersionRange minV maxV) = vr @@ -744,7 +748,8 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, - mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences + mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu USING (group_id) @@ -808,6 +813,7 @@ groupMemberQuery = SELECT m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -975,7 +981,9 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, memberContactId = Just contactId, memberContactProfileId = localProfileId profile, activeConn = Nothing, - memberChatVRange = peerChatVRange + memberChatVRange = peerChatVRange, + createdAt, + updatedAt = createdAt } where insertMember_ = @@ -1257,7 +1265,9 @@ createNewMember_ memberContactId, memberContactProfileId, activeConn, - memberChatVRange + memberChatVRange, + createdAt, + updatedAt = createdAt } checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId) @@ -1509,9 +1519,11 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at, -- via GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index f37201c487..22a3aadd34 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -515,7 +515,8 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe -- GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences + p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN contacts c ON m.contact_id = c.contact_id @@ -2521,16 +2522,19 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember rm.group_member_id, rm.group_id, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.image, rp.contact_link, rp.local_alias, rp.preferences, + rm.created_at, rm.updated_at, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, - dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences + dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences, + dbm.created_at, dbm.updated_at FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN group_members m ON m.group_member_id = i.group_member_id diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 041adbec1d..0adc91d0d4 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -42,9 +42,11 @@ Query: mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at, -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id @@ -555,7 +557,8 @@ Query: -- GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences + p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN contacts c ON m.contact_id = c.contact_id @@ -704,16 +707,19 @@ Query: m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember rm.group_member_id, rm.group_id, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.image, rp.contact_link, rp.local_alias, rp.preferences, + rm.created_at, rm.updated_at, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, - dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences + dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences, + dbm.created_at, dbm.updated_at FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN group_members m ON m.group_member_id = i.group_member_id @@ -790,9 +796,11 @@ Query: mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at, -- via GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -829,7 +837,8 @@ Query: g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, - mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences + mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu USING (group_id) @@ -4415,7 +4424,8 @@ Query: -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id @@ -4436,7 +4446,8 @@ Query: -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id @@ -4452,6 +4463,7 @@ Query: SELECT m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -4483,6 +4495,7 @@ Query: SELECT m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -4506,6 +4519,7 @@ Query: SELECT m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -4529,6 +4543,7 @@ Query: SELECT m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -4552,6 +4567,7 @@ Query: SELECT m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -4575,6 +4591,7 @@ Query: SELECT m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index a28f4251f9..a556fd0a49 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -579,7 +579,7 @@ type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe Member type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64) :. GroupMemberRow -type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) +type GroupMemberRow = (Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) :. (UTCTime, UTCTime) toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. businessRow :. (uiThemes, customData, chatItemTTL) :. userMemberRow) = @@ -591,7 +591,7 @@ toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, chatTags, chatItemTTL, uiThemes, customData} toGroupMember :: Int64 -> GroupMemberRow -> GroupMember -toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) = +toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences) :. (createdAt, updatedAt)) = let memberProfile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} memberSettings = GroupMemberSettings {showMessages} blockedByAdmin = maybe False mrsBlocked memberRestriction_ @@ -615,7 +615,8 @@ groupInfoQuery = -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 89f8c94d51..e2ee71636b 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -784,7 +784,9 @@ data GroupMember = GroupMember -- member chat protocol version range; if member has active connection, its version range is preferred; -- for membership current supportedChatVRange is set, it's not updated on protocol version increase in database, -- but it's correctly set on read (see toGroupInfo) - memberChatVRange :: VersionRangeChat + memberChatVRange :: VersionRangeChat, + createdAt :: UTCTime, + updatedAt :: UTCTime } deriving (Eq, Show) diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 6994a8528a..5bdc379cbf 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -87,6 +87,11 @@ mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup = superUsers, ownersGroup, directoryLog = Just $ ps "directory_service.log", + blockedWordsFile = Nothing, + blockedExtensionRules = Nothing, + nameSpellingFile = Nothing, + profileNameLimit = maxBound, + acceptAsObserver = Nothing, serviceName = "SimpleX-Directory", runCLI = False, searchResults = 3, diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 248d6ead47..03671d1d94 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -390,15 +390,15 @@ withTmpFiles = (removeDirectoryRecursive "tests/tmp") testChatN :: HasCallStack => ChatConfig -> ChatOpts -> [Profile] -> (HasCallStack => [TestCC] -> IO ()) -> TestParams -> IO () -testChatN cfg opts ps test params = do - tcs <- getTestCCs (zip ps [1 ..]) [] - test tcs - concurrentlyN_ $ map ( [TestCC] -> IO [TestCC] getTestCCs [] tcs = pure tcs getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat params cfg opts (show db) p <*> getTestCCs envs' tcs + entTests tcs = do + concurrentlyN_ $ map ( TestCC -> Int -> Expectation ( Date: Tue, 25 Feb 2025 14:05:49 +0400 Subject: [PATCH 09/24] core: communicate group join rejection (#5661) --- src/Simplex/Chat/Controller.hs | 12 ++++ src/Simplex/Chat/Library/Commands.hs | 3 +- src/Simplex/Chat/Library/Internal.hs | 60 ++++++++++++++----- src/Simplex/Chat/Library/Subscriber.hs | 52 ++++++++++------ src/Simplex/Chat/Protocol.hs | 15 ++++- src/Simplex/Chat/Store/Groups.hs | 46 +++++++++----- .../SQLite/Migrations/chat_query_plans.txt | 19 +++++- src/Simplex/Chat/Types.hs | 51 +++++++++++++++- src/Simplex/Chat/View.hs | 7 ++- tests/ChatTests/Groups.hs | 30 ++++++++++ tests/ProtocolTests.hs | 8 +-- 11 files changed, 246 insertions(+), 57 deletions(-) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 5b1712b6b4..e64553af98 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -844,6 +844,12 @@ data ChatResponse | CRAppSettings {appSettings :: AppSettings} | CRTimedAction {action :: String, durationMilliseconds :: Int64} | CRCustomChatResponse {user_ :: Maybe User, response :: Text} + | CRTerminalEvent TerminalEvent + deriving (Show) + +data TerminalEvent + = TEGroupLinkRejected {user :: User, groupInfo :: GroupInfo, groupRejectionReason :: GroupRejectionReason} + | TERejectingGroupJoinRequestMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, groupRejectionReason :: GroupRejectionReason} deriving (Show) data DeletedRcvQueue = DeletedRcvQueue @@ -1491,6 +1497,10 @@ chatCmdError user = CRChatCmdError user . ChatError . CECommandError throwChatError :: ChatErrorType -> CM a throwChatError = throwError . ChatError +toViewTE :: TerminalEvent -> CM () +toViewTE = toView . CRTerminalEvent +{-# INLINE toViewTE #-} + -- | Emit local events. toView :: ChatResponse -> CM () toView = lift . toView' @@ -1630,6 +1640,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCSR") ''RemoteCtrlStopReason) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHSR") ''RemoteHostStopReason) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "TE") ''TerminalEvent) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse) $(JQ.deriveFromJSON defaultJSON ''ArchiveConfig) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 23f4406b9c..624f6a6dc1 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2621,7 +2621,7 @@ processChatCommand' vr = \case contactMember :: Contact -> [GroupMember] -> Maybe GroupMember contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> - cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft + cId == Just contactId && s /= GSMemRejected && s /= GSMemRemoved && s /= GSMemLeft checkSndFile :: CryptoFile -> CM Integer checkSndFile (CryptoFile f cfArgs) = do fsFilePath <- lift $ toFSFilePath f @@ -2969,6 +2969,7 @@ processChatCommand' vr = \case (Just gInfo, _) -> groupPlan gInfo where groupPlan gInfo@GroupInfo {membership} + | memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo) | not (memberActive membership) && not (memberRemoved membership) = pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo) | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo) diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 3e96e1e496..4a62c4ccb6 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -218,10 +218,11 @@ prepareGroupMsg db user g@GroupInfo {membership} mc mentions quotedItemId_ itemF updatedMentionNames :: MsgContent -> Maybe MarkdownList -> Map MemberName CIMention -> (MsgContent, Maybe MarkdownList, Map MemberName CIMention) updatedMentionNames mc ft_ mentions = case ft_ of - Just ft | not (null ft) && not (null mentions) && not (all sameName $ M.assocs mentions) -> - let (mentions', ft') = mapAccumL update M.empty ft - text = T.concat $ map markdownText ft' - in (mc {text} :: MsgContent, Just ft', mentions') + Just ft + | not (null ft) && not (null mentions) && not (all sameName $ M.assocs mentions) -> + let (mentions', ft') = mapAccumL update M.empty ft + text = T.concat $ map markdownText ft' + in (mc {text} :: MsgContent, Just ft', mentions') _ -> (mc, ft_, mentions) where sameName (name, CIMention {memberRef}) = case memberRef of @@ -261,9 +262,10 @@ getCIMentions db user GroupInfo {groupId} ft_ mentions = case ft_ of getRcvCIMentions :: DB.Connection -> User -> GroupInfo -> Maybe MarkdownList -> Map MemberName MsgMention -> IO (Map MemberName CIMention) getRcvCIMentions db user GroupInfo {groupId} ft_ mentions = case ft_ of - Just ft | not (null ft) && not (null mentions) -> - let mentions' = uniqueMsgMentions maxRcvMentions mentions $ mentionedNames ft - in mapM (getMentionedMemberByMemberId db user groupId) mentions' + Just ft + | not (null ft) && not (null mentions) -> + let mentions' = uniqueMsgMentions maxRcvMentions mentions $ mentionedNames ft + in mapM (getMentionedMemberByMemberId db user groupId) mentions' _ -> pure M.empty -- prevent "invisible" and repeated-with-different-name mentions @@ -274,8 +276,9 @@ uniqueMsgMentions maxMentions mentions = go M.empty S.empty 0 go acc seen n (name : rest) | n >= maxMentions = acc | otherwise = case M.lookup name mentions of - Just mm@MsgMention {memberId} | S.notMember memberId seen -> - go (M.insert name mm acc) (S.insert memberId seen) (n + 1) rest + Just mm@MsgMention {memberId} + | S.notMember memberId seen -> + go (M.insert name mm acc) (S.insert memberId seen) (n + 1) rest _ -> go acc seen n rest getMessageMentions :: DB.Connection -> User -> GroupId -> Text -> IO (Map MemberName GroupMemberId) @@ -827,7 +830,7 @@ acceptGroupJoinRequestAsync gVar <- asks random (groupMemberId, memberId) <- withStore $ \db -> do liftIO $ deleteContactRequestRec db user ucr - createAcceptedMember db gVar user gInfo ucr gLinkMemRole + createJoiningMember db gVar user gInfo ucr gLinkMemRole GSMemAccepted currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo let Profile {displayName} = profileToSendOnAccept user incognitoProfile True GroupMember {memberRole = userRole, memberId = userMemberId} = membership @@ -846,7 +849,34 @@ acceptGroupJoinRequestAsync let chatV = vr `peerConnChatVersion` cReqChatVRange connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV withStore $ \db -> do - liftIO $ createAcceptedMemberConnection db user connIds chatV ucr groupMemberId subMode + liftIO $ createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode + getGroupMemberById db vr user groupMemberId + +acceptGroupJoinSendRejectAsync :: User -> GroupInfo -> UserContactRequest -> GroupRejectionReason -> CM GroupMember +acceptGroupJoinSendRejectAsync + user + gInfo@GroupInfo {groupProfile, membership} + ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange} + rejectionReason = do + gVar <- asks random + (groupMemberId, memberId) <- withStore $ \db -> do + liftIO $ deleteContactRequestRec db user ucr + createJoiningMember db gVar user gInfo ucr GRObserver GSMemRejected + let GroupMember {memberRole = userRole, memberId = userMemberId} = membership + msg = + XGrpLinkReject $ + GroupLinkRejection + { fromMember = MemberIdRole userMemberId userRole, + invitedMember = MemberIdRole memberId GRObserver, + groupProfile, + rejectionReason + } + subMode <- chatReadVar subscriptionMode + vr <- chatVersionRange + let chatV = vr `peerConnChatVersion` cReqChatVRange + connIds <- agentAcceptContactAsync user False invId msg subMode PQSupportOff chatV + withStore $ \db -> do + liftIO $ createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode getGroupMemberById db vr user groupMemberId acceptBusinessJoinRequestAsync :: User -> UserContactRequest -> CM GroupInfo @@ -879,7 +909,7 @@ acceptBusinessJoinRequestAsync subMode <- chatReadVar subscriptionMode let chatV = vr `peerConnChatVersion` cReqChatVRange connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV - withStore' $ \db -> createAcceptedMemberConnection db user connIds chatV ucr groupMemberId subMode + withStore' $ \db -> createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode let cd = CDGroupSnd gInfo createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing createGroupFeatureItems user cd CISndGroupFeature gInfo @@ -1514,7 +1544,7 @@ sendGroupMessages_ _user gInfo@GroupInfo {groupId} members events = do (subtract 1 <$> memIdx_,) $ snd $ foldr' addBody (lastRef, memIdsReqs) mbs where addBody :: Either ChatError a -> (Int, ([GroupMemberId], [Either ChatError ChatMsgReq])) -> (Int, ([GroupMemberId], [Either ChatError ChatMsgReq])) - addBody mb (i, (memIds, reqs)) = + addBody mb (i, (memIds, reqs)) = let req = (conn,msgFlags,) . mkMb memIdx_ i <$> mb in (i - 1, (groupMemberId : memIds, req : reqs)) sndMessageMBR :: Maybe Int -> Int -> SndMessage -> (ValueOrRef MsgBody, [MessageId]) @@ -1542,10 +1572,10 @@ sendGroupMessages_ _user gInfo@GroupInfo {groupId} members events = do data MemberSendAction = MSASend Connection | MSASendBatched Connection | MSAPending | MSAForwarded memberSendAction :: GroupInfo -> NonEmpty (ChatMsgEvent e) -> [GroupMember] -> GroupMember -> Maybe MemberSendAction -memberSendAction gInfo events members m@GroupMember {memberRole} = case memberConn m of +memberSendAction gInfo events members m@GroupMember {memberRole, memberStatus} = case memberConn m of Nothing -> pendingOrForwarded Just conn@Connection {connStatus} - | connDisabled conn || connStatus == ConnDeleted -> Nothing + | connDisabled conn || connStatus == ConnDeleted || memberStatus == GSMemRejected -> Nothing | connInactive conn -> Just MSAPending | connStatus == ConnSndReady || connStatus == ConnReady -> sendBatchedOrSeparate conn | otherwise -> pendingOrForwarded diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 120e74cb7a..44ad4ccc85 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -22,7 +22,6 @@ import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader import qualified Data.Aeson as J -import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB import Data.Either (lefts, partitionEithers, rights) @@ -764,11 +763,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO update member profile pure () | otherwise -> messageError "x.grp.mem.info: memberId is different from expected" - XInfo _ -> pure () -- sent when connecting via group link + -- sent when connecting via group link + XInfo _ -> + -- TODO [group rejection] Keep rejected member record and connection for ability to start dialogue. + when (memberStatus m == GSMemRejected) $ do + deleteMemberConnection' user m True + withStore' $ \db -> deleteGroupMember db user m XOk -> pure () _ -> messageError "INFO from member must have x.grp.mem.info, x.info or x.ok" pure () - CON _pqEnc -> do + CON _pqEnc -> unless (memberStatus m == GSMemRejected) $ do withStore' $ \db -> do updateGroupMemberStatus db userId m GSMemConnected unless (memberActive membership) $ @@ -1291,8 +1295,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> CM () profileContactRequest invId chatVRange p@Profile {displayName, image} xContactId_ reqPQSup = do - cfg <- asks config - withAllowedName cfg $ withStore (\db -> createOrUpdateContactRequest db vr user userContactLinkId invId chatVRange p xContactId_ reqPQSup) >>= \case + withStore (\db -> createOrUpdateContactRequest db vr user userContactLinkId invId chatVRange p xContactId_ reqPQSup) >>= \case CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact CORGroup gInfo -> toView $ CRBusinessRequestAlreadyAccepted user gInfo CORRequest cReq -> do @@ -1318,19 +1321,29 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CRAcceptingContactRequest user ct Just groupId -> do gInfo <- withStore $ \db -> getGroupInfo db vr user groupId - let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo - if v >= groupFastLinkJoinVersion - then do - let useRole = userMemberRole gLinkMemRole $ acceptAsObserver cfg - mem <- acceptGroupJoinRequestAsync user gInfo cReq useRole profileMode - createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing - toView $ CRAcceptingGroupJoinRequestMember user gInfo mem - else messageError "processUserContactRequest: chat version range incompatible for accepting group join request" + cfg <- asks config + case rejectionReason cfg of + Nothing + | v < groupFastLinkJoinVersion -> + messageError "processUserContactRequest: chat version range incompatible for accepting group join request" + | otherwise -> do + let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo + useRole = userMemberRole gLinkMemRole $ acceptAsObserver cfg + mem <- acceptGroupJoinRequestAsync user gInfo cReq useRole profileMode + createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing + toView $ CRAcceptingGroupJoinRequestMember user gInfo mem + Just rjctReason + | v < groupJoinRejectVersion -> + messageWarning $ "processUserContactRequest (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked" + | otherwise -> do + mem <- acceptGroupJoinSendRejectAsync user gInfo cReq rjctReason + toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason _ -> toView $ CRReceivedContactRequest user cReq where - withAllowedName ChatConfig {profileNameLimit, allowedProfileName} action - | T.length displayName <= profileNameLimit && maybe True ($ displayName) allowedProfileName = action - | otherwise = liftIO $ putStrLn $ "Joining of " <> T.unpack displayName <> " is blocked" -- TODO send response, maybe event to UI? + rejectionReason ChatConfig {profileNameLimit, allowedProfileName} + | T.length displayName > profileNameLimit = Just GRRLongName + | maybe False (\f -> not $ f displayName) allowedProfileName = Just GRRBlockedName + | otherwise = Nothing userMemberRole linkRole = \case Just AOAll -> GRObserver Just AONameOnly | noImage -> GRObserver @@ -2475,6 +2488,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db vr user conn' glInv toView $ CRGroupLinkConnecting user gInfo host pure (conn', True) + XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do + (gInfo, host) <- withStore $ \db -> createGroupRejectedViaLink db vr user conn' glRjct + toView $ CRGroupLinkConnecting user gInfo host + toViewTE $ TEGroupLinkRejected user gInfo rejectionReason + pure (conn', True) -- TODO show/log error, other events in SMP confirmation _ -> pure (conn', False) @@ -2828,7 +2846,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = createUnknownMember :: GroupInfo -> MemberId -> CM GroupMember createUnknownMember gInfo memberId = do - let name = T.take 7 . safeDecodeUtf8 . B64.encode . unMemberId $ memberId + let name = nameFromMemberId memberId withStore $ \db -> createNewUnknownGroupMember db vr user gInfo memberId name directMsgReceived :: Contact -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> CM () diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index c19e89a776..94e08a0897 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -74,12 +74,13 @@ import Simplex.Messaging.Version hiding (version) -- 10 - business chats (2024-11-29) -- 11 - fix profile update in business chats (2024-12-05) -- 12 - support sending and receiving content reports (2025-01-03) +-- 14 - support sending and receiving group join rejection (2025-02-24) -- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig. -- This indirection is needed for backward/forward compatibility testing. -- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code. currentChatVersion :: VersionChat -currentChatVersion = VersionChat 12 +currentChatVersion = VersionChat 14 -- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above) supportedChatVRange :: VersionRangeChat @@ -130,6 +131,10 @@ businessChatPrefsVersion = VersionChat 11 contentReportsVersion :: VersionChat contentReportsVersion = VersionChat 12 +-- support sending and receiving group join rejection (XGrpLinkReject) +groupJoinRejectVersion :: VersionChat +groupJoinRejectVersion = VersionChat 14 + agentToChatVersion :: VersionSMPA -> VersionChat agentToChatVersion v | v < pqdrSMPAgentVersion = initialChatVersion @@ -326,7 +331,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json XGrpAcpt :: MemberId -> ChatMsgEvent 'Json XGrpLinkInv :: GroupLinkInvitation -> ChatMsgEvent 'Json - -- XGrpLinkReject :: GroupProfile -> RejectionReason -> ChatMsgEvent 'Json + XGrpLinkReject :: GroupLinkRejection -> ChatMsgEvent 'Json XGrpLinkMem :: Profile -> ChatMsgEvent 'Json XGrpMemNew :: MemberInfo -> ChatMsgEvent 'Json XGrpMemIntro :: MemberInfo -> Maybe MemberRestrictions -> ChatMsgEvent 'Json @@ -816,6 +821,7 @@ data CMEventTag (e :: MsgEncoding) where XGrpInv_ :: CMEventTag 'Json XGrpAcpt_ :: CMEventTag 'Json XGrpLinkInv_ :: CMEventTag 'Json + XGrpLinkReject_ :: CMEventTag 'Json XGrpLinkMem_ :: CMEventTag 'Json XGrpMemNew_ :: CMEventTag 'Json XGrpMemIntro_ :: CMEventTag 'Json @@ -867,6 +873,7 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XGrpInv_ -> "x.grp.inv" XGrpAcpt_ -> "x.grp.acpt" XGrpLinkInv_ -> "x.grp.link.inv" + XGrpLinkReject_ -> "x.grp.link.reject" XGrpLinkMem_ -> "x.grp.link.mem" XGrpMemNew_ -> "x.grp.mem.new" XGrpMemIntro_ -> "x.grp.mem.intro" @@ -919,6 +926,7 @@ instance StrEncoding ACMEventTag where "x.grp.inv" -> XGrpInv_ "x.grp.acpt" -> XGrpAcpt_ "x.grp.link.inv" -> XGrpLinkInv_ + "x.grp.link.reject" -> XGrpLinkReject_ "x.grp.link.mem" -> XGrpLinkMem_ "x.grp.mem.new" -> XGrpMemNew_ "x.grp.mem.intro" -> XGrpMemIntro_ @@ -967,6 +975,7 @@ toCMEventTag msg = case msg of XGrpInv _ -> XGrpInv_ XGrpAcpt _ -> XGrpAcpt_ XGrpLinkInv _ -> XGrpLinkInv_ + XGrpLinkReject _ -> XGrpLinkReject_ XGrpLinkMem _ -> XGrpLinkMem_ XGrpMemNew _ -> XGrpMemNew_ XGrpMemIntro _ _ -> XGrpMemIntro_ @@ -1068,6 +1077,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpInv_ -> XGrpInv <$> p "groupInvitation" XGrpAcpt_ -> XGrpAcpt <$> p "memberId" XGrpLinkInv_ -> XGrpLinkInv <$> p "groupLinkInvitation" + XGrpLinkReject_ -> XGrpLinkReject <$> p "groupLinkRejection" XGrpLinkMem_ -> XGrpLinkMem <$> p "profile" XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo" XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo" <*> opt "memberRestrictions" @@ -1130,6 +1140,7 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @ XGrpInv groupInv -> o ["groupInvitation" .= groupInv] XGrpAcpt memId -> o ["memberId" .= memId] XGrpLinkInv groupLinkInv -> o ["groupLinkInvitation" .= groupLinkInv] + XGrpLinkReject groupLinkRjct -> o ["groupLinkRejection" .= groupLinkRjct] XGrpLinkMem profile -> o ["profile" .= profile] XGrpMemNew memInfo -> o ["memberInfo" .= memInfo] XGrpMemIntro memInfo memRestrictions -> o $ ("memberRestrictions" .=? memRestrictions) ["memberInfo" .= memInfo] diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index bddbdbcce5..67722ebd0f 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -33,6 +33,7 @@ module Simplex.Chat.Store.Groups createGroupInvitation, deleteContactCardKeepConn, createGroupInvitedViaLink, + createGroupRejectedViaLink, setViaGroupLinkHash, setGroupInvitationChatItemId, getGroup, @@ -67,8 +68,8 @@ module Simplex.Chat.Store.Groups getGroupInvitation, createNewContactMember, createNewContactMemberAsync, - createAcceptedMember, - createAcceptedMemberConnection, + createJoiningMember, + createJoiningMemberConnection, createBusinessRequestGroup, getContactViaMember, setNewContactMemberConnRequest, @@ -519,18 +520,33 @@ deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile { DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId) createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) -createGroupInvitedViaLink +createGroupInvitedViaLink db vr user conn GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, business} = do + let fromMemberProfile = profileFromName fromMemberName + createGroupViaLink' db vr user conn fromMember fromMemberProfile invitedMember groupProfile business GSMemAccepted + +createGroupRejectedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupRejectedViaLink db vr user conn GroupLinkRejection {fromMember = fromMember@MemberIdRole {memberId}, invitedMember, groupProfile} = do + let fromMemberProfile = profileFromName $ nameFromMemberId memberId + createGroupViaLink' db vr user conn fromMember fromMemberProfile invitedMember groupProfile Nothing GSMemRejected + +createGroupViaLink' :: DB.Connection -> VersionRangeChat -> User -> Connection -> MemberIdRole -> Profile -> MemberIdRole -> GroupProfile -> Maybe BusinessChatInfo -> GroupMemberStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupViaLink' db vr user@User {userId, userContactId} Connection {connId, customUserProfileId} - GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, business} = do + fromMember + fromMemberProfile + invitedMember + groupProfile + business + memStatus = do currentTs <- liftIO getCurrentTime groupId <- insertGroup_ currentTs hostMemberId <- insertHost_ currentTs groupId liftIO $ DB.execute db "UPDATE connections SET conn_type = ?, group_member_id = ?, updated_at = ? WHERE connection_id = ?" (ConnMember, hostMemberId, currentTs, connId) -- using IBUnknown since host is created without contact - void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember GSMemAccepted IBUnknown customUserProfileId currentTs vr + void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember memStatus IBUnknown customUserProfileId currentTs vr liftIO $ setViaGroupLinkHash db groupId connId (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user hostMemberId where @@ -554,7 +570,6 @@ createGroupInvitedViaLink ((profileId, localDisplayName, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. businessChatInfoRow business) insertedRowId db insertHost_ currentTs groupId = do - let fromMemberProfile = profileFromName fromMemberName (localDisplayName, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs let MemberIdRole {memberId, memberRole} = fromMember liftIO $ do @@ -566,7 +581,7 @@ createGroupInvitedViaLink user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (groupId, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown) + ( (groupId, memberId, memberRole, GCHostMember, memStatus, fromInvitedBy userContactId IBUnknown) :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) ) insertedRowId db @@ -783,9 +798,9 @@ getGroupSummary db User {userId} groupId = do JOIN group_members m USING (group_id) WHERE g.user_id = ? AND g.group_id = ? - AND m.member_status NOT IN (?,?,?,?) + AND m.member_status NOT IN (?,?,?,?,?) |] - (userId, groupId, GSMemRemoved, GSMemLeft, GSMemUnknown, GSMemInvited) + (userId, groupId, GSMemRejected, GSMemRemoved, GSMemLeft, GSMemUnknown, GSMemInvited) pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_} getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [(GroupMemberRole, FullGroupPreferences)] @@ -1026,14 +1041,15 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo :. (minV, maxV) ) -createAcceptedMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> ExceptT StoreError IO (GroupMemberId, MemberId) -createAcceptedMember +createJoiningMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> GroupMemberStatus -> ExceptT StoreError IO (GroupMemberId, MemberId) +createJoiningMember db gVar User {userId, userContactId} GroupInfo {groupId, membership} UserContactRequest {cReqChatVRange, localDisplayName, profileId} - memberRole = + memberRole + memberStatus = createWithRandomId gVar $ \memId -> do createdAt <- liftIO getCurrentTime insertMember_ (MemberId memId) createdAt @@ -1051,13 +1067,13 @@ createAcceptedMember peer_chat_min_version, peer_chat_max_version) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (groupId, memberId, memberRole, GCInviteeMember, GSMemAccepted, fromInvitedBy userContactId IBUser, groupMemberId' membership) + ( (groupId, memberId, memberRole, GCInviteeMember, memberStatus, fromInvitedBy userContactId IBUser, groupMemberId' membership) :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, createdAt, createdAt) :. (minV, maxV) ) -createAcceptedMemberConnection :: DB.Connection -> User -> (CommandId, ConnId) -> VersionChat -> UserContactRequest -> GroupMemberId -> SubscriptionMode -> IO () -createAcceptedMemberConnection +createJoiningMemberConnection :: DB.Connection -> User -> (CommandId, ConnId) -> VersionChat -> UserContactRequest -> GroupMemberId -> SubscriptionMode -> IO () +createJoiningMemberConnection db user@User {userId} (cmdId, agentConnId) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 0adc91d0d4..72f8e4b8fd 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -1012,7 +1012,7 @@ Query: JOIN group_members m USING (group_id) WHERE g.user_id = ? AND g.group_id = ? - AND m.member_status NOT IN (?,?,?,?) + AND m.member_status NOT IN (?,?,?,?,?) Plan: SEARCH g USING INTEGER PRIMARY KEY (rowid=?) @@ -5025,6 +5025,15 @@ SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=? SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) +Query: DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ? +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) +SEARCH connections USING COVERING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (member_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) + Query: DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ? Plan: SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) @@ -5496,6 +5505,10 @@ Query: SELECT count(1) FROM chat_items WHERE chat_item_id > ? Plan: SEARCH chat_items USING INTEGER PRIMARY KEY (rowid>?) +Query: SELECT count(1) FROM group_members +Plan: +SCAN group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id + Query: SELECT count(1) FROM pending_group_messages Plan: SCAN pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id @@ -5532,6 +5545,10 @@ Query: SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contac Plan: SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) +Query: SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1 +Plan: +SEARCH group_members USING INDEX idx_group_members_user_id (user_id=?) + Query: SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND local_display_name = ? Plan: SEARCH group_members USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index e2ee71636b..94b67d8349 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -26,14 +26,17 @@ module Simplex.Chat.Types where +import Control.Applicative ((<|>)) import Crypto.Number.Serialize (os2ip) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString, pack, unpack) import qualified Data.ByteString.Lazy as LB +import Data.Functor (($>)) import Data.Int (Int64) import Data.Maybe (isJust) import Data.Text (Text) @@ -670,6 +673,41 @@ data GroupLinkInvitation = GroupLinkInvitation } deriving (Eq, Show) +data GroupLinkRejection = GroupLinkRejection + { fromMember :: MemberIdRole, + invitedMember :: MemberIdRole, + groupProfile :: GroupProfile, + rejectionReason :: GroupRejectionReason + } + deriving (Eq, Show) + +data GroupRejectionReason + = GRRLongName + | GRRBlockedName + | GRRUnknown {text :: Text} + deriving (Eq, Show) + +instance FromField GroupRejectionReason where fromField = blobFieldDecoder strDecode + +instance ToField GroupRejectionReason where toField = toField . strEncode + +instance StrEncoding GroupRejectionReason where + strEncode = \case + GRRLongName -> "long_name" + GRRBlockedName -> "blocked_name" + GRRUnknown text -> encodeUtf8 text + strP = + "long_name" $> GRRLongName + <|> "blocked_name" $> GRRBlockedName + <|> GRRUnknown . safeDecodeUtf8 <$> A.takeByteString + +instance FromJSON GroupRejectionReason where + parseJSON = strParseJSON "GroupRejectionReason" + +instance ToJSON GroupRejectionReason where + toJSON = strToJSON + toEncoding = strToJEncoding + data MemberIdRole = MemberIdRole { memberId :: MemberId, memberRole :: GroupMemberRole @@ -862,6 +900,9 @@ instance ToJSON MemberId where toJSON = strToJSON toEncoding = strToJEncoding +nameFromMemberId :: MemberId -> ContactName +nameFromMemberId = T.take 7 . safeDecodeUtf8 . B64.encode . unMemberId + data InvitedBy = IBContact {byContactId :: Int64} | IBUser | IBUnknown deriving (Eq, Show) @@ -950,7 +991,8 @@ instance TextEncoding GroupMemberCategory where GCPostMember -> "post" data GroupMemberStatus - = GSMemRemoved -- member who was removed from the group + = GSMemRejected -- joining member who was rejected by the host, or host that rejected the join + | GSMemRemoved -- member who was removed from the group | GSMemLeft -- member who left the group | GSMemGroupDeleted -- user member of the deleted group | GSMemUnknown -- unknown member, whose message was forwarded by an admin (likely member wasn't introduced due to not being a current member, but message was included in history) @@ -977,6 +1019,7 @@ instance ToJSON GroupMemberStatus where memberActive :: GroupMember -> Bool memberActive m = case memberStatus m of + GSMemRejected -> False GSMemRemoved -> False GSMemLeft -> False GSMemGroupDeleted -> False @@ -996,6 +1039,7 @@ memberCurrent = memberCurrent' . memberStatus -- update getGroupSummary if this is changed memberCurrent' :: GroupMemberStatus -> Bool memberCurrent' = \case + GSMemRejected -> False GSMemRemoved -> False GSMemLeft -> False GSMemGroupDeleted -> False @@ -1011,6 +1055,7 @@ memberCurrent' = \case memberRemoved :: GroupMember -> Bool memberRemoved m = case memberStatus m of + GSMemRejected -> True GSMemRemoved -> True GSMemLeft -> True GSMemGroupDeleted -> True @@ -1026,6 +1071,7 @@ memberRemoved m = case memberStatus m of instance TextEncoding GroupMemberStatus where textDecode = \case + "rejected" -> Just GSMemRejected "removed" -> Just GSMemRemoved "left" -> Just GSMemLeft "deleted" -> Just GSMemGroupDeleted @@ -1040,6 +1086,7 @@ instance TextEncoding GroupMemberStatus where "creator" -> Just GSMemCreator _ -> Nothing textEncode = \case + GSMemRejected -> "rejected" GSMemRemoved -> "removed" GSMemLeft -> "left" GSMemGroupDeleted -> "deleted" @@ -1793,6 +1840,8 @@ $(JQ.deriveJSON defaultJSON ''GroupInvitation) $(JQ.deriveJSON defaultJSON ''GroupLinkInvitation) +$(JQ.deriveJSON defaultJSON ''GroupLinkRejection) + $(JQ.deriveJSON defaultJSON ''IntroInvitation) $(JQ.deriveJSON defaultJSON ''MemberRestrictions) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 69f9799948..a8c2d215a8 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -444,6 +444,9 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRAppSettings as -> ["app settings: " <> viewJSON as] CRTimedAction _ _ -> [] CRCustomChatResponse u r -> ttyUser' u $ map plain $ T.lines r + CRTerminalEvent te -> case te of + TERejectingGroupJoinRequestMember _ g m reason -> [ttyFullMember m <> ": rejecting request to join group " <> ttyGroup' g <> ", reason: " <> sShow reason] + TEGroupLinkRejected u g reason -> ttyUser u [ttyGroup' g <> ": join rejected, reason: " <> sShow reason] where ttyUser :: User -> [StyledString] -> [StyledString] ttyUser user@User {showNtfs, activeUser, viewPwdHash} ss @@ -1128,7 +1131,7 @@ showRole = plain . strEncode viewGroupMembers :: Group -> [StyledString] viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filter (not . removedOrLeft) $ membership : members where - removedOrLeft m = let s = memberStatus m in s == GSMemRemoved || s == GSMemLeft + removedOrLeft m = let s = memberStatus m in s == GSMemRejected || s == GSMemRemoved || s == GSMemLeft groupMember m = memIncognito m <> ttyFullMember m <> ": " <> plain (intercalate ", " $ [role m] <> category m <> status m <> muted m) role :: GroupMember -> String role GroupMember {memberRole} = B.unpack $ strEncode memberRole @@ -1138,6 +1141,7 @@ viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filt GCHostMember -> ["host"] _ -> [] status m = case memberStatus m of + GSMemRejected -> ["rejected"] GSMemRemoved -> ["removed"] GSMemLeft -> ["left"] GSMemUnknown -> ["status unknown"] @@ -1178,6 +1182,7 @@ viewGroupsList gs = map groupSS $ sortOn (ldn_ . fst) gs s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s <> alias g where viewMemberStatus = \case + GSMemRejected -> delete "you are rejected" GSMemRemoved -> delete "you are removed" GSMemLeft -> delete "you left" GSMemGroupDeleted -> delete "group deleted" diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index bdb3b443b3..6625fb8094 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -98,6 +98,7 @@ chatGroupTests = do it "group link member role" testGroupLinkMemberRole it "host profile received" testGroupLinkHostProfileReceived it "existing contact merged" testGroupLinkExistingContactMerged + it "reject member joining via group link - blocked name" testGroupLinkRejectBlockedName describe "group link connection plan" $ do it "ok to connect; known group" testPlanGroupLinkKnown it "own group link" testPlanGroupLinkOwn @@ -2871,6 +2872,35 @@ testGroupLinkExistingContactMerged = bob #> "#team hi there" alice <# "#team bob> hi there" +testGroupLinkRejectBlockedName :: HasCallStack => TestParams -> IO () +testGroupLinkRejectBlockedName = + testChatCfg2 cfg aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + bob ##> ("/c " <> gLink) + bob <## "connection request sent!" + alice <## "bob (Bob): rejecting request to join group #team, reason: GRRBlockedName" + bob <## "#team: joining the group..." + bob <## "#team: join rejected, reason: GRRBlockedName" + + threadDelay 100000 + + alice `hasContactProfiles` ["alice"] + memCount <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM group_members" :: IO [[Int]] + memCount `shouldBe` [[1]] + + bob ##> ("/c " <> gLink) + bob <## "group link: known group #team" + bob <## "use #team to send messages" + where + cfg = testCfg {allowedProfileName = Just (const False)} + testPlanGroupLinkKnown :: HasCallStack => TestParams -> IO () testPlanGroupLinkKnown = testChat2 aliceProfile bobProfile $ diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index cb293895a9..83087a89a8 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -133,7 +133,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new chat message with chat version range" $ - "{\"v\":\"1-12\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1-14\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new quote" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" @@ -249,13 +249,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} it "x.grp.mem.new with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-12\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-14\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} it "x.grp.mem.intro" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing it "x.grp.mem.intro with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-12\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-14\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing it "x.grp.mem.intro with member restrictions" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" @@ -270,7 +270,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-12\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-14\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" From 1fcb352db471b583cafe9e94e8adc65b53878765 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 25 Feb 2025 19:15:56 +0400 Subject: [PATCH 10/24] ui: rejected group previews (#5665) --- apps/ios/Shared/Views/ChatList/ChatPreviewView.swift | 4 +++- apps/ios/SimpleXChat/ChatTypes.swift | 5 +++++ .../commonMain/kotlin/chat/simplex/common/model/ChatModel.kt | 5 +++++ .../chat/simplex/common/views/chatlist/ChatPreviewView.kt | 4 +++- .../common/src/commonMain/resources/MR/base/strings.xml | 2 ++ 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 6266395905..6969ae325c 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -143,6 +143,7 @@ struct ChatPreviewView: View { } case let .group(groupInfo): switch (groupInfo.membership.memberStatus) { + case .memRejected: inactiveIcon() case .memLeft: inactiveIcon() case .memRemoved: inactiveIcon() case .memGroupDeleted: inactiveIcon() @@ -168,7 +169,7 @@ struct ChatPreviewView: View { let v = previewTitle(t) switch (groupInfo.membership.memberStatus) { case .memInvited: v.foregroundColor(deleting ? theme.colors.secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary) - case .memAccepted: v.foregroundColor(theme.colors.secondary) + case .memAccepted, .memRejected: v.foregroundColor(theme.colors.secondary) default: if deleting { v.foregroundColor(theme.colors.secondary) } else { v } } default: previewTitle(t) @@ -336,6 +337,7 @@ struct ChatPreviewView: View { } case let .group(groupInfo): switch (groupInfo.membership.memberStatus) { + case .memRejected: chatPreviewInfoText("rejected") case .memInvited: groupInvitationPreviewText(groupInfo) case .memAccepted: chatPreviewInfoText("connecting…") default: EmptyView() diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 554207e90d..169a7c21de 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2141,6 +2141,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { public var memberActive: Bool { switch memberStatus { + case .memRejected: return false case .memRemoved: return false case .memLeft: return false case .memGroupDeleted: return false @@ -2158,6 +2159,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { public var memberCurrent: Bool { switch memberStatus { + case .memRejected: return false case .memRemoved: return false case .memLeft: return false case .memGroupDeleted: return false @@ -2288,6 +2290,7 @@ public enum GroupMemberCategory: String, Decodable, Hashable { } public enum GroupMemberStatus: String, Decodable, Hashable { + case memRejected = "rejected" case memRemoved = "removed" case memLeft = "left" case memGroupDeleted = "deleted" @@ -2303,6 +2306,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable { public var text: LocalizedStringKey { switch self { + case .memRejected: return "rejected" case .memRemoved: return "removed" case .memLeft: return "left" case .memGroupDeleted: return "group deleted" @@ -2320,6 +2324,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable { public var shortText: LocalizedStringKey { switch self { + case .memRejected: return "rejected" case .memRemoved: return "removed" case .memLeft: return "left" case .memGroupDeleted: return "group deleted" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 6424397531..7ef8123d2f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1910,6 +1910,7 @@ data class GroupMember ( } val memberActive: Boolean get() = when (this.memberStatus) { + GroupMemberStatus.MemRejected -> false GroupMemberStatus.MemRemoved -> false GroupMemberStatus.MemLeft -> false GroupMemberStatus.MemGroupDeleted -> false @@ -1925,6 +1926,7 @@ data class GroupMember ( } val memberCurrent: Boolean get() = when (this.memberStatus) { + GroupMemberStatus.MemRejected -> false GroupMemberStatus.MemRemoved -> false GroupMemberStatus.MemLeft -> false GroupMemberStatus.MemGroupDeleted -> false @@ -2028,6 +2030,7 @@ enum class GroupMemberCategory { @Serializable enum class GroupMemberStatus { + @SerialName("rejected") MemRejected, @SerialName("removed") MemRemoved, @SerialName("left") MemLeft, @SerialName("deleted") MemGroupDeleted, @@ -2042,6 +2045,7 @@ enum class GroupMemberStatus { @SerialName("creator") MemCreator; val text: String get() = when (this) { + MemRejected -> generalGetString(MR.strings.group_member_status_rejected) MemRemoved -> generalGetString(MR.strings.group_member_status_removed) MemLeft -> generalGetString(MR.strings.group_member_status_left) MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted) @@ -2057,6 +2061,7 @@ enum class GroupMemberStatus { } val shortText: String get() = when (this) { + MemRejected -> generalGetString(MR.strings.group_member_status_rejected) MemRemoved -> generalGetString(MR.strings.group_member_status_removed) MemLeft -> generalGetString(MR.strings.group_member_status_left) MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index d379131171..93d512507a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -70,6 +70,7 @@ fun ChatPreviewView( } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { + GroupMemberStatus.MemRejected -> inactiveIcon() GroupMemberStatus.MemLeft -> inactiveIcon() GroupMemberStatus.MemRemoved -> inactiveIcon() GroupMemberStatus.MemGroupDeleted -> inactiveIcon() @@ -156,7 +157,7 @@ fun ChatPreviewView( else if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary ) - GroupMemberStatus.MemAccepted -> chatPreviewTitleText(MaterialTheme.colors.secondary) + GroupMemberStatus.MemAccepted, GroupMemberStatus.MemRejected -> chatPreviewTitleText(MaterialTheme.colors.secondary) else -> chatPreviewTitleText( if (deleting) MaterialTheme.colors.secondary @@ -237,6 +238,7 @@ fun ChatPreviewView( } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { + GroupMemberStatus.MemRejected -> Text(stringResource(MR.strings.group_preview_rejected)) GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(currentUserProfileDisplayName, cInfo.groupInfo)) GroupMemberStatus.MemAccepted -> Text(stringResource(MR.strings.group_connection_pending), color = MaterialTheme.colors.secondary) else -> {} diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 1d445f710f..aa0f49ee1d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -418,6 +418,7 @@ send direct message you are invited to group join as %s + rejected connecting… Tap to start a new chat Chat with the developers @@ -1622,6 +1623,7 @@ owner + rejected removed left group deleted From b482d4d8127d064aab0bea6437c0d98ee33c4a11 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 27 Feb 2025 07:38:40 +0000 Subject: [PATCH 11/24] core: fix search in chat (#5677) --- src/Simplex/Chat/Store/Messages.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 22a3aadd34..1902fd002e 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -1260,7 +1260,7 @@ getGroupChatItemIDs db User {userId} GroupInfo {groupId} contentFilter range cou (orParams ts itemId) " ORDER BY item_ts DESC, chat_item_id DESC " where - orCond c1 c2 = " (" <> c <> " AND " <> c1 <> ") OR (" <> c <> " AND " <> c2 <> ") " + orCond c1 c2 = " ((" <> c <> " AND " <> c1 <> ") OR (" <> c <> " AND " <> c2 <> ")) " orParams ts itemId = (p :. (Only ts) :. p :. (ts, itemId)) rangeQuery :: ToRow p => Query -> p -> Query -> IO [ChatItemId] rangeQuery c p ob From fefddb3b5a6240df853a2bdecf8b767bfbc4ad91 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 28 Feb 2025 21:45:24 +0700 Subject: [PATCH 12/24] ios: go to forwarded item or search result (#5679) * ios: go to forwarded item or search result * react on touch * changes --- apps/ios/Shared/Model/ChatModel.swift | 17 ++ apps/ios/Shared/Model/SimpleXAPI.swift | 4 +- .../Shared/Views/Chat/ChatItemsLoader.swift | 24 ++- .../Shared/Views/Chat/ChatScrollHelpers.swift | 2 +- apps/ios/Shared/Views/Chat/ChatView.swift | 182 +++++++++++++----- .../Chat/ComposeMessage/ComposeView.swift | 4 + .../ComposeMessage/NativeTextEditor.swift | 9 +- .../Chat/ComposeMessage/SendMessageView.swift | 8 +- .../Shared/Views/Chat/EndlessScrollView.swift | 24 ++- apps/ios/Shared/Views/TerminalView.swift | 4 +- apps/ios/SimpleXChat/ChatTypes.swift | 14 ++ 11 files changed, 228 insertions(+), 64 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 9171bf5073..a1c5a55c3b 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -97,6 +97,22 @@ class ItemsModel: ObservableObject { } } } + + func loadOpenChatNoWait(_ chatId: ChatId, _ openAroundItemId: ChatItem.ID? = nil) { + navigationTimeoutTask?.cancel() + loadChatTask?.cancel() + loadChatTask = Task { + // try? await Task.sleep(nanoseconds: 1000_000000) + await loadChat(chatId: chatId, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil) + if !Task.isCancelled { + await MainActor.run { + if openAroundItemId == nil { + ChatModel.shared.chatId = chatId + } + } + } + } + } } class ChatTagsModel: ObservableObject { @@ -259,6 +275,7 @@ final class ChatModel: ObservableObject { @Published var deletedChats: Set = [] // current chat @Published var chatId: String? + @Published var openAroundItemId: ChatItem.ID? = nil var chatItemStatuses: Dictionary = [:] @Published var chatToTop: String? @Published var groupMembers: [GMember] = [] diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 0ef2a87aa7..354f8243fc 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -338,7 +338,7 @@ func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async { await loadChat(chatId: chat.chatInfo.id, search: search, clearItems: clearItems) } -func loadChat(chatId: ChatId, search: String = "", clearItems: Bool = true) async { +func loadChat(chatId: ChatId, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async { let m = ChatModel.shared let im = ItemsModel.shared m.chatItemStatuses = [:] @@ -348,7 +348,7 @@ func loadChat(chatId: ChatId, search: String = "", clearItems: Bool = true) asyn ItemsModel.shared.chatItemsChangesListener.cleared() } } - await apiLoadMessages(chatId, search == "" ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage), im.chatState, search, { 0...0 }) + await apiLoadMessages(chatId, openAroundItemId != nil ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage) : (search == "" ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)), im.chatState, search, openAroundItemId, { 0...0 }) } func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -> ChatItemInfo { diff --git a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift index 4253a4f1e4..add28cd7f9 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift @@ -16,6 +16,7 @@ func apiLoadMessages( _ pagination: ChatPagination, _ chatState: ActiveChatState, _ search: String = "", + _ openAroundItemId: ChatItem.ID? = nil, _ visibleItemIndexesNonReversed: @MainActor () -> ClosedRange = { 0 ... 0 } ) async { let chat: Chat @@ -32,7 +33,8 @@ func apiLoadMessages( // For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes let paginationIsInitial = switch pagination { case .initial: true; default: false } let paginationIsLast = switch pagination { case .last: true; default: false } - if ((chatModel.chatId != chat.id || chat.chatItems.isEmpty) && !paginationIsInitial && !paginationIsLast) || Task.isCancelled { + // When openAroundItemId is provided, chatId can be different too + if ((chatModel.chatId != chat.id || chat.chatItems.isEmpty) && !paginationIsInitial && !paginationIsLast && openAroundItemId == nil) || Task.isCancelled { return } @@ -102,8 +104,13 @@ func apiLoadMessages( } } case .around: - newItems.append(contentsOf: oldItems) - let newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, chatState.splits, visibleItemIndexesNonReversed) + let newSplits: [Int64] + if openAroundItemId == nil { + newItems.append(contentsOf: oldItems) + newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, chatState.splits, visibleItemIndexesNonReversed) + } else { + newSplits = [] + } // currently, items will always be added on top, which is index 0 newItems.insert(contentsOf: chat.chatItems, at: 0) let newReversed: [ChatItem] = newItems.reversed() @@ -114,8 +121,15 @@ func apiLoadMessages( chatState.totalAfter = navInfo.afterTotal chatState.unreadTotal = chat.chatStats.unreadCount chatState.unreadAfter = navInfo.afterUnread - // no need to set it, count will be wrong - // unreadAfterNewestLoaded.value = navInfo.afterUnread + + if let openAroundItemId { + chatState.unreadAfterNewestLoaded = navInfo.afterUnread + ChatModel.shared.openAroundItemId = openAroundItemId + ChatModel.shared.chatId = chatId + } else { + // no need to set it, count will be wrong + // chatState.unreadAfterNewestLoaded = navInfo.afterUnread + } } case .last: newItems.append(contentsOf: oldItems) diff --git a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift index 094c6e13e1..873f24d5c3 100644 --- a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift +++ b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift @@ -46,7 +46,7 @@ func preloadIfNeeded( loadItems: @escaping (Bool, ChatPagination) async -> Bool ) { let state = PreloadState.shared - guard !listState.isScrolling, + guard !listState.isScrolling && !listState.isAnimatedScrolling, state.prevFirstVisible != listState.firstVisibleItemIndex || state.prevItemsCount != mergedItems.boxedValue.indexInParentItems.count, !state.preloading, listState.totalItemsCount > 0 diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index f3bfa294b4..90c9629352 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -29,6 +29,7 @@ struct ChatView: View { @State private var composeState = ComposeState() @State private var selectedRange = NSRange() @State private var keyboardVisible = false + @State private var keyboardHiddenDate = Date.now @State private var connectionStats: ConnectionStats? @State private var customUserProfile: Profile? @State private var connectionCode: String? @@ -37,7 +38,7 @@ struct ChatView: View { @State private var requestedTopScroll = false @State private var loadingBottomItems = false @State private var requestedBottomScroll = false - @State private var searchMode = false + @State private var showSearch = false @State private var searchText: String = "" @FocusState private var searchFocussed // opening GroupMemberInfoView on member icon @@ -54,11 +55,8 @@ struct ChatView: View { @State private var allowLoadMoreItems: Bool = false @State private var ignoreLoadingRequests: Int64? = nil @State private var animatedScrollingInProgress: Bool = false - @State private var updateMergedItemsTask: Task? = nil @State private var floatingButtonModel: FloatingButtonModel = FloatingButtonModel() - private let useItemsUpdateTask = false - @State private var scrollView: EndlessScrollView = EndlessScrollView(frame: .zero) @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial @@ -101,6 +99,7 @@ struct ChatView: View { chat: chat, composeState: $composeState, keyboardVisible: $keyboardVisible, + keyboardHiddenDate: $keyboardHiddenDate, selectedRange: $selectedRange ) .disabled(!cInfo.sendMsgEnabled) @@ -131,7 +130,7 @@ struct ChatView: View { } .safeAreaInset(edge: .top) { VStack(spacing: .zero) { - if searchMode { searchToolbar() } + if showSearch { searchToolbar() } Divider() } .background(ToolbarMaterial.material(toolbarMaterial)) @@ -232,15 +231,45 @@ struct ChatView: View { scrollView.listState.onUpdateListener = onChatItemsUpdated initChatView() theme = buildTheme() - if let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() }) { + closeSearch() + mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + scrollView.updateItems(mergedItems.boxedValue.items) + + if let openAround = chatModel.openAroundItemId, let index = mergedItems.boxedValue.indexInParentItems[openAround] { + scrollView.scrollToItem(index) + } else if let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() }) { scrollView.scrollToItem(unreadIndex) } else { scrollView.scrollToBottom() } + if chatModel.openAroundItemId != nil { + chatModel.openAroundItemId = nil + } } else { dismiss() } } + .onChange(of: chatModel.openAroundItemId) { openAround in + if let openAround { + closeSearch() + mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + scrollView.updateItems(mergedItems.boxedValue.items) + chatModel.openAroundItemId = nil + + if let index = mergedItems.boxedValue.indexInParentItems[openAround] { + scrollView.scrollToItem(index) + } + + // this may already being loading because of changed chat id (see .onChange(of: chat.id) + if !loadingBottomItems { + loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) + allowLoadMoreItems = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + allowLoadMoreItems = true + } + } + } + } .onDisappear { VideoPlayerView.players.removeAll() stopAudioPlayer() @@ -429,9 +458,13 @@ struct ChatView: View { index = mergedItems.boxedValue.indexInParentItems[itemId] } if let index { - await MainActor.run { animatedScrollingInProgress = true } - await scrollView.scrollToItemAnimated(min(ItemsModel.shared.reversedChatItems.count - 1, index)) - await MainActor.run { animatedScrollingInProgress = false } + closeKeyboardAndRun { + Task { + await MainActor.run { animatedScrollingInProgress = true } + await scrollView.scrollToItemAnimated(min(ItemsModel.shared.reversedChatItems.count - 1, index)) + await MainActor.run { animatedScrollingInProgress = false } + } + } } } catch { logger.error("Error scrolling to item: \(error)") @@ -460,9 +493,7 @@ struct ChatView: View { .cornerRadius(10.0) Button ("Cancel") { - searchText = "" - searchMode = false - searchFocussed = false + closeSearch() Task { await loadChat(chat: chat) } } } @@ -517,7 +548,9 @@ struct ChatView: View { showChatInfoSheet: $showChatInfoSheet, revealedItems: $revealedItems, selectedChatItems: $selectedChatItems, - forwardedChatItems: $forwardedChatItems + forwardedChatItems: $forwardedChatItems, + searchText: $searchText, + closeKeyboardAndRun: closeKeyboardAndRun ) // crashes on Cell size calculation without this line .environmentObject(ChatModel.shared) @@ -535,25 +568,10 @@ struct ChatView: View { } } .onChange(of: im.reversedChatItems) { items in - updateMergedItemsTask?.cancel() - if useItemsUpdateTask { - updateMergedItemsTask = Task { - let items = MergedItems.create(items, revealedItems, im.chatState) - if Task.isCancelled { - return - } - await MainActor.run { - mergedItems.boxedValue = items - scrollView.updateItems(mergedItems.boxedValue.items) - } - } - } else { - mergedItems.boxedValue = MergedItems.create(items, revealedItems, im.chatState) - scrollView.updateItems(mergedItems.boxedValue.items) - } + mergedItems.boxedValue = MergedItems.create(items, revealedItems, im.chatState) + scrollView.updateItems(mergedItems.boxedValue.items) } .onChange(of: revealedItems) { revealed in - updateMergedItemsTask?.cancel() mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealed, im.chatState) scrollView.updateItems(mergedItems.boxedValue.items) } @@ -567,6 +585,7 @@ struct ChatView: View { .padding(.vertical, -100) .onTapGesture { hideKeyboard() } .onChange(of: searchText) { s in + guard showSearch else { return } Task { await loadChat(chat: chat, search: s) mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) @@ -880,11 +899,29 @@ struct ChatView: View { } private func focusSearch() { - searchMode = true + showSearch = true searchFocussed = true searchText = "" } + private func closeSearch() { + showSearch = false + searchText = "" + searchFocussed = false + } + + private func closeKeyboardAndRun(_ action: @escaping () -> Void) { + var delay: TimeInterval = 0 + if keyboardVisible || keyboardHiddenDate.timeIntervalSinceNow >= -1 || showSearch { + delay = 0.5 + closeSearch() + hideKeyboard() + } + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + action() + } + } + private func addMembersButton() -> some View { Button { if case let .group(gInfo) = chat.chatInfo { @@ -1079,6 +1116,7 @@ struct ChatView: View { pagination, im.chatState, searchText, + nil, { visibleItemIndexesNonReversed(scrollView.listState, mergedItems.boxedValue) } ) return true @@ -1136,10 +1174,14 @@ struct ChatView: View { @State private var showChatItemInfoSheet: Bool = false @State private var chatItemInfo: ChatItemInfo? @State private var msgWidth: CGFloat = 0 + @State private var touchInProgress: Bool = false @Binding var selectedChatItems: Set? @Binding var forwardedChatItems: [ChatItem] + @Binding var searchText: String + var closeKeyboardAndRun: (@escaping () -> Void) -> Void + @State private var allowMenu: Bool = true @State private var markedRead = false @State private var markReadTask: Task? = nil @@ -1257,6 +1299,16 @@ struct ChatView: View { markedRead = false } .actionSheet(item: $actionSheet) { $0.actionSheet } + // skip updating struct on touch if no need to show GoTo button + .if(touchInProgress || searchIsNotBlank || (chatItem.meta.itemForwarded != nil && chatItem.meta.itemForwarded != .unknown)) { + // long press listener steals taps from top-level listener, so repeating it's logic here as well + $0.onTapGesture { + hideKeyboard() + } + .onLongPressGesture(minimumDuration: .infinity, perform: {}, onPressingChanged: { pressing in + touchInProgress = pressing + }) + } } private func unreadItemIds(_ range: ClosedRange) -> ([ChatItem.ID], Int) { @@ -1290,6 +1342,11 @@ struct ChatView: View { } } + private var searchIsNotBlank: Bool { + get { + searchText.count > 0 && !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + } @available(iOS 16.0, *) struct MemberLayout: Layout { @@ -1459,18 +1516,26 @@ struct ChatView: View { @ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading VStack(alignment: alignment.horizontal, spacing: 3) { - ChatItemView( - chat: chat, - chatItem: ci, - scrollToItemId: scrollToItemId, - maxWidth: maxWidth, - allowMenu: $allowMenu - ) - .environment(\.revealed, revealed) - .environment(\.showTimestamp, itemSeparation.timestamp) - .modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap && (ci.meta.itemDeleted == nil || revealed))) - .contextMenu { menu(ci, range, live: composeState.liveMessage != nil) } - .accessibilityLabel("") + HStack { + if ci.chatDir.sent { + goToItemButton(true) + } + ChatItemView( + chat: chat, + chatItem: ci, + scrollToItemId: scrollToItemId, + maxWidth: maxWidth, + allowMenu: $allowMenu + ) + .environment(\.revealed, revealed) + .environment(\.showTimestamp, itemSeparation.timestamp) + .modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap && (ci.meta.itemDeleted == nil || revealed))) + .contextMenu { menu(ci, range, live: composeState.liveMessage != nil) } + .accessibilityLabel("") + if !ci.chatDir.sent { + goToItemButton(false) + } + } if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 { chatItemReactions(ci) .padding(.bottom, 4) @@ -2133,6 +2198,37 @@ struct ChatView: View { } } + func goToItemInnerButton(_ alignStart: Bool, _ image: String, touchInProgress: Bool, _ onClick: @escaping () -> Void) -> some View { + Button { + onClick() + } label: { + Image(systemName: image) + .resizable() + .frame(width: 13, height: 13) + .padding([alignStart ? .trailing : .leading], 10) + .tint(theme.colors.secondary.opacity(touchInProgress ? 1.0 : 0.4)) + } + } + + @ViewBuilder + func goToItemButton(_ alignStart: Bool) -> some View { + let chatTypeApiIdMsgId = chatItem.meta.itemForwarded?.chatTypeApiIdMsgId + if searchIsNotBlank { + goToItemInnerButton(alignStart, "magnifyingglass", touchInProgress: touchInProgress) { + closeKeyboardAndRun { + ItemsModel.shared.loadOpenChatNoWait(chat.id, chatItem.id) + } + } + } else if let chatTypeApiIdMsgId { + goToItemInnerButton(alignStart, "arrow.right", touchInProgress: touchInProgress) { + closeKeyboardAndRun { + let (chatType, apiId, msgId) = chatTypeApiIdMsgId + ItemsModel.shared.loadOpenChatNoWait("\(chatType.rawValue)\(apiId)", msgId) + } + } + } + } + private struct SelectedChatItem: View { @EnvironmentObject var theme: AppTheme var ciId: Int64 diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 80c5973211..6ded9cae72 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -325,6 +325,7 @@ struct ComposeView: View { @ObservedObject var chat: Chat @Binding var composeState: ComposeState @Binding var keyboardVisible: Bool + @Binding var keyboardHiddenDate: Date @Binding var selectedRange: NSRange @State var linkUrl: URL? = nil @@ -434,6 +435,7 @@ struct ComposeView: View { timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages), onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }}, keyboardVisible: $keyboardVisible, + keyboardHiddenDate: $keyboardHiddenDate, sendButtonColor: chat.chatInfo.incognito ? .indigo.opacity(colorScheme == .dark ? 1 : 0.7) : theme.colors.primary @@ -1280,6 +1282,7 @@ struct ComposeView_Previews: PreviewProvider { chat: chat, composeState: $composeState, keyboardVisible: Binding.constant(true), + keyboardHiddenDate: Binding.constant(Date.now), selectedRange: $selectedRange ) .environmentObject(ChatModel()) @@ -1287,6 +1290,7 @@ struct ComposeView_Previews: PreviewProvider { chat: chat, composeState: $composeState, keyboardVisible: Binding.constant(true), + keyboardHiddenDate: Binding.constant(Date.now), selectedRange: $selectedRange ) .environmentObject(ChatModel()) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift index 3eeb7ba7f5..d809fd7b76 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -16,6 +16,7 @@ struct NativeTextEditor: UIViewRepresentable { @Binding var disableEditing: Bool @Binding var height: CGFloat @Binding var focused: Bool + @Binding var lastUnfocusedDate: Date @Binding var placeholder: String? @Binding var selectedRange: NSRange let onImagesAdded: ([UploadContent]) -> Void @@ -42,7 +43,12 @@ struct NativeTextEditor: UIViewRepresentable { onImagesAdded(images) } } - field.setOnFocusChangedListener { focused = $0 } + field.setOnFocusChangedListener { + focused = $0 + if !focused { + lastUnfocusedDate = .now + } + } field.delegate = field field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4) field.setPlaceholderView() @@ -266,6 +272,7 @@ struct NativeTextEditor_Previews: PreviewProvider{ disableEditing: Binding.constant(false), height: Binding.constant(100), focused: Binding.constant(false), + lastUnfocusedDate: Binding.constant(.now), placeholder: Binding.constant("Placeholder"), selectedRange: Binding.constant(NSRange(location: 0, length: 0)), onImagesAdded: { _ in } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index 9013bb6a88..30767d66ec 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -32,6 +32,7 @@ struct SendMessageView: View { @State private var holdingVMR = false @Namespace var namespace @Binding var keyboardVisible: Bool + @Binding var keyboardHiddenDate: Date var sendButtonColor = Color.accentColor @State private var teHeight: CGFloat = NativeTextEditor.minHeight @State private var teFont: Font = .body @@ -61,6 +62,7 @@ struct SendMessageView: View { disableEditing: $composeState.inProgress, height: $teHeight, focused: $keyboardVisible, + lastUnfocusedDate: $keyboardHiddenDate, placeholder: Binding(get: { composeState.placeholder }, set: { _ in }), selectedRange: $selectedRange, onImagesAdded: onMediaAdded @@ -441,7 +443,8 @@ struct SendMessageView_Previews: PreviewProvider { selectedRange: $selectedRange, sendMessage: { _ in }, onMediaAdded: { _ in }, - keyboardVisible: Binding.constant(true) + keyboardVisible: Binding.constant(true), + keyboardHiddenDate: Binding.constant(Date.now) ) } VStack { @@ -452,7 +455,8 @@ struct SendMessageView_Previews: PreviewProvider { selectedRange: $selectedRangeEditing, sendMessage: { _ in }, onMediaAdded: { _ in }, - keyboardVisible: Binding.constant(true) + keyboardVisible: Binding.constant(true), + keyboardHiddenDate: Binding.constant(Date.now) ) } } diff --git a/apps/ios/Shared/Views/Chat/EndlessScrollView.swift b/apps/ios/Shared/Views/Chat/EndlessScrollView.swift index 81345e4a03..3d4fb381a6 100644 --- a/apps/ios/Shared/Views/Chat/EndlessScrollView.swift +++ b/apps/ios/Shared/Views/Chat/EndlessScrollView.swift @@ -171,8 +171,10 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu visibleItems.last?.index ?? 0 } - /// Whether there is scroll to item in progress or not + /// Whether there is a non-animated scroll to item in progress or not var isScrolling: Bool = false + /// Whether there is an animated scroll to item in progress or not + var isAnimatedScrolling: Bool = false override init() { super.init() @@ -349,11 +351,11 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu if // there is auto scroll in progress and the first item has a higher offset than bottom part // of the screen. In order to make scrolling down & up equal in time, we treat this as a sign to // re-make the first visible item - (listState.isScrolling && vis.view.frame.origin.y + vis.view.bounds.height < contentOffsetY + bounds.height) || + (listState.isAnimatedScrolling && vis.view.frame.origin.y + vis.view.bounds.height < contentOffsetY + bounds.height) || // the fist visible item previously is hidden now, remove it and move on !isVisible(vis.view) { let newIndex: Int - if listState.isScrolling { + if listState.isAnimatedScrolling { // skip many items to make the scrolling take less time var indexDiff = !alreadyChangedIndexWhileScrolling ? Int(ceil(abs(offsetsDiff / averageItemHeight))) : 0 // if index was already changed, no need to change it again. Otherwise, the scroll will overscoll and return back animated. Because it means the whole screen was scrolled @@ -471,7 +473,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu } func scrollToItem(_ index: Int, top: Bool = true) { - if index >= listState.items.count || listState.isScrolling { + if index >= listState.items.count || listState.isScrolling || listState.isAnimatedScrolling { return } if bounds.height == 0 { @@ -498,7 +500,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu //let step: CGFloat = max(0.1, CGFloat(abs(index - firstOrLastIndex)) * scrollStepMultiplier) var stepSlowdownMultiplier: CGFloat = 1 - while true { + while i < 200 { let up = index > listState.firstVisibleItemIndex if upPrev != up { stepSlowdownMultiplier = stepSlowdownMultiplier * 0.5 @@ -522,18 +524,22 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu break } contentOffset = CGPointMake(contentOffset.x, adjustedOffset) + adaptItems(listState.items, false) + snapToContent(animated: false) i += 1 } + adaptItems(listState.items, false) + snapToContent(animated: false) estimatedContentHeight.update(contentOffset, listState, averageItemHeight, true) } func scrollToItemAnimated(_ index: Int, top: Bool = true) async { - if index >= listState.items.count || listState.isScrolling { + if index >= listState.items.count || listState.isScrolling || listState.isAnimatedScrolling { return } - listState.isScrolling = true + listState.isAnimatedScrolling = true defer { - listState.isScrolling = false + listState.isAnimatedScrolling = false } var adjustedOffset = self.contentOffset.y var i = 0 @@ -543,7 +549,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu //let step: CGFloat = max(0.1, CGFloat(abs(index - firstOrLastIndex)) * scrollStepMultiplier) var stepSlowdownMultiplier: CGFloat = 1 - while true { + while i < 200 { let up = index > listState.firstVisibleItemIndex if upPrev != up { stepSlowdownMultiplier = stepSlowdownMultiplier * 0.5 diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 9885811051..2b58abef65 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -20,6 +20,7 @@ struct TerminalView: View { @State var composeState: ComposeState = ComposeState() @State var selectedRange = NSRange() @State private var keyboardVisible = false + @State private var keyboardHiddenDate = Date.now @State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) @State private var terminalItem: TerminalItem? @State private var scrolled = false @@ -101,7 +102,8 @@ struct TerminalView: View { sendMessage: { _ in consoleSendMessage() }, showVoiceMessageButton: false, onMediaAdded: { _ in }, - keyboardVisible: $keyboardVisible + keyboardVisible: $keyboardVisible, + keyboardHiddenDate: $keyboardHiddenDate ) .padding(.horizontal, 12) } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 169a7c21de..a601e60d5f 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -3261,6 +3261,20 @@ public enum CIForwardedFrom: Decodable, Hashable { } } + public var chatTypeApiIdMsgId: (ChatType, Int64, ChatItem.ID?)? { + switch self { + case .unknown: nil + case let .contact(_, _, contactId, msgId): + if let contactId { + (ChatType.direct, contactId, msgId) + } else { nil } + case let .group(_, _, groupId, msgId): + if let groupId { + (ChatType.group, groupId, msgId) + } else { nil } + } + } + public func text(_ chatType: ChatType) -> LocalizedStringKey { chatType == .local ? (chatName == "" ? "saved" : "saved from \(chatName)") From dce85021650dcaddf156a10f81a91e44f0e86805 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:57:41 +0700 Subject: [PATCH 13/24] android: allow to enter passphrase in case of error reading it (#5683) * android: allow to enter passphrase in case of error reading it * change * refactor * strings --------- Co-authored-by: Evgeny Poberezkin --- .../common/platform/Cryptor.android.kt | 29 ++++++++++++++++--- .../commonMain/resources/MR/base/strings.xml | 5 +++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt index dc6c53ecbc..d9a5fb59e3 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt @@ -30,10 +30,31 @@ internal class Cryptor: CryptorInterface { } return null } - val cipher: Cipher = Cipher.getInstance(TRANSFORMATION) - val spec = GCMParameterSpec(128, iv) - cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) - return runCatching { String(cipher.doFinal(data))}.onFailure { Log.e(TAG, "doFinal: ${it.stackTraceToString()}") }.getOrNull() + + try { + val cipher: Cipher = Cipher.getInstance(TRANSFORMATION) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + return String(cipher.doFinal(data)) + } catch (e: Throwable) { + Log.e(TAG, "cipher.init: ${e.stackTraceToString()}") + val randomPassphrase = appPreferences.initialRandomDBPassphrase.get() + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error_reading_passphrase), + text = generalGetString(if (randomPassphrase) { + MR.strings.restore_passphrase_can_not_be_read_desc + } else { + MR.strings.restore_passphrase_can_not_be_read_enter_manually_desc + } + ) + .plus("\n\n").plus(e.stackTraceToString()) + ) + if (randomPassphrase) { + // do not allow to override initial random passphrase in case of such error + throw e + } + return null + } } override fun encryptText(text: String, alias: String): Pair { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index aa0f49ee1d..83f085fe3a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1471,6 +1471,7 @@ Wrong database passphrase + Error reading database passphrase Encrypted database Database error Keychain error @@ -1492,7 +1493,9 @@ Please enter the previous password after restoring database backup. This action can not be undone. Restore Restore database error - Passphrase not found in Keystore, please enter it manually. This may have happened if you restored the app\'s data using a backup tool. If it\'s not the case, please, contact developers. + Passphrase not found in Keystore, please enter it manually. This may have happened if you restored the app\'s data using a backup tool. If it\'s not the case, please contact developers. + Passphrase in Keystore can\'t be read. This may have happened after system update incompatible with the app. If it\'s not the case, please contact developers. + Passphrase in Keystore can\'t be read, please enter it manually. This may have happened after system update incompatible with the app. If it\'s not the case, please contact developers. Database upgrade Database downgrade Incompatible database version From dcea008fb959972ff9bdf5d555133b8c141fd9ea Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:43:39 +0400 Subject: [PATCH 14/24] core: batch apis - remove, block, change role of members (#5674) * core: core: batch remove members * order * foldr * list * style * batch block * change role * test * if --- src/Simplex/Chat/Controller.hs | 12 +- src/Simplex/Chat/Library/Commands.hs | 247 ++++++++++++------ src/Simplex/Chat/Library/Internal.hs | 13 +- src/Simplex/Chat/Library/Subscriber.hs | 14 +- src/Simplex/Chat/Messages/CIContent/Events.hs | 4 +- src/Simplex/Chat/Store/Groups.hs | 7 +- src/Simplex/Chat/View.hs | 26 +- tests/Bots/DirectoryTests.hs | 22 +- tests/ChatTests/Groups.hs | 135 +++++++--- tests/ChatTests/Profiles.hs | 2 +- 10 files changed, 320 insertions(+), 162 deletions(-) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index e64553af98..71b57c72b2 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -366,9 +366,9 @@ data ChatCommand | ApiGetConnNtfMessages {connIds :: NonEmpty AgentConnId} | APIAddMember GroupId ContactId GroupMemberRole | APIJoinGroup {groupId :: GroupId, enableNtfs :: MsgFilter} - | APIMemberRole GroupId GroupMemberId GroupMemberRole - | APIBlockMemberForAll GroupId GroupMemberId Bool - | APIRemoveMember GroupId GroupMemberId + | APIMembersRole GroupId (NonEmpty GroupMemberId) GroupMemberRole + | APIBlockMembersForAll GroupId (NonEmpty GroupMemberId) Bool + | APIRemoveMembers GroupId (NonEmpty GroupMemberId) | APILeaveGroup GroupId | APIListMembers GroupId | APIUpdateGroupProfile GroupId GroupProfile @@ -673,7 +673,7 @@ data ChatResponse | CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact} | CRGroupLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} | CRBusinessLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, fromContact :: Contact} - | CRUserDeletedMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember]} | CRGroupsList {user :: User, groups :: [(GroupInfo, GroupSummary)]} | CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} | CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus @@ -758,9 +758,9 @@ data ChatResponse | CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} | CRMemberRole {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} - | CRMemberRoleUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} + | CRMembersRoleUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], toRole :: GroupMemberRole} | CRMemberBlockedForAll {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, blocked :: Bool} - | CRMemberBlockedForAllUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember, blocked :: Bool} + | CRMembersBlockedForAllUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], blocked :: Bool} | CRConnectedToGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, memberContact :: Maybe Contact} | CRDeletedMember {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember} | CRDeletedMemberUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 624f6a6dc1..f44dca9026 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2023,75 +2023,170 @@ processChatCommand' vr = \case updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` (toView . CRChatError (Just user)) pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing Nothing -> throwChatError $ CEContactNotActive ct - APIMemberRole groupId memberId memRole -> withUser $ \user -> do - Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user groupId - if memberId == groupMemberId' membership - then changeMemberRole user gInfo members membership $ SGEUserRole memRole - else case find ((== memberId) . groupMemberId') members of - Just m -> changeMemberRole user gInfo members m $ SGEMemberRole memberId (fromLocalProfile $ memberProfile m) memRole - _ -> throwChatError CEGroupMemberNotFound + APIMembersRole groupId memberIds newRole -> withUser $ \user -> + withGroupLock "memberRole" groupId . procCmd $ do + g@(Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId + when (selfSelected gInfo) $ throwChatError $ CECommandError "can't change role for self" + let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin) = selectMembers members + when (length invitedMems + length currentMems + length unchangedMems /= length memberIds) $ throwChatError CEGroupMemberNotFound + when (length memberIds > 1 && (anyAdmin || newRole >= GRAdmin)) $ + throwChatError $ CECommandError "can't change role of multiple members when admins selected, or new role is admin" + assertUserGroupRole gInfo $ maximum ([GRAdmin, maxRole, newRole] :: [GroupMemberRole]) + (errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems + (errs2, changed2, acis) <- changeRoleCurrentMems user g currentMems + unless (null acis) $ toView $ CRNewChatItems user acis + let errs = errs1 <> errs2 + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRMembersRoleUser {user, groupInfo = gInfo, members = changed1 <> changed2, toRole = newRole} -- same order is not guaranteed where - changeMemberRole user gInfo members m gEvent = do - let GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberContactId, localDisplayName = cName} = m - assertUserGroupRole gInfo $ maximum ([GRAdmin, mRole, memRole] :: [GroupMemberRole]) - withGroupLock "memberRole" groupId . procCmd $ do - unless (mRole == memRole) $ do - withFastStore' $ \db -> updateGroupMemberRole db user m memRole - case mStatus of - GSMemInvited -> do - withFastStore (\db -> (,) <$> mapM (getContact db vr user) memberContactId <*> liftIO (getMemberInvitation db user $ groupMemberId' m)) >>= \case - (Just ct, Just cReq) -> sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = memRole} cReq - _ -> throwChatError $ CEGroupCantResendInvitation gInfo cName - _ -> do - msg <- sendGroupMessage user gInfo members $ XGrpMemRole mId memRole - ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent gEvent) - toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] - pure CRMemberRoleUser {user, groupInfo = gInfo, member = m {memberRole = memRole}, fromRole = mRole, toRole = memRole} - APIBlockMemberForAll groupId memberId blocked -> withUser $ \user -> do - Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user groupId - when (memberId == groupMemberId' membership) $ throwChatError $ CECommandError "can't block/unblock self" - case splitMember memberId members of - Nothing -> throwChatError $ CEException "expected to find a single blocked member" - Just (bm, remainingMembers) -> do - let GroupMember {memberId = bmMemberId, memberRole = bmRole, memberProfile = bmp} = bm - -- TODO GRModerator when most users migrate - assertUserGroupRole gInfo $ max GRAdmin bmRole - when (blocked == blockedByAdmin bm) $ throwChatError $ CECommandError $ if blocked then "already blocked" else "already unblocked" - withGroupLock "blockForAll" groupId . procCmd $ do - let mrs = if blocked then MRSBlocked else MRSUnrestricted - event = XGrpMemRestrict bmMemberId MemberRestrictions {restriction = mrs} - msg <- sendGroupMessage' user gInfo remainingMembers event - let ciContent = CISndGroupEvent $ SGEMemberBlocked memberId (fromLocalProfile bmp) blocked - ci <- saveSndChatItem user (CDGroupSnd gInfo) msg ciContent - toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] - bm' <- withFastStore $ \db -> do - liftIO $ updateGroupMemberBlocked db user groupId memberId mrs - getGroupMember db vr user groupId memberId - toggleNtf user bm' (not blocked) - pure CRMemberBlockedForAllUser {user, groupInfo = gInfo, member = bm', blocked} + selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds + selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool) + selectMembers = foldr' addMember ([], [], [], GRObserver, False) + where + addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin) + | groupMemberId `elem` memberIds = + let maxRole' = max maxRole memberRole + anyAdmin' = anyAdmin || memberRole >= GRAdmin + in + if + | memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin') + | memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin') + | otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin') + | otherwise = (invited, current, unchanged, maxRole, anyAdmin) + changeRoleInvitedMems :: User -> GroupInfo -> [GroupMember] -> CM ([ChatError], [GroupMember]) + changeRoleInvitedMems user gInfo memsToChange = do + -- not batched, as we need to send different invitations to different connections anyway + mems_ <- forM memsToChange $ \m -> (Right <$> changeRole m) `catchChatError` (pure . Left) + pure $ partitionEithers mems_ + where + changeRole :: GroupMember -> CM GroupMember + changeRole m@GroupMember {groupMemberId, memberContactId, localDisplayName = cName} = do + withFastStore (\db -> (,) <$> mapM (getContact db vr user) memberContactId <*> liftIO (getMemberInvitation db user groupMemberId)) >>= \case + (Just ct, Just cReq) -> do + sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = newRole} cReq + withFastStore' $ \db -> updateGroupMemberRole db user m newRole + pure (m :: GroupMember) {memberRole = newRole} + _ -> throwChatError $ CEGroupCantResendInvitation gInfo cName + changeRoleCurrentMems :: User -> Group -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem]) + changeRoleCurrentMems user (Group gInfo members) memsToChange = case L.nonEmpty memsToChange of + Nothing -> pure ([], [], []) + Just memsToChange' -> do + let events = L.map (\GroupMember {memberId} -> XGrpMemRole memberId newRole) memsToChange' + (msgs_, _gsr) <- sendGroupMessages user gInfo members events + let itemsData = zipWith (fmap . sndItemData) memsToChange (L.toList msgs_) + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData Nothing False + when (length cis_ /= length memsToChange) $ logError "changeRoleCurrentMems: memsToChange and cis_ length mismatch" + (errs, changed) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updMember db) memsToChange) + let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ + pure (errs, changed, acis) + where + sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c + sndItemData GroupMember {groupMemberId, memberProfile} msg = + let content = CISndGroupEvent $ SGEMemberRole groupMemberId (fromLocalProfile memberProfile) newRole + ts = ciContentTexts content + in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing + updMember db m = do + updateGroupMemberRole db user m newRole + pure (m :: GroupMember) {memberRole = newRole} + APIBlockMembersForAll groupId memberIds blockFlag -> withUser $ \user -> + withGroupLock "blockForAll" groupId . procCmd $ do + Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId + when (selfSelected gInfo) $ throwChatError $ CECommandError "can't block/unblock self" + let (blockMems, remainingMems, maxRole, anyAdmin) = selectMembers members + when (length blockMems /= length memberIds) $ throwChatError CEGroupMemberNotFound + when (length memberIds > 1 && anyAdmin) $ throwChatError $ CECommandError "can't block/unblock multiple members when admins selected" + assertUserGroupRole gInfo $ max GRModerator maxRole + blockMembers user gInfo blockMems remainingMems where - splitMember mId ms = case break ((== mId) . groupMemberId') ms of - (_, []) -> Nothing - (ms1, bm : ms2) -> Just (bm, ms1 <> ms2) - APIRemoveMember groupId memberId -> withUser $ \user -> do - Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId - case find ((== memberId) . groupMemberId') members of - Nothing -> throwChatError CEGroupMemberNotFound - Just m@GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberProfile} -> do - assertUserGroupRole gInfo $ max GRAdmin mRole - withGroupLock "removeMember" groupId . procCmd $ do - case mStatus of - GSMemInvited -> do - deleteMemberConnection user m - withFastStore' $ \db -> deleteGroupMember db user m - _ -> do - msg <- sendGroupMessage user gInfo members $ XGrpMemDel mId - ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent $ SGEMemberDeleted memberId (fromLocalProfile memberProfile)) - toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] - deleteMemberConnection' user m True - -- undeleted "member connected" chat item will prevent deletion of member record - deleteOrUpdateMemberRecord user m - pure $ CRUserDeletedMember user gInfo m {memberStatus = GSMemRemoved} + selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds + selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], GroupMemberRole, Bool) + selectMembers = foldr' addMember ([], [], GRObserver, False) + where + addMember m@GroupMember {groupMemberId, memberRole} (block, remaining, maxRole, anyAdmin) + | groupMemberId `elem` memberIds = + let maxRole' = max maxRole memberRole + anyAdmin' = anyAdmin || memberRole >= GRAdmin + in (m : block, remaining, maxRole', anyAdmin') + | otherwise = (block, m : remaining, maxRole, anyAdmin) + blockMembers :: User -> GroupInfo -> [GroupMember] -> [GroupMember] -> CM ChatResponse + blockMembers user gInfo blockMems remainingMems = case L.nonEmpty blockMems of + Nothing -> throwChatError $ CECommandError "no members to block/unblock" + Just blockMems' -> do + let mrs = if blockFlag then MRSBlocked else MRSUnrestricted + events = L.map (\GroupMember {memberId} -> XGrpMemRestrict memberId MemberRestrictions {restriction = mrs}) blockMems' + (msgs_, _gsr) <- sendGroupMessages user gInfo remainingMems events + let itemsData = zipWith (fmap . sndItemData) blockMems (L.toList msgs_) + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData Nothing False + when (length cis_ /= length blockMems) $ logError "blockMembers: blockMems and cis_ length mismatch" + let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ + unless (null acis) $ toView $ CRNewChatItems user acis + (errs, blocked) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updateGroupMemberBlocked db user gInfo mrs) blockMems) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + -- TODO not batched - requires agent batch api + forM_ blocked $ \m -> toggleNtf user m (not blockFlag) + pure CRMembersBlockedForAllUser {user, groupInfo = gInfo, members = blocked, blocked = blockFlag} + where + sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c + sndItemData GroupMember {groupMemberId, memberProfile} msg = + let content = CISndGroupEvent $ SGEMemberBlocked groupMemberId (fromLocalProfile memberProfile) blockFlag + ts = ciContentTexts content + in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing + APIRemoveMembers groupId memberIds -> withUser $ \user -> + withGroupLock "removeMembers" groupId . procCmd $ do + g@(Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId + let (invitedMems, currentMems, maxRole, anyAdmin) = selectMembers members + when (length invitedMems + length currentMems /= length memberIds) $ throwChatError CEGroupMemberNotFound + when (length memberIds > 1 && anyAdmin) $ throwChatError $ CECommandError "can't remove multiple members when admins selected" + assertUserGroupRole gInfo $ max GRAdmin maxRole + (errs1, deleted1) <- deleteInvitedMems user invitedMems + (errs2, deleted2, acis) <- deleteCurrentMems user g currentMems + unless (null acis) $ toView $ CRNewChatItems user acis + let errs = errs1 <> errs2 + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRUserDeletedMembers user gInfo (deleted1 <> deleted2) -- same order is not guaranteed + where + selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], GroupMemberRole, Bool) + selectMembers = foldr' addMember ([], [], GRObserver, False) + where + addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, maxRole, anyAdmin) + | groupMemberId `elem` memberIds = + let maxRole' = max maxRole memberRole + anyAdmin' = anyAdmin || memberRole >= GRAdmin + in + if memberStatus == GSMemInvited + then (m : invited, current, maxRole', anyAdmin') + else (invited, m : current, maxRole', anyAdmin') + | otherwise = (invited, current, maxRole, anyAdmin) + deleteInvitedMems :: User -> [GroupMember] -> CM ([ChatError], [GroupMember]) + deleteInvitedMems user memsToDelete = do + deleteMembersConnections user memsToDelete + lift $ partitionEithers <$> withStoreBatch' (\db -> map (delMember db) memsToDelete) + where + delMember db m = do + deleteGroupMember db user m + pure m {memberStatus = GSMemRemoved} + deleteCurrentMems :: User -> Group -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem]) + deleteCurrentMems user (Group gInfo members) memsToDelete = case L.nonEmpty memsToDelete of + Nothing -> pure ([], [], []) + Just memsToDelete' -> do + let events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId) memsToDelete' + (msgs_, _gsr) <- sendGroupMessages user gInfo members events + let itemsData = zipWith (fmap . sndItemData) memsToDelete (L.toList msgs_) + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData Nothing False + when (length cis_ /= length memsToDelete) $ logError "deleteCurrentMems: memsToDelete and cis_ length mismatch" + deleteMembersConnections' user memsToDelete True + (errs, deleted) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (delMember db) memsToDelete) + let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ + pure (errs, deleted, acis) + where + sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c + sndItemData GroupMember {groupMemberId, memberProfile} msg = + let content = CISndGroupEvent $ SGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) + ts = ciContentTexts content + in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing + delMember db m = do + deleteOrUpdateMemberRecordIO db user m + pure m {memberStatus = GSMemRemoved} APILeaveGroup groupId -> withUser $ \user@User {userId} -> do Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user groupId filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo @@ -2114,18 +2209,14 @@ processChatCommand' vr = \case JoinGroup gName enableNtfs -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIJoinGroup groupId enableNtfs - MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMemberRole gId gMemberId memRole - BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMemberForAll gId gMemberId blocked + MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMembersRole gId [gMemberId] memRole + BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMembersForAll gId [gMemberId] blocked RemoveMembers gName gMemberNames -> withUser $ \user -> do (gId, gMemberIds) <- withStore $ \db -> do gId <- getGroupIdByName db user gName gMemberIds <- forM gMemberNames $ getGroupMemberIdByName db user gId pure (gId, gMemberIds) - rs <- forM (L.zip (L.fromList [1..]) gMemberIds) $ \(i, memId) -> do - r <- processChatCommand (APIRemoveMember gId memId) - when (i < length gMemberIds) $ toView r - pure r - pure $ L.last rs + processChatCommand $ APIRemoveMembers gId gMemberIds LeaveGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APILeaveGroup groupId @@ -3090,7 +3181,7 @@ processChatCommand' vr = \case (msgs_, gsr) <- sendGroupMessages user gInfo ms chatMsgEvents let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) (L.toList msgs_) cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData timed_ live - when (length itemsData /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch" + when (length cis_ /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch" createMemberSndStatuses cis_ msgs_ gsr let r@(_, cis) = partitionEithers cis_ processSendErrs user r @@ -3795,9 +3886,9 @@ chatCommandP = "/_ntf conn messages " *> (ApiGetConnNtfMessages <$> strP), "/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), "/_join #" *> (APIJoinGroup <$> A.decimal <*> pure MFAll), -- needs to be changed to support in UI - "/_member role #" *> (APIMemberRole <$> A.decimal <* A.space <*> A.decimal <*> memberRole), - "/_block #" *> (APIBlockMemberForAll <$> A.decimal <* A.space <*> A.decimal <* A.space <* "blocked=" <*> onOffP), - "/_remove #" *> (APIRemoveMember <$> A.decimal <* A.space <*> A.decimal), + "/_member role #" *> (APIMembersRole <$> A.decimal <*> _strP <*> memberRole), + "/_block #" *> (APIBlockMembersForAll <$> A.decimal <*> _strP <* A.space <* "blocked=" <*> onOffP), + "/_remove #" *> (APIRemoveMembers <$> A.decimal <*> _strP), "/_leave #" *> (APILeaveGroup <$> A.decimal), "/_members #" *> (APIListMembers <$> A.decimal), "/_server test " *> (APITestProtoServer <$> A.decimal <* A.space <*> strP), diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 4a62c4ccb6..eba1bf169f 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1251,11 +1251,14 @@ deleteMemberConnection' user GroupMember {activeConn} waitDelivery = do withStore' $ \db -> updateConnectionStatus db conn ConnDeleted deleteOrUpdateMemberRecord :: User -> GroupMember -> CM () -deleteOrUpdateMemberRecord user@User {userId} member = - withStore' $ \db -> - checkGroupMemberHasItems db user member >>= \case - Just _ -> updateGroupMemberStatus db userId member GSMemRemoved - Nothing -> deleteGroupMember db user member +deleteOrUpdateMemberRecord user member = + withStore' $ \db -> deleteOrUpdateMemberRecordIO db user member + +deleteOrUpdateMemberRecordIO :: DB.Connection -> User -> GroupMember -> IO () +deleteOrUpdateMemberRecordIO db user@User {userId} member = + checkGroupMemberHasItems db user member >>= \case + Just _ -> updateGroupMemberStatus db userId member GSMemRemoved + Nothing -> deleteGroupMember db user member sendDirectContactMessages :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage] sendDirectContactMessages user ct events = do diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 44ad4ccc85..087d49e49a 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -2608,7 +2608,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xGrpMemRestrict :: GroupInfo -> GroupMember -> MemberId -> MemberRestrictions -> RcvMessage -> UTCTime -> CM () xGrpMemRestrict - gInfo@GroupInfo {groupId, membership = GroupMember {memberId = membershipMemId}} + gInfo@GroupInfo {membership = GroupMember {memberId = membershipMemId}} m@GroupMember {memberRole = senderRole} memId MemberRestrictions {restriction} @@ -2619,10 +2619,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = messageError "x.grp.mem.restrict: admin blocks you" | otherwise = withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case - Right bm@GroupMember {groupMemberId = bmId, memberRole, memberProfile = bmp} + Right bm@GroupMember {groupMemberId = bmId, memberRole, blockedByAdmin, memberProfile = bmp} + | blockedByAdmin == mrsBlocked restriction -> pure () | senderRole < GRModerator || senderRole < memberRole -> messageError "x.grp.mem.restrict with insufficient member permissions" | otherwise -> do - bm' <- setMemberBlocked bmId + bm' <- setMemberBlocked bm toggleNtf user bm' (not blocked) let ciContent = CIRcvGroupEvent $ RGEMemberBlocked bmId (fromLocalProfile bmp) blocked ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs ciContent @@ -2630,14 +2631,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView CRMemberBlockedForAll {user, groupInfo = gInfo, byMember = m, member = bm, blocked} Left (SEGroupMemberNotFoundByMemberId _) -> do bm <- createUnknownMember gInfo memId - bm' <- setMemberBlocked $ groupMemberId' bm + bm' <- setMemberBlocked bm toView $ CRUnknownMemberBlocked user gInfo m bm' Left e -> throwError $ ChatErrorStore e where - setMemberBlocked bmId = - withStore $ \db -> do - liftIO $ updateGroupMemberBlocked db user groupId bmId restriction - getGroupMember db vr user groupId bmId + setMemberBlocked bm = withStore' $ \db -> updateGroupMemberBlocked db user gInfo restriction bm blocked = mrsBlocked restriction xGrpMemCon :: GroupInfo -> GroupMember -> MemberId -> CM () diff --git a/src/Simplex/Chat/Messages/CIContent/Events.hs b/src/Simplex/Chat/Messages/CIContent/Events.hs index 74f7d94399..054530e06f 100644 --- a/src/Simplex/Chat/Messages/CIContent/Events.hs +++ b/src/Simplex/Chat/Messages/CIContent/Events.hs @@ -33,9 +33,9 @@ data RcvGroupEvent data SndGroupEvent = SGEMemberRole {groupMemberId :: GroupMemberId, profile :: Profile, role :: GroupMemberRole} - | SGEMemberBlocked {groupMemberId :: GroupMemberId, profile :: Profile, blocked :: Bool} -- CRMemberBlockedForAllUser + | SGEMemberBlocked {groupMemberId :: GroupMemberId, profile :: Profile, blocked :: Bool} -- CRMembersBlockedForAllUser | SGEUserRole {role :: GroupMemberRole} - | SGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRUserDeletedMember + | SGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRUserDeletedMembers | SGEUserLeft -- CRLeftMemberUser | SGEGroupUpdated {groupProfile :: GroupProfile} -- CRGroupUpdated deriving (Show) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 67722ebd0f..a1ce3ab269 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -2062,8 +2062,8 @@ updateGroupMemberSettings db User {userId} gId gMemberId GroupMemberSettings {sh |] (BI showMessages, currentTs, userId, gId, gMemberId) -updateGroupMemberBlocked :: DB.Connection -> User -> GroupId -> GroupMemberId -> MemberRestrictionStatus -> IO () -updateGroupMemberBlocked db User {userId} gId gMemberId memberBlocked = do +updateGroupMemberBlocked :: DB.Connection -> User -> GroupInfo -> MemberRestrictionStatus -> GroupMember -> IO GroupMember +updateGroupMemberBlocked db User {userId} GroupInfo {groupId} mrs m@GroupMember {groupMemberId} = do currentTs <- getCurrentTime DB.execute db @@ -2072,7 +2072,8 @@ updateGroupMemberBlocked db User {userId} gId gMemberId memberBlocked = do SET member_restriction = ?, updated_at = ? WHERE user_id = ? AND group_id = ? AND group_member_id = ? |] - (memberBlocked, currentTs, userId, gId, gMemberId) + (mrs, currentTs, userId, groupId, groupMemberId) + pure m {blockedByAdmin = mrsBlocked mrs} getXGrpMemIntroContDirect :: DB.Connection -> User -> Contact -> IO (Maybe (Int64, XGrpMemIntroCont)) getXGrpMemIntroContDirect db User {userId} Contact {contactId} = do diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index a8c2d215a8..7a20cb2fb0 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -220,7 +220,9 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."] CRGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] CRBusinessLinkConnecting u g _ _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] - CRUserDeletedMember u g m -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"] + CRUserDeletedMembers u g members -> case members of + [m] -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"] + mems' -> ttyUser u [ttyGroup' g <> ": you removed " <> sShow (length mems') <> " members from the group"] CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g CRUnknownMemberCreated u g fwdM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember fwdM <> " forwarded a message from an unknown member, creating unknown member record " <> ttyMember um] CRUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um] @@ -301,9 +303,9 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRJoinedGroupMemberConnecting u g host m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] CRConnectedToGroupMember u g m _ -> ttyUser u [ttyGroup' g <> ": " <> connectedMember m <> " is connected"] CRMemberRole u g by m r r' -> ttyUser u $ viewMemberRoleChanged g by m r r' - CRMemberRoleUser u g m r r' -> ttyUser u $ viewMemberRoleUserChanged g m r r' + CRMembersRoleUser u g members r' -> ttyUser u $ viewMemberRoleUserChanged g members r' CRMemberBlockedForAll u g by m blocked -> ttyUser u $ viewMemberBlockedForAll g by m blocked - CRMemberBlockedForAllUser u g m blocked -> ttyUser u $ viewMemberBlockedForAllUser g m blocked + CRMembersBlockedForAllUser u g members blocked -> ttyUser u $ viewMembersBlockedForAllUser g members blocked CRDeletedMemberUser u g by -> ttyUser u $ [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group"] <> groupPreserved g CRDeletedMember u g by m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember by <> " removed " <> ttyMember m <> " from the group"] CRLeftMember u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " left the group"] @@ -1109,21 +1111,19 @@ viewMemberRoleChanged g@GroupInfo {membership} by m r r' memId = groupMemberId' m view s = [ttyGroup' g <> ": " <> ttyMember by <> " changed " <> s <> " from " <> showRole r <> " to " <> showRole r'] -viewMemberRoleUserChanged :: GroupInfo -> GroupMember -> GroupMemberRole -> GroupMemberRole -> [StyledString] -viewMemberRoleUserChanged g@GroupInfo {membership} m r r' - | r == r' = [ttyGroup' g <> ": member role did not change"] - | groupMemberId' membership == groupMemberId' m = view "your role" - | otherwise = view $ "the role of " <> ttyMember m - where - view s = [ttyGroup' g <> ": you changed " <> s <> " from " <> showRole r <> " to " <> showRole r'] +viewMemberRoleUserChanged :: GroupInfo -> [GroupMember] -> GroupMemberRole -> [StyledString] +viewMemberRoleUserChanged g members r = case members of + [m] -> [ttyGroup' g <> ": you changed the role of " <> ttyMember m <> " to " <> showRole r] + mems' -> [ttyGroup' g <> ": you changed the role of " <> sShow (length mems') <> " members to " <> showRole r] viewMemberBlockedForAll :: GroupInfo -> GroupMember -> GroupMember -> Bool -> [StyledString] viewMemberBlockedForAll g by m blocked = [ttyGroup' g <> ": " <> ttyMember by <> " " <> (if blocked then "blocked" else "unblocked") <> " " <> ttyMember m] -viewMemberBlockedForAllUser :: GroupInfo -> GroupMember -> Bool -> [StyledString] -viewMemberBlockedForAllUser g m blocked = - [ttyGroup' g <> ": you " <> (if blocked then "blocked" else "unblocked") <> " " <> ttyMember m] +viewMembersBlockedForAllUser :: GroupInfo -> [GroupMember] -> Bool -> [StyledString] +viewMembersBlockedForAllUser g members blocked = case members of + [m] -> [ttyGroup' g <> ": you " <> (if blocked then "blocked" else "unblocked") <> " " <> ttyMember m] + mems' -> [ttyGroup' g <> ": you " <> (if blocked then "blocked" else "unblocked") <> " " <> sShow (length mems') <> " members"] showRole :: GroupMemberRole -> StyledString showRole = plain . strEncode diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 5bdc379cbf..2a9ad30dd2 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -122,7 +122,7 @@ testDirectoryService ps = bob <# "SimpleX-Directory> You must grant directory service admin role to register the group" bob ##> "/mr PSA SimpleX-Directory admin" -- putStrLn "*** discover service joins group and creates the link for profile" - bob <## "#PSA: you changed the role of SimpleX-Directory from member to admin" + bob <## "#PSA: you changed the role of SimpleX-Directory to admin" bob <# "SimpleX-Directory> Joining the group PSA…" bob <## "#PSA: SimpleX-Directory joined the group" bob <# "SimpleX-Directory> Joined the group PSA, creating the link…" @@ -579,7 +579,7 @@ testDelistedRoleChanges ps = groupFoundN 3 cath "privacy" -- de-listed if service role changed bob ##> "/mr privacy SimpleX-Directory member" - bob <## "#privacy: you changed the role of SimpleX-Directory from admin to member" + bob <## "#privacy: you changed the role of SimpleX-Directory to member" cath <## "#privacy: bob changed the role of SimpleX-Directory from admin to member" bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to member." bob <## "" @@ -588,7 +588,7 @@ testDelistedRoleChanges ps = groupNotFound cath "privacy" -- re-listed if service role changed back without profile changes cath ##> "/mr privacy SimpleX-Directory admin" - cath <## "#privacy: you changed the role of SimpleX-Directory from member to admin" + cath <## "#privacy: you changed the role of SimpleX-Directory to admin" bob <## "#privacy: cath changed the role of SimpleX-Directory from member to admin" bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" @@ -597,7 +597,7 @@ testDelistedRoleChanges ps = groupFoundN 3 cath "privacy" -- de-listed if owner role changed cath ##> "/mr privacy bob admin" - cath <## "#privacy: you changed the role of bob from owner to admin" + cath <## "#privacy: you changed the role of bob to admin" bob <## "#privacy: cath changed your role from owner to admin" bob <# "SimpleX-Directory> Your role in the group ID 1 (privacy) is changed to admin." bob <## "" @@ -606,7 +606,7 @@ testDelistedRoleChanges ps = groupNotFound cath "privacy" -- re-listed if owner role changed back without profile changes cath ##> "/mr privacy bob owner" - cath <## "#privacy: you changed the role of bob from admin to owner" + cath <## "#privacy: you changed the role of bob to owner" bob <## "#privacy: cath changed your role from admin to owner" bob <# "SimpleX-Directory> Your role in the group ID 1 (privacy) is changed to owner." bob <## "" @@ -627,7 +627,7 @@ testNotDelistedMemberRoleChanged ps = cath <## "use @SimpleX-Directory to send messages" groupFoundN 3 cath "privacy" bob ##> "/mr privacy cath member" - bob <## "#privacy: you changed the role of cath from owner to member" + bob <## "#privacy: you changed the role of cath to member" cath <## "#privacy: bob changed your role from owner to member" groupFoundN 3 cath "privacy" @@ -641,11 +641,11 @@ testNotSentApprovalBadRoles ps = submitGroup bob "privacy" "Privacy" welcomeWithLink <- groupAccepted bob "privacy" bob ##> "/mr privacy SimpleX-Directory member" - bob <## "#privacy: you changed the role of SimpleX-Directory from admin to member" + bob <## "#privacy: you changed the role of SimpleX-Directory to member" updateProfileWithLink bob "privacy" welcomeWithLink 1 bob <# "SimpleX-Directory> You must grant directory service admin role to register the group" bob ##> "/mr privacy SimpleX-Directory admin" - bob <## "#privacy: you changed the role of SimpleX-Directory from member to admin" + bob <## "#privacy: you changed the role of SimpleX-Directory to admin" bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is submitted for approval." @@ -666,14 +666,14 @@ testNotApprovedBadRoles ps = updateProfileWithLink bob "privacy" welcomeWithLink 1 notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 bob ##> "/mr privacy SimpleX-Directory member" - bob <## "#privacy: you changed the role of SimpleX-Directory from admin to member" + bob <## "#privacy: you changed the role of SimpleX-Directory to member" let approve = "/approve 1:privacy 1" superUser #> ("@SimpleX-Directory " <> approve) superUser <# ("SimpleX-Directory> > " <> approve) superUser <## " Group is not approved: SimpleX-Directory is not an admin." groupNotFound cath "privacy" bob ##> "/mr privacy SimpleX-Directory admin" - bob <## "#privacy: you changed the role of SimpleX-Directory from member to admin" + bob <## "#privacy: you changed the role of SimpleX-Directory to admin" bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is submitted for approval." @@ -940,7 +940,7 @@ testListUserGroups ps = -- with de-listed group groupFound cath "anonymity" cath ##> "/mr anonymity SimpleX-Directory member" - cath <## "#anonymity: you changed the role of SimpleX-Directory from admin to member" + cath <## "#anonymity: you changed the role of SimpleX-Directory to member" cath <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (anonymity) is changed to member." cath <## "" cath <## "The group is no longer listed in the directory." diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 6625fb8094..14539ac219 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -173,7 +173,8 @@ chatGroupTests = do it "messages are fully deleted" testBlockForAllFullDelete it "another admin can unblock" testBlockForAllAnotherAdminUnblocks it "member was blocked before joining group" testBlockForAllBeforeJoining - it "can't repeat block, unblock" testBlockForAllCantRepeat + it "repeat block, unblock" testBlockForAllRepeat + it "block multiple members" testBlockForAllMultipleMembers describe "group member inactivity" $ do it "mark member inactive on reaching quota" testGroupMemberInactive describe "group member reports" $ do @@ -265,7 +266,7 @@ testGroupShared alice bob cath checkMessages = do -- test observer role alice ##> "/mr team bob observer" concurrentlyN_ - [ alice <## "#team: you changed the role of bob from admin to observer", + [ alice <## "#team: you changed the role of bob to observer", bob <## "#team: alice changed your role from admin to observer", cath <## "#team: alice changed the role of bob from admin to observer" ] @@ -280,7 +281,7 @@ testGroupShared alice bob cath checkMessages = do ] alice ##> "/mr team bob admin" concurrentlyN_ - [ alice <## "#team: you changed the role of bob from observer to admin", + [ alice <## "#team: you changed the role of bob to admin", bob <## "#team: alice changed your role from observer to admin", cath <## "#team: alice changed the role of bob from observer to admin" ] @@ -1460,7 +1461,7 @@ testUpdateMemberRole = alice <## "to add members use /a team or /create link #team" addMember "team" alice bob GRAdmin alice ##> "/mr team bob member" - alice <## "#team: you changed the role of bob from admin to member" + alice <## "#team: you changed the role of bob to member" bob <## "#team: alice invites you to join the group as member" bob <## "use /j team to accept" bob ##> "/j team" @@ -1472,7 +1473,7 @@ testUpdateMemberRole = bob <## "#team: you have insufficient permissions for this action, the required role is admin" alice ##> "/mr team bob admin" concurrently_ - (alice <## "#team: you changed the role of bob from member to admin") + (alice <## "#team: you changed the role of bob to admin") (bob <## "#team: alice changed your role from member to admin") bob ##> "/a team cath owner" bob <## "#team: you have insufficient permissions for this action, the required role is owner" @@ -1488,13 +1489,7 @@ testUpdateMemberRole = alice <## "#team: new member cath is connected" ] alice ##> "/mr team alice admin" - concurrentlyN_ - [ alice <## "#team: you changed your role from owner to admin", - bob <## "#team: alice changed the role from owner to admin", - cath <## "#team: alice changed the role from owner to admin" - ] - alice ##> "/d #team" - alice <## "#team: you have insufficient permissions for this action, the required role is owner" + alice <## "bad chat command: can't change role for self" testGroupDescription :: HasCallStack => TestParams -> IO () testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do @@ -1579,7 +1574,7 @@ testGroupModerate = -- disableFullDeletion3 "team" alice bob cath alice ##> "/mr team cath member" concurrentlyN_ - [ alice <## "#team: you changed the role of cath from admin to member", + [ alice <## "#team: you changed the role of cath to member", bob <## "#team: alice changed the role of cath from admin to member", cath <## "#team: alice changed your role from admin to member" ] @@ -1662,7 +1657,7 @@ testGroupModerateFullDelete = -- disableFullDeletion3 "team" alice bob cath alice ##> "/mr team cath member" concurrentlyN_ - [ alice <## "#team: you changed the role of cath from admin to member", + [ alice <## "#team: you changed the role of cath to member", bob <## "#team: alice changed the role of cath from admin to member", cath <## "#team: alice changed your role from admin to member" ] @@ -2691,7 +2686,7 @@ testGroupLinkMemberRole = bob <## "#team: you don't have permission to send messages" alice ##> "/mr #team bob member" - alice <## "#team: you changed the role of bob from observer to member" + alice <## "#team: you changed the role of bob to member" bob <## "#team: alice changed your role from observer to member" bob #> "#team hey now" @@ -2721,7 +2716,7 @@ testGroupLinkMemberRole = cath <## "#team: you don't have permission to send messages" alice ##> "/mr #team cath admin" - alice <## "#team: you changed the role of cath from observer to admin" + alice <## "#team: you changed the role of cath to admin" cath <## "#team: alice changed your role from observer to admin" bob <## "#team: alice changed the role of cath from observer to admin" @@ -2730,7 +2725,7 @@ testGroupLinkMemberRole = bob <# "#team cath> hey" cath ##> "/mr #team bob admin" - cath <## "#team: you changed the role of bob from member to admin" + cath <## "#team: you changed the role of bob to admin" bob <## "#team: cath changed your role from member to admin" alice <## "#team: cath changed the role of bob from member to admin" @@ -4132,14 +4127,14 @@ testGroupMsgForwardReport = alice ##> "/mr team bob moderator" concurrentlyN_ - [ alice <## "#team: you changed the role of bob from admin to moderator", + [ alice <## "#team: you changed the role of bob to moderator", bob <## "#team: alice changed your role from admin to moderator", cath <## "#team: alice changed the role of bob from admin to moderator" ] alice ##> "/mr team cath member" concurrentlyN_ - [ alice <## "#team: you changed the role of cath from admin to member", + [ alice <## "#team: you changed the role of cath to member", bob <## "#team: alice changed the role of cath from admin to member", cath <## "#team: alice changed your role from admin to member" ] @@ -4157,7 +4152,7 @@ testGroupMsgForwardReport = alice ##> "/mr team bob member" concurrentlyN_ - [ alice <## "#team: you changed the role of bob from moderator to member", + [ alice <## "#team: you changed the role of bob to member", bob <## "#team: alice changed your role from moderator to member", cath <## "#team: alice changed the role of bob from moderator to member" ] @@ -4315,7 +4310,7 @@ testGroupMsgForwardChangeRole = setupGroupForwarding3 "team" alice bob cath cath ##> "/mr #team bob member" - cath <## "#team: you changed the role of bob from admin to member" + cath <## "#team: you changed the role of bob to member" alice <## "#team: cath changed the role of bob from admin to member" bob <## "#team: cath changed your role from admin to member" -- TODO show as forwarded @@ -5942,19 +5937,13 @@ testBlockForAllBeforeJoining = cc <## "#team: alice added dan (Daniel) to the group (connecting...)" cc <## "#team: new member dan is connected" -testBlockForAllCantRepeat :: HasCallStack => TestParams -> IO () -testBlockForAllCantRepeat = +testBlockForAllRepeat :: HasCallStack => TestParams -> IO () +testBlockForAllRepeat = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath -- disableFullDeletion3 "team" alice bob cath - alice ##> "/unblock for all #team bob" - alice <## "bad chat command: already unblocked" - - cath ##> "/unblock for all #team bob" - cath <## "bad chat command: already unblocked" - bob #> "#team 1" [alice, cath] *<# "#team bob> 1" @@ -5964,10 +5953,10 @@ testBlockForAllCantRepeat = bob "/block for all #team bob" - alice <## "bad chat command: already blocked" + alice <## "#team: you blocked bob" cath ##> "/block for all #team bob" - cath <## "bad chat command: already blocked" + cath <## "#team: you blocked bob" bob #> "#team 2" alice <# "#team bob> 2 [blocked by admin] " @@ -5979,16 +5968,92 @@ testBlockForAllCantRepeat = bob "/unblock for all #team bob" - alice <## "bad chat command: already unblocked" + alice <## "#team: you unblocked bob" cath ##> "/unblock for all #team bob" - cath <## "bad chat command: already unblocked" + cath <## "#team: you unblocked bob" bob #> "#team 3" [alice, cath] *<# "#team bob> 3" bob #$> ("/_get chat #1 count=3", chat, [(1, "1"), (1, "2"), (1, "3")]) +testBlockForAllMultipleMembers :: HasCallStack => TestParams -> IO () +testBlockForAllMultipleMembers = + testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + createGroup3 "team" alice bob cath + + connectUsers alice dan + addMember "team" alice dan GRMember + dan ##> "/j team" + concurrentlyN_ + [ alice <## "#team: dan joined the group", + do + dan <## "#team: you joined the group" + dan + <### [ "#team: member bob (Bob) is connected", + "#team: member cath (Catherine) is connected" + ], + do + bob <## "#team: alice added dan (Daniel) to the group (connecting...)" + bob <## "#team: new member dan is connected", + do + cath <## "#team: alice added dan (Daniel) to the group (connecting...)" + cath <## "#team: new member dan is connected" + ] + + -- lower roles to for batch block to be allowed (can't batch block if admins are selected) + alice ##> "/mr team bob member" + concurrentlyN_ + [ alice <## "#team: you changed the role of bob to member", + bob <## "#team: alice changed your role from admin to member", + cath <## "#team: alice changed the role of bob from admin to member", + dan <## "#team: alice changed the role of bob from admin to member" + ] + alice ##> "/mr team cath member" + concurrentlyN_ + [ alice <## "#team: you changed the role of cath to member", + bob <## "#team: alice changed the role of cath from admin to member", + cath <## "#team: alice changed your role from admin to member", + dan <## "#team: alice changed the role of cath from admin to member" + ] + + bob #> "#team 1" + [alice, cath, dan] *<# "#team bob> 1" + + cath #> "#team 2" + [alice, bob, dan] *<# "#team cath> 2" + + alice ##> "/_block #1 2,3 blocked=on" + alice <## "#team: you blocked 2 members" + dan <## "#team: alice blocked bob" + dan <## "#team: alice blocked cath" + bob "#team 3" + [alice, dan] *<# "#team bob> 3 [blocked by admin] " + cath <# "#team bob> 3" + + cath #> "#team 4" + [alice, dan] *<# "#team cath> 4 [blocked by admin] " + bob <# "#team cath> 4" + + alice ##> "/_block #1 2,3 blocked=off" + alice <## "#team: you unblocked 2 members" + dan <## "#team: alice unblocked bob" + dan <## "#team: alice unblocked cath" + bob "#team 5" + [alice, cath, dan] *<# "#team bob> 5" + + cath #> "#team 6" + [alice, bob, dan] *<# "#team cath> 6" + testGroupMemberInactive :: HasCallStack => TestParams -> IO () testGroupMemberInactive ps = do withSmpServer' serverCfg' $ do @@ -6067,13 +6132,13 @@ testGroupMemberReports = -- disableFullDeletion3 "jokes" alice bob cath alice ##> "/mr jokes bob moderator" concurrentlyN_ - [ alice <## "#jokes: you changed the role of bob from admin to moderator", + [ alice <## "#jokes: you changed the role of bob to moderator", bob <## "#jokes: alice changed your role from admin to moderator", cath <## "#jokes: alice changed the role of bob from admin to moderator" ] alice ##> "/mr jokes cath member" concurrentlyN_ - [ alice <## "#jokes: you changed the role of cath from admin to member", + [ alice <## "#jokes: you changed the role of cath to member", bob <## "#jokes: alice changed the role of cath from admin to member", cath <## "#jokes: alice changed your role from admin to member" ] diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 4ab5e8fbd3..5d2b9f5ba8 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -773,7 +773,7 @@ testBusinessUpdateProfiles = withTestOutput $ testChat4 businessProfile alicePro biz <# "#alisa alisa_1> hello again" -- customer can invite members too, if business allows biz ##> "/mr alisa alisa_1 admin" - biz <## "#alisa: you changed the role of alisa_1 from member to admin" + biz <## "#alisa: you changed the role of alisa_1 to admin" alice <## "#biz: biz_1 changed your role from member to admin" connectUsers alice bob alice ##> "/a #biz bob" From 1b757911fa0bcb605a90e45643b9538626023d84 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 1 Mar 2025 01:55:17 +0700 Subject: [PATCH 15/24] ui: batch apis for members (#5681) * ui: batch apis for members * ios --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Model/SimpleXAPI.swift | 18 ++--- .../Views/Chat/Group/GroupChatInfoView.swift | 8 ++- .../Chat/Group/GroupMemberInfoView.swift | 24 ++++--- apps/ios/SimpleXChat/APITypes.swift | 36 +++++----- .../chat/simplex/common/model/SimpleXAPI.kt | 70 ++++++++++--------- .../views/chat/group/GroupChatInfoView.kt | 12 ++-- .../views/chat/group/GroupMemberInfoView.kt | 32 ++++++--- 7 files changed, 114 insertions(+), 86 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 354f8243fc..545e90763f 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1588,21 +1588,21 @@ func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult { } } -func apiRemoveMember(_ groupId: Int64, _ memberId: Int64) async throws -> GroupMember { - let r = await chatSendCmd(.apiRemoveMember(groupId: groupId, memberId: memberId), bgTask: false) - if case let .userDeletedMember(_, _, member) = r { return member } +func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64]) async throws -> [GroupMember] { + let r = await chatSendCmd(.apiRemoveMembers(groupId: groupId, memberIds: memberIds), bgTask: false) + if case let .userDeletedMembers(_, _, members) = r { return members } throw r } -func apiMemberRole(_ groupId: Int64, _ memberId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember { - let r = await chatSendCmd(.apiMemberRole(groupId: groupId, memberId: memberId, memberRole: memberRole), bgTask: false) - if case let .memberRoleUser(_, _, member, _, _) = r { return member } +func apiMembersRole(_ groupId: Int64, _ memberIds: [Int64], _ memberRole: GroupMemberRole) async throws -> [GroupMember] { + let r = await chatSendCmd(.apiMembersRole(groupId: groupId, memberIds: memberIds, memberRole: memberRole), bgTask: false) + if case let .membersRoleUser(_, _, members, _) = r { return members } throw r } -func apiBlockMemberForAll(_ groupId: Int64, _ memberId: Int64, _ blocked: Bool) async throws -> GroupMember { - let r = await chatSendCmd(.apiBlockMemberForAll(groupId: groupId, memberId: memberId, blocked: blocked), bgTask: false) - if case let .memberBlockedForAllUser(_, _, member, _) = r { return member } +func apiBlockMembersForAll(_ groupId: Int64, _ memberIds: [Int64], _ blocked: Bool) async throws -> [GroupMember] { + let r = await chatSendCmd(.apiBlockMembersForAll(groupId: groupId, memberIds: memberIds, blocked: blocked), bgTask: false) + if case let .membersBlockedForAllUser(_, _, members, _) = r { return members } throw r } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 2a827de195..56d994b397 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -681,12 +681,14 @@ struct GroupChatInfoView: View { primaryButton: .destructive(Text("Remove")) { Task { do { - let updatedMember = try await apiRemoveMember(groupInfo.groupId, mem.groupMemberId) + let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) await MainActor.run { - _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + updatedMembers.forEach { updatedMember in + _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + } } } catch let error { - logger.error("apiRemoveMember error: \(responseError(error))") + logger.error("apiRemoveMembers error: \(responseError(error))") let a = getErrorAlert(error, "Error removing member") alert = .error(title: a.title, error: a.message) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index f985356289..4a931ed23f 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -610,13 +610,15 @@ struct GroupMemberInfoView: View { primaryButton: .destructive(Text("Remove")) { Task { do { - let updatedMember = try await apiRemoveMember(groupInfo.groupId, mem.groupMemberId) + let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) await MainActor.run { - _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + updatedMembers.forEach { updatedMember in + _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + } dismiss() } } catch let error { - logger.error("apiRemoveMember error: \(responseError(error))") + logger.error("apiRemoveMembers error: \(responseError(error))") let a = getErrorAlert(error, "Error removing member") alert = .error(title: a.title, error: a.message) } @@ -641,14 +643,16 @@ struct GroupMemberInfoView: View { primaryButton: .default(Text("Change")) { Task { do { - let updatedMember = try await apiMemberRole(groupInfo.groupId, mem.groupMemberId, newRole) + let updatedMembers = try await apiMembersRole(groupInfo.groupId, [mem.groupMemberId], newRole) await MainActor.run { - _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + updatedMembers.forEach { updatedMember in + _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + } } } catch let error { newRole = mem.memberRole - logger.error("apiMemberRole error: \(responseError(error))") + logger.error("apiMembersRole error: \(responseError(error))") let a = getErrorAlert(error, "Error changing role") alert = .error(title: a.title, error: a.message) } @@ -800,12 +804,14 @@ func unblockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert { func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) { Task { do { - let updatedMember = try await apiBlockMemberForAll(gInfo.groupId, member.groupMemberId, blocked) + let updatedMembers = try await apiBlockMembersForAll(gInfo.groupId, [member.groupMemberId], blocked) await MainActor.run { - _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember) + updatedMembers.forEach { updatedMember in + _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember) + } } } catch let error { - logger.error("apiBlockMemberForAll error: \(responseError(error))") + logger.error("apiBlockMembersForAll error: \(responseError(error))") } } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index f331f98a67..20c8785b3a 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -71,9 +71,9 @@ public enum ChatCommand { case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile) case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) case apiJoinGroup(groupId: Int64) - case apiMemberRole(groupId: Int64, memberId: Int64, memberRole: GroupMemberRole) - case apiBlockMemberForAll(groupId: Int64, memberId: Int64, blocked: Bool) - case apiRemoveMember(groupId: Int64, memberId: Int64) + case apiMembersRole(groupId: Int64, memberIds: [Int64], memberRole: GroupMemberRole) + case apiBlockMembersForAll(groupId: Int64, memberIds: [Int64], blocked: Bool) + case apiRemoveMembers(groupId: Int64, memberIds: [Int64]) case apiLeaveGroup(groupId: Int64) case apiListMembers(groupId: Int64) case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile) @@ -250,9 +250,9 @@ public enum ChatCommand { case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))" case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" case let .apiJoinGroup(groupId): return "/_join #\(groupId)" - case let .apiMemberRole(groupId, memberId, memberRole): return "/_member role #\(groupId) \(memberId) \(memberRole.rawValue)" - case let .apiBlockMemberForAll(groupId, memberId, blocked): return "/_block #\(groupId) \(memberId) blocked=\(onOff(blocked))" - case let .apiRemoveMember(groupId, memberId): return "/_remove #\(groupId) \(memberId)" + case let .apiMembersRole(groupId, memberIds, memberRole): return "/_member role #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) \(memberRole.rawValue)" + case let .apiBlockMembersForAll(groupId, memberIds, blocked): return "/_block #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) blocked=\(onOff(blocked))" + case let .apiRemoveMembers(groupId, memberIds): return "/_remove #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ","))" case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)" case let .apiListMembers(groupId): return "/_members #\(groupId)" case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))" @@ -424,9 +424,9 @@ public enum ChatCommand { case .apiNewGroup: return "apiNewGroup" case .apiAddMember: return "apiAddMember" case .apiJoinGroup: return "apiJoinGroup" - case .apiMemberRole: return "apiMemberRole" - case .apiBlockMemberForAll: return "apiBlockMemberForAll" - case .apiRemoveMember: return "apiRemoveMember" + case .apiMembersRole: return "apiMembersRole" + case .apiBlockMembersForAll: return "apiBlockMembersForAll" + case .apiRemoveMembers: return "apiRemoveMembers" case .apiLeaveGroup: return "apiLeaveGroup" case .apiListMembers: return "apiListMembers" case .apiUpdateGroupProfile: return "apiUpdateGroupProfile" @@ -681,16 +681,16 @@ public enum ChatResponse: Decodable, Error { case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?) case groupLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember) case businessLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, fromContact: Contact) - case userDeletedMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case userDeletedMembers(user: UserRef, groupInfo: GroupInfo, members: [GroupMember]) case leftMemberUser(user: UserRef, groupInfo: GroupInfo) case groupMembers(user: UserRef, group: Group) case receivedGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole) case groupDeletedUser(user: UserRef, groupInfo: GroupInfo) case joinedGroupMemberConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember) case memberRole(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) - case memberRoleUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) + case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole) case memberBlockedForAll(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, blocked: Bool) - case memberBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, blocked: Bool) + case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool) case deletedMemberUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember) case deletedMember(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember) case leftMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) @@ -861,16 +861,16 @@ public enum ChatResponse: Decodable, Error { case .userAcceptedGroupSent: return "userAcceptedGroupSent" case .groupLinkConnecting: return "groupLinkConnecting" case .businessLinkConnecting: return "businessLinkConnecting" - case .userDeletedMember: return "userDeletedMember" + case .userDeletedMembers: return "userDeletedMembers" case .leftMemberUser: return "leftMemberUser" case .groupMembers: return "groupMembers" case .receivedGroupInvitation: return "receivedGroupInvitation" case .groupDeletedUser: return "groupDeletedUser" case .joinedGroupMemberConnecting: return "joinedGroupMemberConnecting" case .memberRole: return "memberRole" - case .memberRoleUser: return "memberRoleUser" + case .membersRoleUser: return "membersRoleUser" case .memberBlockedForAll: return "memberBlockedForAll" - case .memberBlockedForAllUser: return "memberBlockedForAllUser" + case .membersBlockedForAllUser: return "membersBlockedForAllUser" case .deletedMemberUser: return "deletedMemberUser" case .deletedMember: return "deletedMember" case .leftMember: return "leftMember" @@ -1048,16 +1048,16 @@ public enum ChatResponse: Decodable, Error { case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))") case let .groupLinkConnecting(u, groupInfo, hostMember): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))") case let .businessLinkConnecting(u, groupInfo, hostMember, fromContact): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))\nfromContact: \(String(describing: fromContact))") - case let .userDeletedMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + case let .userDeletedMembers(u, groupInfo, members): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)") case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .groupMembers(u, group): return withUser(u, String(describing: group)) case let .receivedGroupInvitation(u, groupInfo, contact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmemberRole: \(memberRole)") case let .groupDeletedUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .joinedGroupMemberConnecting(u, groupInfo, hostMember, member): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\nmember: \(member)") case let .memberRole(u, groupInfo, byMember, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)") - case let .memberRoleUser(u, groupInfo, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)") + case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)") case let .memberBlockedForAll(u, groupInfo, byMember, member, blocked): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nblocked: \(blocked)") - case let .memberBlockedForAllUser(u, groupInfo, member, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nblocked: \(blocked)") + case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)") case let .deletedMemberUser(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") case let .deletedMember(u, groupInfo, byMember, deletedMember): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\ndeletedMember: \(deletedMember)") case let .leftMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 208a5557a5..c221e3c15e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1995,34 +1995,34 @@ object ChatController { } } - suspend fun apiRemoveMember(rh: Long?, groupId: Long, memberId: Long): GroupMember? = - when (val r = sendCmd(rh, CC.ApiRemoveMember(groupId, memberId))) { - is CR.UserDeletedMember -> r.member + suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List): List? = + when (val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds))) { + is CR.UserDeletedMembers -> r.members else -> { if (!(networkErrorAlert(r))) { - apiErrorAlert("apiRemoveMember", generalGetString(MR.strings.error_removing_member), r) + apiErrorAlert("apiRemoveMembers", generalGetString(MR.strings.error_removing_member), r) } null } } - suspend fun apiMemberRole(rh: Long?, groupId: Long, memberId: Long, memberRole: GroupMemberRole): GroupMember = - when (val r = sendCmd(rh, CC.ApiMemberRole(groupId, memberId, memberRole))) { - is CR.MemberRoleUser -> r.member + suspend fun apiMembersRole(rh: Long?, groupId: Long, memberIds: List, memberRole: GroupMemberRole): List = + when (val r = sendCmd(rh, CC.ApiMembersRole(groupId, memberIds, memberRole))) { + is CR.MembersRoleUser -> r.members else -> { if (!(networkErrorAlert(r))) { - apiErrorAlert("apiMemberRole", generalGetString(MR.strings.error_changing_role), r) + apiErrorAlert("apiMembersRole", generalGetString(MR.strings.error_changing_role), r) } throw Exception("failed to change member role: ${r.responseType} ${r.details}") } } - suspend fun apiBlockMemberForAll(rh: Long?, groupId: Long, memberId: Long, blocked: Boolean): GroupMember = - when (val r = sendCmd(rh, CC.ApiBlockMemberForAll(groupId, memberId, blocked))) { - is CR.MemberBlockedForAllUser -> r.member + suspend fun apiBlockMembersForAll(rh: Long?, groupId: Long, memberIds: List, blocked: Boolean): List = + when (val r = sendCmd(rh, CC.ApiBlockMembersForAll(groupId, memberIds, blocked))) { + is CR.MembersBlockedForAllUser -> r.members else -> { if (!(networkErrorAlert(r))) { - apiErrorAlert("apiBlockMemberForAll", generalGetString(MR.strings.error_blocking_member_for_all), r) + apiErrorAlert("apiBlockMembersForAll", generalGetString(MR.strings.error_blocking_member_for_all), r) } throw Exception("failed to block member for all: ${r.responseType} ${r.details}") } @@ -2723,13 +2723,17 @@ object ChatController { upsertGroupMember(rhId, r.groupInfo, r.member) } } - is CR.MemberRoleUser -> + is CR.MembersRoleUser -> if (active(r.user)) { withChats { - upsertGroupMember(rhId, r.groupInfo, r.member) + r.members.forEach { member -> + upsertGroupMember(rhId, r.groupInfo, member) + } } withReportsChatsIfOpen { - upsertGroupMember(rhId, r.groupInfo, r.member) + r.members.forEach { member -> + upsertGroupMember(rhId, r.groupInfo, member) + } } } is CR.MemberBlockedForAll -> @@ -3406,9 +3410,9 @@ sealed class CC { class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC() class ApiJoinGroup(val groupId: Long): CC() - class ApiMemberRole(val groupId: Long, val memberId: Long, val memberRole: GroupMemberRole): CC() - class ApiBlockMemberForAll(val groupId: Long, val memberId: Long, val blocked: Boolean): CC() - class ApiRemoveMember(val groupId: Long, val memberId: Long): CC() + class ApiMembersRole(val groupId: Long, val memberIds: List, val memberRole: GroupMemberRole): CC() + class ApiBlockMembersForAll(val groupId: Long, val memberIds: List, val blocked: Boolean): CC() + class ApiRemoveMembers(val groupId: Long, val memberIds: List): CC() class ApiLeaveGroup(val groupId: Long): CC() class ApiListMembers(val groupId: Long): CC() class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC() @@ -3591,9 +3595,9 @@ sealed class CC { is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}" is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}" is ApiJoinGroup -> "/_join #$groupId" - is ApiMemberRole -> "/_member role #$groupId $memberId ${memberRole.memberRole}" - is ApiBlockMemberForAll -> "/_block #$groupId $memberId blocked=${onOff(blocked)}" - is ApiRemoveMember -> "/_remove #$groupId $memberId" + is ApiMembersRole -> "/_member role #$groupId ${memberIds.joinToString(",")} ${memberRole.memberRole}" + is ApiBlockMembersForAll -> "/_block #$groupId ${memberIds.joinToString(",")} blocked=${onOff(blocked)}" + is ApiRemoveMembers -> "/_remove #$groupId ${memberIds.joinToString(",")}" is ApiLeaveGroup -> "/_leave #$groupId" is ApiListMembers -> "/_members #$groupId" is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}" @@ -3754,9 +3758,9 @@ sealed class CC { is ApiNewGroup -> "apiNewGroup" is ApiAddMember -> "apiAddMember" is ApiJoinGroup -> "apiJoinGroup" - is ApiMemberRole -> "apiMemberRole" - is ApiBlockMemberForAll -> "apiBlockMemberForAll" - is ApiRemoveMember -> "apiRemoveMember" + is ApiMembersRole -> "apiMembersRole" + is ApiBlockMembersForAll -> "apiBlockMembersForAll" + is ApiRemoveMembers -> "apiRemoveMembers" is ApiLeaveGroup -> "apiLeaveGroup" is ApiListMembers -> "apiListMembers" is ApiUpdateGroupProfile -> "apiUpdateGroupProfile" @@ -5801,16 +5805,16 @@ sealed class CR { @Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val user: UserRef, val groupInfo: GroupInfo, val hostContact: Contact? = null): CR() @Serializable @SerialName("groupLinkConnecting") class GroupLinkConnecting (val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember): CR() @Serializable @SerialName("businessLinkConnecting") class BusinessLinkConnecting (val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val fromContact: Contact): CR() - @Serializable @SerialName("userDeletedMember") class UserDeletedMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("userDeletedMembers") class UserDeletedMembers(val user: UserRef, val groupInfo: GroupInfo, val members: List): CR() @Serializable @SerialName("leftMemberUser") class LeftMemberUser(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("groupMembers") class GroupMembers(val user: UserRef, val group: Group): CR() @Serializable @SerialName("receivedGroupInvitation") class ReceivedGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val memberRole: GroupMemberRole): CR() @Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR() @Serializable @SerialName("memberRole") class MemberRole(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() - @Serializable @SerialName("memberRoleUser") class MemberRoleUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() + @Serializable @SerialName("membersRoleUser") class MembersRoleUser(val user: UserRef, val groupInfo: GroupInfo, val members: List, val toRole: GroupMemberRole): CR() @Serializable @SerialName("memberBlockedForAll") class MemberBlockedForAll(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val blocked: Boolean): CR() - @Serializable @SerialName("memberBlockedForAllUser") class MemberBlockedForAllUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val blocked: Boolean): CR() + @Serializable @SerialName("membersBlockedForAllUser") class MembersBlockedForAllUser(val user: UserRef, val groupInfo: GroupInfo, val members: List, val blocked: Boolean): CR() @Serializable @SerialName("deletedMemberUser") class DeletedMemberUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("deletedMember") class DeletedMember(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember): CR() @Serializable @SerialName("leftMember") class LeftMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @@ -5986,16 +5990,16 @@ sealed class CR { is UserAcceptedGroupSent -> "userAcceptedGroupSent" is GroupLinkConnecting -> "groupLinkConnecting" is BusinessLinkConnecting -> "businessLinkConnecting" - is UserDeletedMember -> "userDeletedMember" + is UserDeletedMembers -> "userDeletedMembers" is LeftMemberUser -> "leftMemberUser" is GroupMembers -> "groupMembers" is ReceivedGroupInvitation -> "receivedGroupInvitation" is GroupDeletedUser -> "groupDeletedUser" is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting" is MemberRole -> "memberRole" - is MemberRoleUser -> "memberRoleUser" + is MembersRoleUser -> "membersRoleUser" is MemberBlockedForAll -> "memberBlockedForAll" - is MemberBlockedForAllUser -> "memberBlockedForAllUser" + is MembersBlockedForAllUser -> "membersBlockedForAllUser" is DeletedMemberUser -> "deletedMemberUser" is DeletedMember -> "deletedMember" is LeftMember -> "leftMember" @@ -6164,16 +6168,16 @@ sealed class CR { is UserAcceptedGroupSent -> json.encodeToString(groupInfo) is GroupLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember") is BusinessLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nfromContact: $fromContact") - is UserDeletedMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is UserDeletedMembers -> withUser(user, "groupInfo: $groupInfo\nmembers: $members") is LeftMemberUser -> withUser(user, json.encodeToString(groupInfo)) is GroupMembers -> withUser(user, json.encodeToString(group)) is ReceivedGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmemberRole: $memberRole") is GroupDeletedUser -> withUser(user, json.encodeToString(groupInfo)) is JoinedGroupMemberConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member") is MemberRole -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole") - is MemberRoleUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole") + is MembersRoleUser -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\ntoRole: $toRole") is MemberBlockedForAll -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nblocked: $blocked") - is MemberBlockedForAllUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nblocked: $blocked") + is MembersBlockedForAllUser -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\nblocked: $blocked") is DeletedMemberUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is DeletedMember -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\ndeletedMember: $deletedMember") is LeftMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index b3dc333ace..441fe313d4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -213,13 +213,17 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe confirmText = generalGetString(MR.strings.remove_member_confirmation), onConfirm = { withBGApi { - val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId) - if (updatedMember != null) { + val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, listOf(mem.groupMemberId)) + if (updatedMembers != null) { withChats { - upsertGroupMember(rhId, groupInfo, updatedMember) + updatedMembers.forEach { updatedMember -> + upsertGroupMember(rhId, groupInfo, updatedMember) + } } withReportsChatsIfOpen { - upsertGroupMember(rhId, groupInfo, updatedMember) + updatedMembers.forEach { updatedMember -> + upsertGroupMember(rhId, groupInfo, updatedMember) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index ef1c69a5bb..41e4df8792 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -142,12 +142,16 @@ fun GroupMemberInfoView( }) { withBGApi { kotlin.runCatching { - val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it) + val members = chatModel.controller.apiMembersRole(rhId, groupInfo.groupId, listOf(member.groupMemberId), it) withChats { - upsertGroupMember(rhId, groupInfo, mem) + members.forEach { member -> + upsertGroupMember(rhId, groupInfo, member) + } } withReportsChatsIfOpen { - upsertGroupMember(rhId, groupInfo, mem) + members.forEach { member -> + upsertGroupMember(rhId, groupInfo, member) + } } }.onFailure { newRole.value = prevValue @@ -257,13 +261,17 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c confirmText = generalGetString(MR.strings.remove_member_confirmation), onConfirm = { withBGApi { - val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId) - if (removedMember != null) { + val removedMembers = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId)) + if (removedMembers != null) { withChats { - upsertGroupMember(rhId, groupInfo, removedMember) + removedMembers.forEach { removedMember -> + upsertGroupMember(rhId, groupInfo, removedMember) + } } withReportsChatsIfOpen { - upsertGroupMember(rhId, groupInfo, removedMember) + removedMembers.forEach { removedMember -> + upsertGroupMember(rhId, groupInfo, removedMember) + } } } close?.invoke() @@ -804,12 +812,16 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) { withBGApi { - val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked) + val updatedMembers = ChatController.apiBlockMembersForAll(rhId, gInfo.groupId, listOf(member.groupMemberId), blocked) withChats { - upsertGroupMember(rhId, gInfo, updatedMember) + updatedMembers.forEach { updatedMember -> + upsertGroupMember(rhId, gInfo, updatedMember) + } } withReportsChatsIfOpen { - upsertGroupMember(rhId, gInfo, updatedMember) + updatedMembers.forEach { updatedMember -> + upsertGroupMember(rhId, gInfo, updatedMember) + } } } } From 50232fd1799f05c9db4f2fce917f797635e595d4 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 1 Mar 2025 01:55:49 +0700 Subject: [PATCH 16/24] android, desktop: go to forwarded item or search result (#5666) * android, desktop: go to forwarded item or search result * changes * reactions back * button appearance * indentation * change * rename variable * rename function * rename variable * rename variable * fix scroll position --- .../chat/simplex/common/model/ChatModel.kt | 8 + .../simplex/common/views/chat/ChatInfoView.kt | 3 +- .../common/views/chat/ChatItemsLoader.kt | 29 +- .../simplex/common/views/chat/ChatView.kt | 62 +- .../common/views/chat/item/ChatItemView.kt | 913 ++++++++++-------- .../common/views/chat/item/FramedItemView.kt | 2 +- .../views/chatlist/ChatListNavLinkView.kt | 22 +- 7 files changed, 591 insertions(+), 448 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 7ef8123d2f..f784dcb9ed 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -65,6 +65,7 @@ object ChatModel { // current chat val chatId = mutableStateOf(null) + val openAroundItemId: MutableState = mutableStateOf(null) val chatsContext = ChatsContext(null) val reportsChatsContext = ChatsContext(MsgContentTag.Report) // declaration of chatsContext should be before any other variable that is taken from ChatsContext class and used in the model, otherwise, strange crash with NullPointerException for "this" parameter in random functions @@ -3111,6 +3112,13 @@ sealed class CIForwardedFrom { is Group -> chatName } + val chatTypeApiIdMsgId: Triple? + get() = when (this) { + Unknown -> null + is Contact -> if (contactId != null) Triple(ChatType.Direct, contactId, chatItemId) else null + is Group -> if (groupId != null) Triple(ChatType.Group, groupId, chatItemId) else null + } + fun text(chatType: ChatType): String = if (chatType == ChatType.Local) { if (chatName.isEmpty()) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 23ce191557..a730bd1b71 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -1398,7 +1398,8 @@ private suspend fun afterSetChatTTL(rhId: Long?, chatInfo: ChatInfo, progressInd chat, navInfo, contentTag = null, - pagination = pagination + pagination = pagination, + openAroundItemId = null ) } catch (e: Exception) { Log.e(TAG, "apiGetChat error: ${e.stackTraceToString()}") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt index 6a8c277ae1..9050804db4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt @@ -28,13 +28,15 @@ suspend fun apiLoadMessages( contentTag: MsgContentTag?, pagination: ChatPagination, search: String = "", + openAroundItemId: Long? = null, visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 } ) = coroutineScope { val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, contentTag, pagination, search) ?: return@coroutineScope // For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes - if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last) + /** When [openAroundItemId] is provided, chatId can be different too */ + if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last && openAroundItemId == null) || !isActive) return@coroutineScope - processLoadedChat(chat, navInfo, contentTag, pagination, visibleItemIndexesNonReversed) + processLoadedChat(chat, navInfo, contentTag, pagination, openAroundItemId, visibleItemIndexesNonReversed) } suspend fun processLoadedChat( @@ -42,6 +44,7 @@ suspend fun processLoadedChat( navInfo: NavigationInfo, contentTag: MsgContentTag?, pagination: ChatPagination, + openAroundItemId: Long?, visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 } ) { val chatState = chatModel.chatStateForContent(contentTag) @@ -67,7 +70,7 @@ suspend fun processLoadedChat( withChats(contentTag) { chatItemStatuses.clear() chatItems.replaceAll(chat.chatItems) - chatModel.chatId.value = chat.chatInfo.id + chatModel.chatId.value = chat.id splits.value = newSplits if (chat.chatItems.isNotEmpty()) { unreadAfterItemId.value = chat.chatItems.last().id @@ -119,10 +122,15 @@ suspend fun processLoadedChat( } } is ChatPagination.Around -> { - newItems.addAll(oldItems) - val newSplits = removeDuplicatesAndUpperSplits(newItems, chat, splits, visibleItemIndexesNonReversed) + val newSplits = if (openAroundItemId == null) { + newItems.addAll(oldItems) + removeDuplicatesAndUpperSplits(newItems, chat, splits, visibleItemIndexesNonReversed) + } else { + emptyList() + } // currently, items will always be added on top, which is index 0 newItems.addAll(0, chat.chatItems) + withChats(contentTag) { chatItems.replaceAll(newItems) splits.value = listOf(chat.chatItems.last().id) + newSplits @@ -130,8 +138,15 @@ suspend fun processLoadedChat( totalAfter.value = navInfo.afterTotal unreadTotal.value = chat.chatStats.unreadCount unreadAfter.value = navInfo.afterUnread - // no need to set it, count will be wrong - // unreadAfterNewestLoaded.value = navInfo.afterUnread + + if (openAroundItemId != null) { + unreadAfterNewestLoaded.value = navInfo.afterUnread + chatModel.openAroundItemId.value = openAroundItemId + chatModel.chatId.value = chat.id + } else { + // no need to set it, count will be wrong + // unreadAfterNewestLoaded.value = navInfo.afterUnread + } } } is ChatPagination.Last -> { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 8b94f36d75..f9a7f25f48 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -133,9 +133,12 @@ fun ChatView( SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) { val onSearchValueChanged: (String) -> Unit = onSearchValueChanged@{ value -> - if (searchText.value == value) return@onSearchValueChanged - val c = chatModel.getChat(chatInfo.id) ?: return@onSearchValueChanged - if (chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged + val sameText = searchText.value == value + // showSearch can be false with empty text when it was closed manually after clicking on message from search to load .around it + // (required on Android to have this check to prevent call to search with old text) + val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && contentTag == null + val c = chatModel.getChat(chatInfo.id) + if (sameText || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged withBGApi { apiFindMessages(c, value, contentTag) searchText.value = value @@ -344,7 +347,7 @@ fun ChatView( val c = chatModel.getChat(chatId) if (chatModel.chatId.value != chatId) return@ChatLayout if (c != null) { - apiLoadMessages(c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, contentTag, pagination, searchText.value, visibleItemIndexes) + apiLoadMessages(c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, contentTag, pagination, searchText.value, null, visibleItemIndexes) } }, deleteMessage = { itemId, mode -> @@ -602,6 +605,10 @@ fun ChatView( }, changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) }, onSearchValueChanged = onSearchValueChanged, + closeSearch = { + showSearch.value = false + searchText.value = "" + }, onComposed, developerTools = chatModel.controller.appPrefs.developerTools.get(), showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), @@ -699,6 +706,7 @@ fun ChatLayout( markChatRead: () -> Unit, changeNtfsState: (MsgFilter, currentValue: MutableState) -> Unit, onSearchValueChanged: (String) -> Unit, + closeSearch: () -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, showViaProxy: Boolean, @@ -751,7 +759,7 @@ fun ChatLayout( useLinkPreviews, linkMode, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, archiveReports, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, - setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy, + setReaction, showItemDetails, markItemsRead, markChatRead, closeSearch, remember { { onComposed(it) } }, developerTools, showViaProxy, ) } if (chatInfo is ChatInfo.Group && composeState.value.message.text.isNotEmpty()) { @@ -1160,11 +1168,13 @@ fun BoxScope.ChatItemsList( showItemDetails: (ChatInfo, ChatItem) -> Unit, markItemsRead: (List) -> Unit, markChatRead: () -> Unit, + closeSearch: () -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, showViaProxy: Boolean ) { val searchValueIsEmpty = remember { derivedStateOf { searchValue.value.isEmpty() } } + val searchValueIsNotBlank = remember { derivedStateOf { searchValue.value.isNotBlank() } } val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf()) } val contentTag = LocalContentTag.current // not using reversedChatItems inside to prevent possible derivedState bug in Compose when one derived state access can cause crash asking another derived state @@ -1177,15 +1187,29 @@ fun BoxScope.ChatItemsList( val reportsCount = reportsCount(chatInfo.id) val topPaddingToContent = topPaddingToContent(chatView = contentTag == null, contentTag == null && reportsCount > 0) val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() }) + val numberOfBottomAppBars = numberOfBottomAppBars() /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears * */ val maxHeightForList = rememberUpdatedState( - with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * 2).roundToPx() } + with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * numberOfBottomAppBars).roundToPx() } ) - val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, saver = LazyListState.Saver) { - val index = mergedItems.value.items.indexOfLast { it.hasUnread() } + val resetListState = remember { mutableStateOf(false) } + remember(chatModel.openAroundItemId.value) { + if (chatModel.openAroundItemId.value != null) { + closeSearch() + resetListState.value = !resetListState.value + } + } + val highlightedItems = remember { mutableStateOf(setOf()) } + val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, resetListState.value, saver = LazyListState.Saver) { + val openAroundItemId = chatModel.openAroundItemId.value + val index = mergedItems.value.indexInParentItems[openAroundItemId] ?: mergedItems.value.items.indexOfLast { it.hasUnread() } val reportsState = reportsListState + if (openAroundItemId != null) { + highlightedItems.value += openAroundItemId + chatModel.openAroundItemId.value = null + } if (reportsState != null) { reportsListState = null reportsState @@ -1221,7 +1245,6 @@ fun BoxScope.ChatItemsList( val remoteHostIdUpdated = rememberUpdatedState(remoteHostId) val chatInfoUpdated = rememberUpdatedState(chatInfo) - val highlightedItems = remember { mutableStateOf(setOf()) } val scope = rememberCoroutineScope() val scrollToItem: (Long) -> Unit = remember { // In group reports just set the itemId to scroll to so the main ChatView will handle scrolling @@ -1238,7 +1261,7 @@ fun BoxScope.ChatItemsList( scrollToItemId.value = null } } } - LoadLastItems(loadingMoreItems, remoteHostId, chatInfo) + LoadLastItems(loadingMoreItems, resetListState, remoteHostId, chatInfo) SmallScrollOnNewMessage(listState, reversedChatItems) val finishedInitialComposition = remember { mutableStateOf(false) } NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed) @@ -1296,7 +1319,7 @@ fun BoxScope.ChatItemsList( highlightedItems.value = setOf() } } - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @@ -1567,9 +1590,9 @@ fun BoxScope.ChatItemsList( } @Composable -private fun LoadLastItems(loadingMoreItems: MutableState, remoteHostId: Long?, chatInfo: ChatInfo) { +private fun LoadLastItems(loadingMoreItems: MutableState, resetListState: State, remoteHostId: Long?, chatInfo: ChatInfo) { val contentTag = LocalContentTag.current - LaunchedEffect(remoteHostId, chatInfo.id) { + LaunchedEffect(remoteHostId, chatInfo.id, resetListState.value) { try { loadingMoreItems.value = true if (chatModel.chatStateForContent(contentTag).totalAfter.value <= 0) return@LaunchedEffect @@ -1888,6 +1911,17 @@ fun topPaddingToContent(chatView: Boolean, additionalTopBar: Boolean = false): D } } +@Composable +private fun numberOfBottomAppBars(): Int { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val chatBottomBar = remember { appPrefs.chatBottomBar.state } + return if (oneHandUI.value && chatBottomBar.value) { + 2 + } else { + 1 + } +} + @Composable private fun FloatingDate( modifier: Modifier, @@ -2798,6 +2832,7 @@ fun PreviewChatLayout() { markChatRead = {}, changeNtfsState = { _, _ -> }, onSearchValueChanged = {}, + closeSearch = {}, onComposed = {}, developerTools = false, showViaProxy = false, @@ -2874,6 +2909,7 @@ fun PreviewGroupChatLayout() { markChatRead = {}, changeNtfsState = { _, _ -> }, onSearchValueChanged = {}, + closeSearch = {}, onComposed = {}, developerTools = false, showViaProxy = false, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 8c053ad1d2..e4bcc965c4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -3,14 +3,14 @@ package chat.simplex.common.views.chat.item import SectionItemView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* -import androidx.compose.foundation.interaction.HoverInteraction -import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.* import androidx.compose.ui.graphics.* @@ -31,8 +31,10 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.group.LocalContentTag +import chat.simplex.common.views.chatlist.openChat import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource import kotlinx.datetime.Clock import kotlin.math.* @@ -73,6 +75,7 @@ fun ChatItemView( highlighted: State, range: State, selectedChatItems: MutableState?>, + searchIsNotBlank: State, fillMaxWidth: Boolean = true, selectChatItem: () -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, @@ -112,8 +115,16 @@ fun ChatItemView( val onLinkLongClick = { _: String -> showMenu.value = true } val live = remember { derivedStateOf { composeState.value.liveMessage != null } }.value + val bubbleInteractionSource = remember { MutableInteractionSource() } + val lineInteractionSource = remember { MutableInteractionSource() } + val bubblePressed = bubbleInteractionSource.collectIsPressedAsState() + val linePressed = lineInteractionSource.collectIsPressedAsState() + val lineHovered = lineInteractionSource.collectIsHoveredAsState() + val lineActivated = remember { derivedStateOf { lineHovered.value || linePressed.value || bubblePressed.value } } Box( - modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier, + modifier = (if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier) + .hoverable(lineInteractionSource) + .clickable(onClick = {}, interactionSource = lineInteractionSource, indication = null), contentAlignment = alignment, ) { val info = cItem.meta.itemStatus.statusInto @@ -228,197 +239,289 @@ fun ChatItemView( } } - Column(horizontalAlignment = if (cItem.chatDir.sent) Alignment.End else Alignment.Start) { - val interactionSource = remember { MutableInteractionSource() } - val enterInteraction = remember { HoverInteraction.Enter() } - KeyChangeEffect(highlighted.value) { - if (highlighted.value) { - interactionSource.emit(enterInteraction) - } else { - interactionSource.emit(HoverInteraction.Exit(enterInteraction)) + @Composable + fun GoToItemInnerButton(alignStart: Boolean, icon: ImageResource, parentActivated: State, onClick: () -> Unit) { + val buttonInteractionSource = remember { MutableInteractionSource() } + val buttonHovered = buttonInteractionSource.collectIsHoveredAsState() + val buttonPressed = buttonInteractionSource.collectIsPressedAsState() + val buttonActivated = remember { derivedStateOf { buttonHovered.value || buttonPressed.value } } + IconButton( + onClick, + Modifier + .padding(start = if (alignStart) 0.dp else DEFAULT_PADDING_HALF + 3.dp, end = if (alignStart) DEFAULT_PADDING_HALF + 3.dp else 0.dp) + .size(22.dp) + .alpha(if (parentActivated.value || buttonActivated.value) 1f else 0.4f), + interactionSource = buttonInteractionSource + ) { + Icon(painterResource(icon), null, Modifier.size(22.dp), tint = MaterialTheme.colors.secondary) + } + } + + @Composable + fun GoToItemButton(alignStart: Boolean, parentActivated: State) { + val chatTypeApiIdMsgId = cItem.meta.itemForwarded?.chatTypeApiIdMsgId + if (searchIsNotBlank.value) { + GoToItemInnerButton(alignStart, MR.images.ic_search, parentActivated) { + withBGApi { + openChat(rhId, cInfo.chatType, cInfo.apiId, null, cItem.id) + closeReportsIfNeeded() + } + } + } else if (chatTypeApiIdMsgId != null) { + GoToItemInnerButton(alignStart, MR.images.ic_arrow_forward, parentActivated) { + val (chatType, apiId, msgId) = chatTypeApiIdMsgId + withBGApi { + openChat(rhId, chatType, apiId, null, msgId) + closeReportsIfNeeded() + } } } - Column( - Modifier - .clipChatItem(cItem, itemSeparation.largeGap, revealed.value) - .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick, interactionSource = interactionSource, indication = LocalIndication.current) - .onRightClick { showMenu.value = true }, - ) { - @Composable - fun framedItemView() { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToQuotedItemFromItem) - } + } - fun deleteMessageQuestionText(): String { - return if (!sent || fullDeleteAllowed || cInfo is ChatInfo.Local) { - generalGetString(MR.strings.delete_message_cannot_be_undone_warning) - } else { - generalGetString(MR.strings.delete_message_mark_deleted_warning) - } + Column(horizontalAlignment = if (cItem.chatDir.sent) Alignment.End else Alignment.Start) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (cItem.chatDir.sent) { + GoToItemButton(true, lineActivated) } - - @Composable - fun MsgReactionsMenu() { - val rs = MsgReaction.old.mapNotNull { r -> - if (null == cItem.reactions.find { it.userReacted && it.reaction.text == r.text }) { - r + Column(Modifier.weight(1f, fill = false)) { + val enterInteraction = remember { HoverInteraction.Enter() } + KeyChangeEffect(highlighted.value) { + if (highlighted.value) { + bubbleInteractionSource.emit(enterInteraction) } else { - null + bubbleInteractionSource.emit(HoverInteraction.Exit(enterInteraction)) } } - if (rs.isNotEmpty()) { - Row(modifier = Modifier.padding(horizontal = DEFAULT_PADDING).horizontalScroll(rememberScrollState()), verticalAlignment = Alignment.CenterVertically) { - rs.forEach() { r -> - Box( - Modifier.size(36.dp).clickable { - setReaction(cInfo, cItem, true, r) - showMenu.value = false - }, - contentAlignment = Alignment.Center - ) { - ReactionIcon(r.text, 12.sp) - } + Column( + Modifier + .clipChatItem(cItem, itemSeparation.largeGap, revealed.value) + .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick, interactionSource = bubbleInteractionSource, indication = LocalIndication.current) + .onRightClick { showMenu.value = true }, + ) { + @Composable + fun framedItemView() { + FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToQuotedItemFromItem) + } + + fun deleteMessageQuestionText(): String { + return if (!sent || fullDeleteAllowed || cInfo is ChatInfo.Local) { + generalGetString(MR.strings.delete_message_cannot_be_undone_warning) + } else { + generalGetString(MR.strings.delete_message_mark_deleted_warning) } } - } - } - @Composable - fun DeleteItemMenu() { - DefaultDropdownMenu(showMenu) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - if (cItem.canBeDeletedForSelf) { - Divider() - SelectItemAction(showMenu, selectChatItem) - } - } - } - - @Composable - fun MsgContentItemDropdownMenu() { - val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file) - when { - // cItem.id check is a special case for live message chat item which has negative ID while not sent yet - cItem.isReport && cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group -> { - DefaultDropdownMenu(showMenu) { - if (cItem.chatDir !is CIDirection.GroupSnd && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { - ArchiveReportItemAction(cItem.id, cInfo.groupInfo.membership.memberActive, showMenu, archiveReports) + @Composable + fun MsgReactionsMenu() { + val rs = MsgReaction.old.mapNotNull { r -> + if (null == cItem.reactions.find { it.userReacted && it.reaction.text == r.text }) { + r + } else { + null } - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages, buttonText = stringResource(MR.strings.delete_report)) - Divider() - SelectItemAction(showMenu, selectChatItem) } - } - cItem.content.msgContent != null && cItem.id >= 0 && !cItem.isReport -> { - DefaultDropdownMenu(showMenu) { - if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) { - MsgReactionsMenu() - } - if (cItem.meta.itemDeleted == null && !live && !cItem.localNote) { - ItemAction(stringResource(MR.strings.reply_verb), painterResource(MR.images.ic_reply), onClick = { - if (composeState.value.editing) { - composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) - } else { - composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) + if (rs.isNotEmpty()) { + Row(modifier = Modifier.padding(horizontal = DEFAULT_PADDING).horizontalScroll(rememberScrollState()), verticalAlignment = Alignment.CenterVertically) { + rs.forEach() { r -> + Box( + Modifier.size(36.dp).clickable { + setReaction(cInfo, cItem, true, r) + showMenu.value = false + }, + contentAlignment = Alignment.Center + ) { + ReactionIcon(r.text, 12.sp) } - showMenu.value = false - }) - } - val clipboard = LocalClipboardManager.current - val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests } - - val copyAndShareAllowed = when { - cItem.content.text.isNotEmpty() -> true - cItem.file?.forwardingAllowed() == true -> true - else -> false - } - - if (copyAndShareAllowed) { - ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = { - var fileSource = getLoadedFileSource(cItem.file) - val shareIfExists = { - when (val f = fileSource) { - null -> clipboard.shareText(cItem.content.text) - else -> shareFile(cItem.text, f) - } - showMenu.value = false - } - if (chatModel.connectedToRemote() && fileSource == null) { - withLongRunningApi(slow = 600_000) { - cItem.file?.loadRemoteFile(true) - fileSource = getLoadedFileSource(cItem.file) - shareIfExists() - } - } else shareIfExists() - }) - } - if (copyAndShareAllowed) { - ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = { - copyItemToClipboard(cItem, clipboard) - showMenu.value = false - }) - } - if (cItem.file != null && (getLoadedFilePath(cItem.file) != null || (chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded))) { - SaveContentItemAction(cItem, saveFileLauncher, showMenu) - } else if (cItem.file != null && cItem.file.fileStatus is CIFileStatus.RcvInvitation && fileSizeValid(cItem.file)) { - ItemAction(stringResource(MR.strings.download_file), painterResource(MR.images.ic_arrow_downward), onClick = { - withBGApi { - Log.d(TAG, "ChatItemView downloadFileAction") - val user = chatModel.currentUser.value - if (user != null) { - controller.receiveFile(rhId, user, cItem.file.fileId) - } - } - showMenu.value = false - }) - } - if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) { - ItemAction(stringResource(MR.strings.edit_verb), painterResource(MR.images.ic_edit_filled), onClick = { - composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews) - showMenu.value = false - }) - } - if (cItem.meta.itemDeleted == null && - (cItem.file == null || cItem.file.forwardingAllowed()) && - !cItem.isLiveDummy && !live - ) { - ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = { - forwardItem(cInfo, cItem) - showMenu.value = false - }) - } - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - if (revealed.value) { - HideItemAction(revealed, showMenu, reveal) - } - if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) { - CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) - } - if (!(live && cItem.meta.isLive) && !preview) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - } - if (cItem.chatDir !is CIDirection.GroupSnd) { - val groupInfo = cItem.memberToModerate(cInfo)?.first - if (groupInfo != null) { - ModerateItemAction(cItem, questionText = moderateMessageQuestionText(cInfo.featureEnabled(ChatFeature.FullDelete), 1), showMenu, deleteMessage) - } else if (cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group && cInfo.groupInfo.groupFeatureEnabled(GroupFeature.Reports) && cInfo.groupInfo.membership.memberRole == GroupMemberRole.Member && !live) { - ReportItemAction(cItem, composeState, showMenu) } } + } + } + + @Composable + fun DeleteItemMenu() { + DefaultDropdownMenu(showMenu) { + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) } } } - cItem.meta.itemDeleted != null -> { + + @Composable + fun MsgContentItemDropdownMenu() { + val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file) + when { + // cItem.id check is a special case for live message chat item which has negative ID while not sent yet + cItem.isReport && cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group -> { + DefaultDropdownMenu(showMenu) { + if (cItem.chatDir !is CIDirection.GroupSnd && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + ArchiveReportItemAction(cItem.id, cInfo.groupInfo.membership.memberActive, showMenu, archiveReports) + } + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages, buttonText = stringResource(MR.strings.delete_report)) + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + cItem.content.msgContent != null && cItem.id >= 0 && !cItem.isReport -> { + DefaultDropdownMenu(showMenu) { + if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) { + MsgReactionsMenu() + } + if (cItem.meta.itemDeleted == null && !live && !cItem.localNote) { + ItemAction(stringResource(MR.strings.reply_verb), painterResource(MR.images.ic_reply), onClick = { + if (composeState.value.editing) { + composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) + } else { + composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) + } + showMenu.value = false + }) + } + val clipboard = LocalClipboardManager.current + val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests } + val copyAndShareAllowed = when { + cItem.content.text.isNotEmpty() -> true + cItem.file?.forwardingAllowed() == true -> true + else -> false + } + + if (copyAndShareAllowed) { + ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = { + var fileSource = getLoadedFileSource(cItem.file) + val shareIfExists = { + when (val f = fileSource) { + null -> clipboard.shareText(cItem.content.text) + else -> shareFile(cItem.text, f) + } + showMenu.value = false + } + if (chatModel.connectedToRemote() && fileSource == null) { + withLongRunningApi(slow = 600_000) { + cItem.file?.loadRemoteFile(true) + fileSource = getLoadedFileSource(cItem.file) + shareIfExists() + } + } else shareIfExists() + }) + } + if (copyAndShareAllowed) { + ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = { + copyItemToClipboard(cItem, clipboard) + showMenu.value = false + }) + } + if (cItem.file != null && (getLoadedFilePath(cItem.file) != null || (chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded))) { + SaveContentItemAction(cItem, saveFileLauncher, showMenu) + } else if (cItem.file != null && cItem.file.fileStatus is CIFileStatus.RcvInvitation && fileSizeValid(cItem.file)) { + ItemAction(stringResource(MR.strings.download_file), painterResource(MR.images.ic_arrow_downward), onClick = { + withBGApi { + Log.d(TAG, "ChatItemView downloadFileAction") + val user = chatModel.currentUser.value + if (user != null) { + controller.receiveFile(rhId, user, cItem.file.fileId) + } + } + showMenu.value = false + }) + } + if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) { + ItemAction(stringResource(MR.strings.edit_verb), painterResource(MR.images.ic_edit_filled), onClick = { + composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews) + showMenu.value = false + }) + } + if (cItem.meta.itemDeleted == null && + (cItem.file == null || cItem.file.forwardingAllowed()) && + !cItem.isLiveDummy && !live + ) { + ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = { + forwardItem(cInfo, cItem) + showMenu.value = false + }) + } + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) + if (revealed.value) { + HideItemAction(revealed, showMenu, reveal) + } + if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) { + CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) + } + if (!(live && cItem.meta.isLive) && !preview) { + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + } + if (cItem.chatDir !is CIDirection.GroupSnd) { + val groupInfo = cItem.memberToModerate(cInfo)?.first + if (groupInfo != null) { + ModerateItemAction(cItem, questionText = moderateMessageQuestionText(cInfo.featureEnabled(ChatFeature.FullDelete), 1), showMenu, deleteMessage) + } else if (cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group && cInfo.groupInfo.groupFeatureEnabled(GroupFeature.Reports) && cInfo.groupInfo.membership.memberRole == GroupMemberRole.Member && !live) { + ReportItemAction(cItem, composeState, showMenu) + } + } + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + cItem.meta.itemDeleted != null -> { + DefaultDropdownMenu(showMenu) { + if (revealed.value) { + HideItemAction(revealed, showMenu, reveal) + } else if (!cItem.isDeletedContent) { + RevealItemAction(revealed, showMenu, reveal) + } else if (range.value != null) { + ExpandItemAction(revealed, showMenu, reveal) + } + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + cItem.isDeletedContent -> { + DefaultDropdownMenu(showMenu) { + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + cItem.mergeCategory != null && ((range.value?.count() ?: 0) > 1 || revealed.value) -> { + DefaultDropdownMenu(showMenu) { + if (revealed.value) { + ShrinkItemAction(revealed, showMenu, reveal) + } else { + ExpandItemAction(revealed, showMenu, reveal) + } + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + else -> { + DefaultDropdownMenu(showMenu) { + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (selectedChatItems.value == null) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + } + } + + @Composable + fun MarkedDeletedItemDropdownMenu() { DefaultDropdownMenu(showMenu) { - if (revealed.value) { - HideItemAction(revealed, showMenu, reveal) - } else if (!cItem.isDeletedContent) { + if (!cItem.isDeletedContent) { RevealItemAction(revealed, showMenu, reveal) - } else if (range.value != null) { - ExpandItemAction(revealed, showMenu, reveal) } ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) @@ -428,268 +531,222 @@ fun ChatItemView( } } } - cItem.isDeletedContent -> { - DefaultDropdownMenu(showMenu) { - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - if (cItem.canBeDeletedForSelf) { - Divider() - SelectItemAction(showMenu, selectChatItem) - } - } - } - cItem.mergeCategory != null && ((range.value?.count() ?: 0) > 1 || revealed.value) -> { - DefaultDropdownMenu(showMenu) { - if (revealed.value) { - ShrinkItemAction(revealed, showMenu, reveal) - } else { - ExpandItemAction(revealed, showMenu, reveal) - } - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - if (cItem.canBeDeletedForSelf) { - Divider() - SelectItemAction(showMenu, selectChatItem) - } - } - } - else -> { - DefaultDropdownMenu(showMenu) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - if (selectedChatItems.value == null) { - Divider() - SelectItemAction(showMenu, selectChatItem) - } - } - } - } - } - @Composable - fun MarkedDeletedItemDropdownMenu() { - DefaultDropdownMenu(showMenu) { - if (!cItem.isDeletedContent) { - RevealItemAction(revealed, showMenu, reveal) - } - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - if (cItem.canBeDeletedForSelf) { - Divider() - SelectItemAction(showMenu, selectChatItem) - } - } - } - - @Composable - fun ContentItem() { - val mc = cItem.content.msgContent - if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) { - MarkedDeletedItemView(cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) - MarkedDeletedItemDropdownMenu() - } else { - if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { - if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) { - EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) - } else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) { - CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) + @Composable + fun ContentItem() { + val mc = cItem.content.msgContent + if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) { + MarkedDeletedItemView(cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + MarkedDeletedItemDropdownMenu() } else { - framedItemView() + if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { + if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) { + EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + } else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) { + CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) + } else { + framedItemView() + } + } else { + framedItemView() + } + MsgContentItemDropdownMenu() } - } else { - framedItemView() } - MsgContentItemDropdownMenu() - } - } - @Composable fun LegacyDeletedItem() { - DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) - DefaultDropdownMenu(showMenu) { - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - if (cItem.canBeDeletedForSelf) { - Divider() - SelectItemAction(showMenu, selectChatItem) + @Composable fun LegacyDeletedItem() { + DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + DefaultDropdownMenu(showMenu) { + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + + @Composable fun CallItem(status: CICallStatus, duration: Int) { + CICallItemView(cInfo, cItem, status, duration, showTimestamp = showTimestamp, acceptCall, cInfo.timedMessagesTTL) + DeleteItemMenu() + } + + fun mergedGroupEventText(chatItem: ChatItem, reversedChatItems: List): String? { + val (count, ns) = chatModel.getConnectedMemberNames(chatItem, reversedChatItems) + val members = when { + ns.size == 1 -> String.format(generalGetString(MR.strings.rcv_group_event_1_member_connected), ns[0]) + ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1]) + ns.size == 3 -> String.format(generalGetString(MR.strings.rcv_group_event_3_members_connected), ns[0], ns[1], ns[2]) + ns.size > 3 -> String.format(generalGetString(MR.strings.rcv_group_event_n_members_connected), ns[0], ns[1], ns.size - 2) + else -> "" + } + return if (count <= 1) { + null + } else if (ns.isEmpty()) { + generalGetString(MR.strings.rcv_group_events_count).format(count) + } else if (count > ns.size) { + members + " " + generalGetString(MR.strings.rcv_group_and_other_events).format(count - ns.size) + } else { + members + } + } + + fun eventItemViewText(reversedChatItems: List): AnnotatedString { + val memberDisplayName = cItem.memberDisplayName + val t = mergedGroupEventText(cItem, reversedChatItems) + return if (!revealed.value && t != null) { + chatEventText(t, cItem.timestampText) + } else if (memberDisplayName != null) { + buildAnnotatedString { + withStyle(chatEventStyle) { append(memberDisplayName) } + append(" ") + }.plus(chatEventText(cItem)) + } else { + chatEventText(cItem) + } + } + + @Composable fun EventItemView() { + val reversedChatItems = chatModel.chatItemsForContent(LocalContentTag.current).value.asReversed() + CIEventView(eventItemViewText(reversedChatItems)) + } + + @Composable + fun DeletedItem() { + MarkedDeletedItemView(cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + DefaultDropdownMenu(showMenu) { + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) + DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + + @Composable + fun E2EEInfoNoPQText() { + Text( + buildAnnotatedString { + withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_no_pq)) } + }, + Modifier.padding(horizontal = 6.dp, vertical = 6.dp) + ) + } + + @Composable + fun DirectE2EEInfoText(e2EEInfo: E2EEInfo) { + if (e2EEInfo.pqEnabled) { + Text( + buildAnnotatedString { + withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_pq)) } + }, + Modifier.padding(horizontal = 6.dp, vertical = 6.dp) + ) + } else { + E2EEInfoNoPQText() + } + } + + when (val c = cItem.content) { + is CIContent.SndMsgContent -> ContentItem() + is CIContent.RcvMsgContent -> ContentItem() + is CIContent.SndDeleted -> LegacyDeletedItem() + is CIContent.RcvDeleted -> LegacyDeletedItem() + is CIContent.SndCall -> CallItem(c.status, c.duration) + is CIContent.RcvCall -> CallItem(c.status, c.duration) + is CIContent.RcvIntegrityError -> if (developerTools) { + IntegrityErrorItemView(c.msgError, cItem, showTimestamp, cInfo.timedMessagesTTL) + DeleteItemMenu() + } else { + Box(Modifier.size(0.dp)) {} + } + is CIContent.RcvDecryptionError -> { + CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember) + DeleteItemMenu() + } + is CIContent.RcvGroupInvitation -> { + CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL) + DeleteItemMenu() + } + is CIContent.SndGroupInvitation -> { + CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL) + DeleteItemMenu() + } + is CIContent.RcvDirectEventContent -> { + EventItemView() + MsgContentItemDropdownMenu() + } + is CIContent.RcvGroupEventContent -> { + when (c.rcvGroupEvent) { + is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat) + else -> EventItemView() + } + MsgContentItemDropdownMenu() + } + is CIContent.SndGroupEventContent -> { + EventItemView() + MsgContentItemDropdownMenu() + } + is CIContent.RcvConnEventContent -> { + EventItemView() + MsgContentItemDropdownMenu() + } + is CIContent.SndConnEventContent -> { + EventItemView() + MsgContentItemDropdownMenu() + } + is CIContent.RcvChatFeature -> { + CIChatFeatureView(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.SndChatFeature -> { + CIChatFeatureView(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.RcvChatPreference -> { + val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null + CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature) + DeleteItemMenu() + } + is CIContent.SndChatPreference -> { + CIChatFeatureView(cInfo, cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.RcvGroupFeature -> { + CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.SndGroupFeature -> { + CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.RcvChatFeatureRejected -> { + CIChatFeatureView(cInfo, cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.RcvGroupFeatureRejected -> { + CIChatFeatureView(cInfo, cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.SndModerated -> DeletedItem() + is CIContent.RcvModerated -> DeletedItem() + is CIContent.RcvBlocked -> DeletedItem() + is CIContent.SndDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo) + is CIContent.RcvDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo) + is CIContent.SndGroupE2EEInfo -> E2EEInfoNoPQText() + is CIContent.RcvGroupE2EEInfo -> E2EEInfoNoPQText() + is CIContent.InvalidJSON -> { + CIInvalidJSONView(c.json) + DeleteItemMenu() + } } } } - - @Composable fun CallItem(status: CICallStatus, duration: Int) { - CICallItemView(cInfo, cItem, status, duration, showTimestamp = showTimestamp, acceptCall, cInfo.timedMessagesTTL) - DeleteItemMenu() - } - - fun mergedGroupEventText(chatItem: ChatItem, reversedChatItems: List): String? { - val (count, ns) = chatModel.getConnectedMemberNames(chatItem, reversedChatItems) - val members = when { - ns.size == 1 -> String.format(generalGetString(MR.strings.rcv_group_event_1_member_connected), ns[0]) - ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1]) - ns.size == 3 -> String.format(generalGetString(MR.strings.rcv_group_event_3_members_connected), ns[0], ns[1], ns[2]) - ns.size > 3 -> String.format(generalGetString(MR.strings.rcv_group_event_n_members_connected), ns[0], ns[1], ns.size - 2) - else -> "" - } - return if (count <= 1) { - null - } else if (ns.isEmpty()) { - generalGetString(MR.strings.rcv_group_events_count).format(count) - } else if (count > ns.size) { - members + " " + generalGetString(MR.strings.rcv_group_and_other_events).format(count - ns.size) - } else { - members - } - } - - fun eventItemViewText(reversedChatItems: List): AnnotatedString { - val memberDisplayName = cItem.memberDisplayName - val t = mergedGroupEventText(cItem, reversedChatItems) - return if (!revealed.value && t != null) { - chatEventText(t, cItem.timestampText) - } else if (memberDisplayName != null) { - buildAnnotatedString { - withStyle(chatEventStyle) { append(memberDisplayName) } - append(" ") - }.plus(chatEventText(cItem)) - } else { - chatEventText(cItem) - } - } - - @Composable fun EventItemView() { - val reversedChatItems = chatModel.chatItemsForContent(LocalContentTag.current).value.asReversed() - CIEventView(eventItemViewText(reversedChatItems)) - } - - @Composable - fun DeletedItem() { - MarkedDeletedItemView(cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) - DefaultDropdownMenu(showMenu) { - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages) - if (cItem.canBeDeletedForSelf) { - Divider() - SelectItemAction(showMenu, selectChatItem) - } - } - } - - @Composable - fun E2EEInfoNoPQText() { - Text( - buildAnnotatedString { - withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_no_pq)) } - }, - Modifier.padding(horizontal = 6.dp, vertical = 6.dp) - ) - } - - @Composable - fun DirectE2EEInfoText(e2EEInfo: E2EEInfo) { - if (e2EEInfo.pqEnabled) { - Text( - buildAnnotatedString { - withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_pq)) } - }, - Modifier.padding(horizontal = 6.dp, vertical = 6.dp) - ) - } else { - E2EEInfoNoPQText() - } - } - - when (val c = cItem.content) { - is CIContent.SndMsgContent -> ContentItem() - is CIContent.RcvMsgContent -> ContentItem() - is CIContent.SndDeleted -> LegacyDeletedItem() - is CIContent.RcvDeleted -> LegacyDeletedItem() - is CIContent.SndCall -> CallItem(c.status, c.duration) - is CIContent.RcvCall -> CallItem(c.status, c.duration) - is CIContent.RcvIntegrityError -> if (developerTools) { - IntegrityErrorItemView(c.msgError, cItem, showTimestamp, cInfo.timedMessagesTTL) - DeleteItemMenu() - } else { - Box(Modifier.size(0.dp)) {} - } - is CIContent.RcvDecryptionError -> { - CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember) - DeleteItemMenu() - } - is CIContent.RcvGroupInvitation -> { - CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL) - DeleteItemMenu() - } - is CIContent.SndGroupInvitation -> { - CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL) - DeleteItemMenu() - } - is CIContent.RcvDirectEventContent -> { - EventItemView() - MsgContentItemDropdownMenu() - } - is CIContent.RcvGroupEventContent -> { - when (c.rcvGroupEvent) { - is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat) - else -> EventItemView() - } - MsgContentItemDropdownMenu() - } - is CIContent.SndGroupEventContent -> { - EventItemView() - MsgContentItemDropdownMenu() - } - is CIContent.RcvConnEventContent -> { - EventItemView() - MsgContentItemDropdownMenu() - } - is CIContent.SndConnEventContent -> { - EventItemView() - MsgContentItemDropdownMenu() - } - is CIContent.RcvChatFeature -> { - CIChatFeatureView(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.SndChatFeature -> { - CIChatFeatureView(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.RcvChatPreference -> { - val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null - CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature) - DeleteItemMenu() - } - is CIContent.SndChatPreference -> { - CIChatFeatureView(cInfo, cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.RcvGroupFeature -> { - CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.SndGroupFeature -> { - CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.RcvChatFeatureRejected -> { - CIChatFeatureView(cInfo, cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.RcvGroupFeatureRejected -> { - CIChatFeatureView(cInfo, cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.SndModerated -> DeletedItem() - is CIContent.RcvModerated -> DeletedItem() - is CIContent.RcvBlocked -> DeletedItem() - is CIContent.SndDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo) - is CIContent.RcvDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo) - is CIContent.SndGroupE2EEInfo -> E2EEInfoNoPQText() - is CIContent.RcvGroupE2EEInfo -> E2EEInfoNoPQText() - is CIContent.InvalidJSON -> { - CIInvalidJSONView(c.json) - DeleteItemMenu() - } + if (!cItem.chatDir.sent) { + GoToItemButton(false, lineActivated) } } - if (cItem.content.msgContent != null && (cItem.meta.itemDeleted == null || revealed.value) && cItem.reactions.isNotEmpty()) { ChatItemReactions() } @@ -1109,7 +1166,7 @@ fun Modifier.clipChatItem(chatItem: ChatItem? = null, tailVisible: Boolean = fal private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boolean, sent: Boolean = false): GenericShape = GenericShape { size, _ -> val (msgTailWidth, msgBubbleMaxRadius) = with(density) { Pair(msgTailWidthDp.toPx(), msgBubbleMaxRadius.toPx()) } - val width = if (sent && tailVisible) size.width - msgTailWidth else size.width + val width = size.width val height = size.height val rxMax = min(msgBubbleMaxRadius, width / 2) val ryMax = min(msgBubbleMaxRadius, height / 2) @@ -1224,6 +1281,12 @@ fun shapeStyle(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVisible: Bo } } +private fun closeReportsIfNeeded() { + if (appPlatform.isAndroid && ModalManager.end.isLastModalOpen(ModalViewId.GROUP_REPORTS)) { + ModalManager.end.closeModals() + } +} + fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) { AlertManager.shared.showAlertDialog( title = generalGetString(cancelAction.alert.titleId), @@ -1338,6 +1401,7 @@ fun PreviewChatItemView( highlighted = remember { mutableStateOf(false) }, range = remember { mutableStateOf(0..1) }, selectedChatItems = remember { mutableStateOf(setOf()) }, + searchIsNotBlank = remember { mutableStateOf(false) }, selectChatItem = {}, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, @@ -1385,6 +1449,7 @@ fun PreviewChatItemViewDeletedContent() { highlighted = remember { mutableStateOf(false) }, range = remember { mutableStateOf(0..1) }, selectedChatItems = remember { mutableStateOf(setOf()) }, + searchIsNotBlank = remember { mutableStateOf(false) }, selectChatItem = {}, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 1fee43f80e..689bf195f5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -202,7 +202,7 @@ fun FramedItemView( Column( Modifier .width(IntrinsicSize.Max) - .padding(start = if (tailRendered) msgTailWidthDp else 0.dp, end = if (sent && tailRendered) msgTailWidthDp else 0.dp) + .padding(start = if (!sent && tailRendered) msgTailWidthDp else 0.dp, end = if (sent && tailRendered) msgTailWidthDp else 0.dp) ) { PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) { @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 4cdd4b5c05..89dc84a1fc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -210,8 +210,26 @@ suspend fun openGroupChat(rhId: Long?, groupId: Long, contentTag: MsgContentTag? suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, contentTag: MsgContentTag? = null) = openChat(rhId, chatInfo.chatType, chatInfo.apiId, contentTag) -private suspend fun openChat(rhId: Long?, chatType: ChatType, apiId: Long, contentTag: MsgContentTag? = null) = - apiLoadMessages(rhId, chatType, apiId, contentTag, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) +suspend fun openChat( + rhId: Long?, + chatType: ChatType, + apiId: Long, + contentTag: MsgContentTag? = null, + openAroundItemId: Long? = null +) = + apiLoadMessages( + rhId, + chatType, + apiId, + contentTag, + if (openAroundItemId != null) { + ChatPagination.Around(openAroundItemId, ChatPagination.INITIAL_COUNT) + } else { + ChatPagination.Initial(ChatPagination.INITIAL_COUNT) + }, + "", + openAroundItemId + ) suspend fun openLoadedChat(chat: Chat, contentTag: MsgContentTag? = null) { withChats(contentTag) { From 7471fd2af5838dc0467aebc570b5ea75e5df3209 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 2 Mar 2025 22:44:26 +0000 Subject: [PATCH 17/24] docs: update privacy policy (#5646) * docs: update privacy policy * remove OSI requirement * update --- PRIVACY.md | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 7c4bfbf660..18e5539726 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -123,6 +123,16 @@ This section applies only to the experimental group directory operated by Simple [SimpleX Directory](/docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://simplex.chat/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). +#### Public groups and content channels + +You may participate in a public group and receive content from a public channel (Group). In case you send messages or comments to the Group, you grant a license: +- to all recipients: + - to share your messages with the new Group members and outside of the group, e.g. via quoting (replying), forwarding and copy-pasting your message. When your message is deleted or marked as deleted, the copies of your message will not be deleted. + - to retain a copy of your messages according to the Group settings (e.g., the Group may allow irreversible message deletion from the recipient devices for a limited period of time, or it may only allow to edit and mark messages as deleted on recipient devices). Deleting message from the recipient devices or marking message as deleted revokes the license to share the message. +- to Group owners: to share your messages with the new Group members as history of the Group. Currently, the Group history shared with the new members is limited to 100 messages. + +Group owners may use chat relays or automated bots (Chat Relays) to re-broadcast member messages to all members, for efficiency. The Chat Relays may be operated by the group owners, by preset operators or by 3rd parties. The Chat Relays have access to and will retain messages in line with Group settings, for technical functioning of the Group. Neither you nor group owners grant any content license to Chat Relay operators. + #### User Support The app includes support contact operated by SimpleX Chat Ltd. If you contact support, any personal data you share is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion) when it is possible, and avoid sharing any personal information. @@ -131,9 +141,9 @@ The app includes support contact operated by SimpleX Chat Ltd. If you contact su Preset server operators will not share the information on their servers with each other, other than aggregate usage statistics. -Preset server operators will not provide general access to their servers or the data on their servers to each other. +Preset server operators must not provide general access to their servers or the data on their servers to each other. -Preset server operators will provide non-administrative access to control port of preset servers to SimpleX Chat Ltd, for the purposes of removing identified illegal content. This control port access only allows deleting known links and files, and access to aggregate statistics, but does NOT allow enumerating any information on the servers. +Preset server operators will provide non-administrative access to control port of preset servers to SimpleX Chat Ltd, for the purposes of removing illegal content identified in publicly accessible resources (contact and group addresses, and downloadable files). This control port access only allows deleting known links and files, and accessing aggregate server-wide statistics, but does NOT allow enumerating any information on the servers or accessing statistics related to specific users. ### Information Preset Server Operators May Share @@ -148,7 +158,7 @@ The cases when the preset server operators may share the data temporarily stored - To detect, prevent, or otherwise address fraud, security, or technical issues. - To protect against harm to the rights, property, or safety of software users, operators of preset servers, or the public as required or permitted by law. -At the time of updating this document, the preset server operators have never provided or have been requested the access to the preset relay servers or any information from the servers by any third parties. If the preset server operators are ever requested to provide such access or information, they will follow the due legal process to limit any information shared with the third parties to the minimally required by law. +By the time of updating this document, the preset server operators were not served with any enforceable requests and did not provide any information from the servers to any third parties. If the preset server operators are ever requested to provide such access or information, they will follow the due legal process to limit any information shared with the third parties to the minimally required by law. Preset server operators will publish information they are legally allowed to share about such requests in the [Transparency reports](./docs/TRANSPARENCY.md). @@ -190,7 +200,18 @@ You accept the Conditions of Use of Software and Infrastructure ("Conditions") b **Legal usage**. You agree to use SimpleX Chat Applications only for legal purposes. You will not use (or assist others in using) the Applications in ways that: 1) violate or infringe the rights of Software users, SimpleX Chat Ltd, other preset server operators, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal communications, e.g. spam. While server operators cannot access content or identify messages or groups, in some cases the links to the illegal communications can be shared publicly on social media or websites. Preset server operators reserve the right to remove such links from the preset servers and disrupt the conversations that send illegal content via their servers, whether they were reported by the users or discovered by the operators themselves. -**Damage to SimpleX Chat Ltd and Preset Server Operators**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit SimpleX Chat Applications in unauthorized manners, or in ways that harm Software users, SimpleX Chat Ltd, other preset server operators, their Infrastructure, or any other systems. For example, you must not 1) access preset operators' Infrastructure or systems without authorization, in any way other than by using the Software; 2) disrupt the integrity or performance of preset operators' Infrastructure; 3) collect information about the users in any manner; or 4) sell, rent, or charge for preset operators' Infrastructure. This does not prohibit you from providing your own Infrastructure to others, whether free or for a fee, as long as you do not violate these Conditions and AGPLv3 license, including the requirement to publish any modifications of the relay server software. +**Damage to SimpleX Chat Ltd and Preset Server Operators**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit SimpleX Chat Applications in unauthorized manners, or in ways that harm Software users, SimpleX Chat Ltd, other preset server operators, their Infrastructure, or any other systems. For example, you must not 1) access preset operators' Infrastructure or systems without authorization, in any way other than by using the Software or by using a 3rd party client applications that satisfies the requirements of the Conditions of use (see the next section); 2) disrupt the integrity or performance of preset operators' Infrastructure; 3) collect information about the users in any manner; or 4) sell, rent, or charge for preset operators' Infrastructure. This does not prohibit you from providing your own Infrastructure to others, whether free or for a fee, as long as you do not violate these Conditions and AGPLv3 license, including the requirement to publish any modifications of the relay server software. + +**3rd party client applications**. You may use a 3rd party application (App) to access preset operators' Infrastructure or systems, provided that this App: +- is compatible with the protocol specifications not older than 1 year, +- provides user-to-user messaging only or enables automated chat bots sending messages requested by users (in case of bots, it must be made clear to the users that these are automated bots), +- implements the same limits, rules and restrictions as Software, +- requires that the users accept the same Conditions of use of preset operators' Infrastructure as in Software prior to providing access to this Infrastructure, +- displays the notice that it is the App for using SimpleX network, +- provides its source code under open-source license accessible to the users via the App interface. In case the App uses the source code of Software, the App's source code must be provided under AGPLv3 license, and in case it is developed without using Software code its source code must be provided under any widely recognized free open-source license, +- does NOT use the branding of SimpleX Chat Ltd without the permission, +- does NOT pretend to be Software, +- complies with these Conditions of use. **Keeping your data secure**. SimpleX Chat is the first communication software that aims to be 100% private by design - server software neither has the ability to access your messages, nor it has information about who you communicate with. That means that you are solely responsible for keeping your device, your user profile and any data safe and secure. If you lose your phone or remove the Software from the device, you will not be able to recover the lost data, unless you made a back up. To protect the data you need to make regular backups, as using old backups may disrupt your communication with some of the contacts. SimpleX Chat Ltd and other preset server operators are not responsible for any data loss. @@ -222,4 +243,4 @@ You accept the Conditions of Use of Software and Infrastructure ("Conditions") b **Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd and preset server operators at any time by deleting the Applications from your devices and discontinuing use of the Infrastructure of SimpleX Chat Ltd and preset server operators. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd and/or preset server operators. -Updated November 14, 2024 +Updated March 3, 2025 From 3d076a89e729440b6f60a71ac7e19ae797f8070a Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 3 Mar 2025 16:09:25 +0000 Subject: [PATCH 18/24] core: update privacy policy for the apps (#5696) --- src/Simplex/Chat/Operators.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 6af0c4a17e..15135d4e9e 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -57,10 +57,10 @@ import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8) usageConditionsCommit :: Text -usageConditionsCommit = "a5061f3147165a05979d6ace33960aced2d6ac03" +usageConditionsCommit = "7471fd2af5838dc0467aebc570b5ea75e5df3209" previousConditionsCommit :: Text -previousConditionsCommit = "11a44dc1fd461a93079f897048b46998db55da5c" +previousConditionsCommit = "a5061f3147165a05979d6ace33960aced2d6ac03" usageConditionsText :: Text usageConditionsText = From 27bf19c2b1b367274df29fa760f859a843453d34 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 3 Mar 2025 20:26:05 +0400 Subject: [PATCH 19/24] ui: updated conditions (#5700) * ios: updated conditions ui * view * kotlin * show Updated conditions via review button * same for android --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/ContentView.swift | 3 +- .../Onboarding/ChooseServerOperators.swift | 3 +- .../Views/Onboarding/WhatsNewView.swift | 3 +- .../NetworkAndServers/NetworkAndServers.swift | 42 +++++++++++++--- .../common/views/chatlist/ChatListView.kt | 8 ++- .../views/onboarding/ChooseServerOperators.kt | 1 + .../common/views/onboarding/WhatsNewView.kt | 31 +++++++++--- .../networkAndServers/NetworkAndServers.kt | 50 +++++++++++++++++-- .../commonMain/resources/MR/base/strings.xml | 1 + 9 files changed, 119 insertions(+), 23 deletions(-) diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 65631954e5..5418f4bcdd 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -303,7 +303,8 @@ struct ContentView: View { case .updatedConditions: UsageConditionsView( currUserServers: Binding.constant([]), - userServers: Binding.constant([]) + userServers: Binding.constant([]), + updated: true ) .modifier(ThemedBackground(grouped: true)) .task { await setConditionsNotified_() } diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 1a0a736acd..24379cf9e3 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -163,7 +163,8 @@ struct ChooseServerOperators: View { case .showConditions: UsageConditionsView( currUserServers: Binding.constant([]), - userServers: Binding.constant([]) + userServers: Binding.constant([]), + updated: false ) .modifier(ThemedBackground(grouped: true)) } diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index f2b4dd7928..4547c6d20a 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -647,7 +647,8 @@ struct WhatsNewView: View { case .showConditions: UsageConditionsView( currUserServers: Binding.constant([]), - userServers: Binding.constant([]) + userServers: Binding.constant([]), + updated: true ) .modifier(ThemedBackground(grouped: true)) } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 16aa98bc5f..7d8424a67d 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -20,11 +20,11 @@ private enum NetworkAlert: Identifiable { } private enum NetworkAndServersSheet: Identifiable { - case showConditions + case showConditions(updated: Bool) var id: String { switch self { - case .showConditions: return "showConditions" + case let .showConditions(updated): return "showConditions \(updated)" } } } @@ -169,10 +169,11 @@ struct NetworkAndServers: View { } .sheet(item: $sheetItem) { item in switch item { - case .showConditions: + case let .showConditions(updated): UsageConditionsView( currUserServers: $ss.servers.currUserServers, - userServers: $ss.servers.userServers + userServers: $ss.servers.userServers, + updated: updated ) .modifier(ThemedBackground(grouped: true)) } @@ -218,7 +219,8 @@ struct NetworkAndServers: View { private func conditionsButton(_ conditionsAction: UsageConditionsAction) -> some View { Button { - sheetItem = .showConditions + let updated = if case .review = conditionsAction { true } else { false } + sheetItem = .showConditions(updated: updated) } label: { switch conditionsAction { case .review: @@ -235,13 +237,18 @@ struct UsageConditionsView: View { @EnvironmentObject var theme: AppTheme @Binding var currUserServers: [UserOperatorServers] @Binding var userServers: [UserOperatorServers] + var updated: Bool var body: some View { VStack(alignment: .leading, spacing: 20) { HStack { - Text("Conditions of use").font(.largeTitle).bold() - Spacer() - conditionsLinkButton() + if updated { + Text("Updated conditions").font(.largeTitle).bold() + } else { + Text("Conditions of use").font(.largeTitle).bold() + Spacer() + conditionsLinkButton() + } } .padding(.top) .padding(.top) @@ -265,6 +272,12 @@ struct UsageConditionsView: View { .multilineTextAlignment(.center) .frame(maxWidth: .infinity, alignment: .center) .padding(.horizontal, 32) + if updated { + conditionsDiffButton(.footnote) + } + } else if updated { + conditionsDiffButton() + .padding(.top) } } .padding(.bottom) @@ -312,6 +325,19 @@ struct UsageConditionsView: View { } } } + + @ViewBuilder private func conditionsDiffButton(_ font: Font? = nil) -> some View { + let commit = ChatModel.shared.conditions.currentConditions.conditionsCommit + if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") { + Link(destination: commitUrl) { + HStack { + Text("Open changes") + Image(systemName: "arrow.up.right.circle") + } + .font(font) + } + } + } } func validateServers_(_ userServers: Binding<[UserOperatorServers]>, _ serverErrors: Binding<[UserServersError]>) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 2375e26dd4..e551ed9635 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -145,7 +145,13 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow UsageConditionsView(userServers = mutableStateOf(emptyList()), currUserServers = mutableStateOf(emptyList()), close = close, rhId = rhId) + Row( + modifier = Modifier + .clip(shape = CircleShape) + .clickable { + modalManager.showModalCloseable { close -> + UsageConditionsView( + userServers = mutableStateOf(emptyList()), + currUserServers = mutableStateOf(emptyList()), + updated = true, + close = close, + rhId = rhId + ) + } } - } - ) + .padding(horizontal = 6.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + stringResource(MR.strings.view_updated_conditions), + color = MaterialTheme.colors.primary + ) + } } if (!viaSettings) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt index 835e01ec27..c211ba4c66 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -22,8 +22,12 @@ import androidx.compose.ui.text.* import androidx.compose.ui.text.input.* import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.* import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* import chat.simplex.common.model.* @@ -180,7 +184,15 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) { @Composable fun ConditionsButton(conditionsAction: UsageConditionsAction, rhId: Long?) { SectionItemView( - click = { ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> UsageConditionsView(currUserServers, userServers, close, rhId) } }, + click = { ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> + UsageConditionsView( + currUserServers, + userServers, + updated = conditionsAction is UsageConditionsAction.Review, + close, + rhId + ) + } }, ) { Text( stringResource(if (conditionsAction is UsageConditionsAction.Review) MR.strings.operator_review_conditions else MR.strings.operator_conditions_accepted), @@ -700,6 +712,7 @@ private fun UnsavedChangesIndicator() { fun UsageConditionsView( currUserServers: MutableState>, userServers: MutableState>, + updated: Boolean, close: () -> Unit, rhId: Long? ) { @@ -733,8 +746,35 @@ fun UsageConditionsView( } } + @Composable + fun ConditionsDiffButton() { + val uriHandler = LocalUriHandler.current + val commit = chatModel.conditions.value.currentConditions.conditionsCommit + Column ( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier + .clip(shape = CircleShape) + .clickable { + val commitUrl = "https://github.com/simplex-chat/simplex-chat/commit/$commit" + uriHandler.openUriCatching(commitUrl) + } + .padding(horizontal = 6.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text(stringResource(MR.strings.operator_open_changes), color = MaterialTheme.colors.primary) + Spacer(Modifier.width(8.dp)) + Icon(painterResource(MR.images.ic_outbound), contentDescription = null, tint = MaterialTheme.colors.primary) + } + } + } + ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false, withPadding = false, bottomPadding = DEFAULT_PADDING) + val title = if (updated) MR.strings.operator_updated_conditions else MR.strings.operator_conditions_of_use + AppBarTitle(stringResource(title), enableAlphaChanges = false, withPadding = false, bottomPadding = DEFAULT_PADDING) when (val conditionsAction = chatModel.conditions.value.conditionsAction) { is UsageConditionsAction.Review -> { if (conditionsAction.operators.isNotEmpty()) { @@ -743,7 +783,7 @@ fun UsageConditionsView( Column(modifier = Modifier.weight(1f).padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) { ConditionsTextView(rhId) } - AcceptConditionsButton(conditionsAction.operators.map { it.operatorId }, close, if (conditionsAction.deadline != null) DEFAULT_PADDING_HALF else DEFAULT_PADDING * 2) + AcceptConditionsButton(conditionsAction.operators.map { it.operatorId }, close, if (conditionsAction.deadline != null || updated) DEFAULT_PADDING_HALF else DEFAULT_PADDING * 2) if (conditionsAction.deadline != null) { SectionTextFooter( text = AnnotatedString(String.format(generalGetString(MR.strings.operator_conditions_accepted_for_enabled_operators_on), localDate(conditionsAction.deadline))), @@ -751,6 +791,10 @@ fun UsageConditionsView( ) Spacer(Modifier.fillMaxWidth().height(DEFAULT_PADDING)) } + if (updated) { + ConditionsDiffButton() + Spacer(Modifier.fillMaxWidth().height(DEFAULT_PADDING)) + } } is UsageConditionsAction.Accepted -> { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 83f085fe3a..d6f182bc02 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1859,6 +1859,7 @@ View conditions Accept conditions Conditions of use + Updated conditions %s, accept conditions of use.]]> Use for messages To receive From b2de37a9fb309284584e6691323270320c31af19 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 3 Mar 2025 18:57:29 +0000 Subject: [PATCH 20/24] core: member acceptance (#5678) * core: member acceptance * migration * move hook * core: support sending direct messages to members (#5680) * fix compilation, todos * fix test * predicates * comment * extend hook * wip * wip * wip * wip * fix test * mute output * schema * better query * plans * fix test * directory * captcha * captcha works * remove column, add UI types and group status icon * fix test * query plans * exclude messages of pending members from history * commands for filter settings * core: separately delete pending approval members; other apis validation (#5699) * accepted status * send captcha messages as replies * fix blocked words * simpler filter info * info about /filter and /role after group registration * update query plans --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- .../Views/ChatList/ChatPreviewView.swift | 2 +- apps/ios/SimpleXChat/ChatTypes.swift | 5 + .../chat/simplex/common/model/ChatModel.kt | 5 + .../commonMain/resources/MR/base/strings.xml | 2 + .../resources/MR/images/ic_help_filled.svg | 1 + apps/simplex-directory-service/Main.hs | 7 +- .../src/Directory/BlockedWords.hs | 55 ++- .../src/Directory/Events.hs | 75 +++- .../src/Directory/Options.hs | 38 +- .../src/Directory/Service.hs | 384 +++++++++++++----- .../src/Directory/Store.hs | 127 ++++-- simplex-chat.cabal | 8 +- src/Simplex/Chat.hs | 3 - src/Simplex/Chat/Bot.hs | 17 +- src/Simplex/Chat/Controller.hs | 46 ++- src/Simplex/Chat/Library/Commands.hs | 207 ++++++---- src/Simplex/Chat/Library/Internal.hs | 161 +++++++- src/Simplex/Chat/Library/Subscriber.hs | 247 ++++------- src/Simplex/Chat/Messages.hs | 2 + src/Simplex/Chat/ProfileGenerator.hs | 13 - src/Simplex/Chat/Protocol.hs | 7 + src/Simplex/Chat/Store.hs | 1 + src/Simplex/Chat/Store/Groups.hs | 19 +- src/Simplex/Chat/Store/Messages.hs | 27 +- src/Simplex/Chat/Store/Profiles.hs | 40 +- .../SQLite/Migrations/chat_query_plans.txt | 4 +- src/Simplex/Chat/Types.hs | 12 + src/Simplex/Chat/Types/Shared.hs | 24 ++ src/Simplex/Chat/View.hs | 18 +- tests/Bots/DirectoryTests.hs | 15 +- tests/ChatTests/Groups.hs | 137 ++++++- tests/SchemaDump.hs | 4 +- 32 files changed, 1188 insertions(+), 525 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help_filled.svg diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 6969ae325c..7f92862f66 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -154,7 +154,7 @@ struct ChatPreviewView: View { } } - @ViewBuilder private func inactiveIcon() -> some View { + private func inactiveIcon() -> some View { Image(systemName: "multiply.circle.fill") .foregroundColor(.secondary.opacity(0.65)) .background(Circle().foregroundColor(Color(uiColor: .systemBackground))) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index a601e60d5f..468bc2ea8f 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2147,6 +2147,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { case .memGroupDeleted: return false case .memUnknown: return false case .memInvited: return false + case .memPendingApproval: return true case .memIntroduced: return false case .memIntroInvited: return false case .memAccepted: return false @@ -2165,6 +2166,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { case .memGroupDeleted: return false case .memUnknown: return false case .memInvited: return false + case .memPendingApproval: return false case .memIntroduced: return true case .memIntroInvited: return true case .memAccepted: return true @@ -2296,6 +2298,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable { case memGroupDeleted = "deleted" case memUnknown = "unknown" case memInvited = "invited" + case memPendingApproval = "pending_approval" case memIntroduced = "introduced" case memIntroInvited = "intro-inv" case memAccepted = "accepted" @@ -2312,6 +2315,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable { case .memGroupDeleted: return "group deleted" case .memUnknown: return "unknown status" case .memInvited: return "invited" + case .memPendingApproval: return "pending approval" case .memIntroduced: return "connecting (introduced)" case .memIntroInvited: return "connecting (introduction invitation)" case .memAccepted: return "connecting (accepted)" @@ -2330,6 +2334,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable { case .memGroupDeleted: return "group deleted" case .memUnknown: return "unknown" case .memInvited: return "invited" + case .memPendingApproval: return "pending" case .memIntroduced: return "connecting" case .memIntroInvited: return "connecting" case .memAccepted: return "connecting" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index f784dcb9ed..7afcd69487 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1917,6 +1917,7 @@ data class GroupMember ( GroupMemberStatus.MemGroupDeleted -> false GroupMemberStatus.MemUnknown -> false GroupMemberStatus.MemInvited -> false + GroupMemberStatus.MemPendingApproval -> true GroupMemberStatus.MemIntroduced -> false GroupMemberStatus.MemIntroInvited -> false GroupMemberStatus.MemAccepted -> false @@ -1933,6 +1934,7 @@ data class GroupMember ( GroupMemberStatus.MemGroupDeleted -> false GroupMemberStatus.MemUnknown -> false GroupMemberStatus.MemInvited -> false + GroupMemberStatus.MemPendingApproval -> false GroupMemberStatus.MemIntroduced -> true GroupMemberStatus.MemIntroInvited -> true GroupMemberStatus.MemAccepted -> true @@ -2037,6 +2039,7 @@ enum class GroupMemberStatus { @SerialName("deleted") MemGroupDeleted, @SerialName("unknown") MemUnknown, @SerialName("invited") MemInvited, + @SerialName("pending_approval") MemPendingApproval, @SerialName("introduced") MemIntroduced, @SerialName("intro-inv") MemIntroInvited, @SerialName("accepted") MemAccepted, @@ -2052,6 +2055,7 @@ enum class GroupMemberStatus { MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted) MemUnknown -> generalGetString(MR.strings.group_member_status_unknown) MemInvited -> generalGetString(MR.strings.group_member_status_invited) + MemPendingApproval -> generalGetString(MR.strings.group_member_status_pending_approval) MemIntroduced -> generalGetString(MR.strings.group_member_status_introduced) MemIntroInvited -> generalGetString(MR.strings.group_member_status_intro_invitation) MemAccepted -> generalGetString(MR.strings.group_member_status_accepted) @@ -2068,6 +2072,7 @@ enum class GroupMemberStatus { MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted) MemUnknown -> generalGetString(MR.strings.group_member_status_unknown_short) MemInvited -> generalGetString(MR.strings.group_member_status_invited) + MemPendingApproval -> generalGetString(MR.strings.group_member_status_pending_approval_short) MemIntroduced -> generalGetString(MR.strings.group_member_status_connecting) MemIntroInvited -> generalGetString(MR.strings.group_member_status_connecting) MemAccepted -> generalGetString(MR.strings.group_member_status_connecting) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index d6f182bc02..52e25a6d8d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1632,6 +1632,8 @@ group deleted unknown status invited + pending approval + pending connecting (introduced) connecting (introduction invitation) connecting (accepted) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help_filled.svg new file mode 100644 index 0000000000..ba3d3a393a --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help_filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/simplex-directory-service/Main.hs b/apps/simplex-directory-service/Main.hs index b01f088483..2091ab444b 100644 --- a/apps/simplex-directory-service/Main.hs +++ b/apps/simplex-directory-service/Main.hs @@ -5,7 +5,9 @@ module Main where import Directory.Options import Directory.Service import Directory.Store +import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) import Simplex.Chat.Core +import Simplex.Chat.Terminal (terminalChatConfig) main :: IO () main = do @@ -14,5 +16,6 @@ main = do if runCLI then directoryServiceCLI st opts else do - cfg <- directoryChatConfig opts - simplexChatCore cfg (mkChatOpts opts) $ directoryService st opts + env <- newServiceState opts + let cfg = terminalChatConfig {chatHooks = defaultChatHooks {acceptMember = Just $ acceptMemberHook opts env}} + simplexChatCore cfg (mkChatOpts opts) $ directoryService st opts env diff --git a/apps/simplex-directory-service/src/Directory/BlockedWords.hs b/apps/simplex-directory-service/src/Directory/BlockedWords.hs index 7477226e53..a29e2c99e0 100644 --- a/apps/simplex-directory-service/src/Directory/BlockedWords.hs +++ b/apps/simplex-directory-service/src/Directory/BlockedWords.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + module Directory.BlockedWords where import Data.Char (isMark, isPunctuation, isSpace) @@ -5,28 +8,38 @@ import Data.List (isPrefixOf) import Data.Maybe (fromMaybe) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M +import Data.Set (Set) +import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Normalize as TN -containsBlockedWords :: Map Char [Char] -> [String] -> Text -> Bool -containsBlockedWords spelling blockedWords s = - let normalizedWords = concatMap words $ normalizeText spelling s - -- Fully normalize the entire string (no spaces or punctuation) - fullNorm = normalizeText spelling $ T.filter (not . isSpace) s - -- Check if any individual word is a swear word - wordCheck = any (`elem` blockedWords) normalizedWords - -- Check if the full string, when normalized, matches a swear word exactly - fullCheck = any (\bw -> T.length s <= length bw * 2 && any (bw ==) fullNorm) blockedWords - -- Check if the string is a single word (no spaces) - isSingleWord = not $ T.any isSpace s - in wordCheck || (fullCheck && not isSingleWord) +data BlockedWordsConfig = BlockedWordsConfig + { blockedWords :: Set Text, + blockedFragments :: Set Text, + extensionRules :: [(String, [String])], + spelling :: Map Char [Char] + } -normalizeText :: Map Char [Char] -> Text -> [String] -normalizeText spelling = - filter (not . null) - . map (filter (\c -> not (isPunctuation c) && not (isMark c))) - . allSubstitutions spelling +hasBlockedFragments :: BlockedWordsConfig -> Text -> Bool +hasBlockedFragments BlockedWordsConfig {spelling, blockedFragments} s = + any (\w -> any (`T.isInfixOf` w) blockedFragments) ws + where + ws = S.fromList $ filter (not . T.null) $ normalizeText spelling s + +hasBlockedWords :: BlockedWordsConfig -> Text -> Bool +hasBlockedWords BlockedWordsConfig {spelling, blockedWords} s = + not $ ws1 `S.disjoint` blockedWords && (length ws <= 1 || ws2 `S.disjoint` blockedWords) + where + ws = T.words s + ws1 = normalizeWords ws + ws2 = normalizeWords $ T.splitOn " " s + normalizeWords = S.fromList . filter (not . T.null) . concatMap (normalizeText spelling) + +normalizeText :: Map Char [Char] -> Text -> [Text] +normalizeText spelling' = + map (T.pack . filter (\c -> not $ isSpace c || isPunctuation c || isMark c)) + . allSubstitutions spelling' . removeTriples . T.unpack . T.toLower @@ -44,12 +57,12 @@ removeTriples xs = go xs '\0' False -- Generate all possible strings by substituting each character allSubstitutions :: Map Char [Char] -> String -> [String] -allSubstitutions spelling = sequence . map substs +allSubstitutions spelling' = sequence . map substs where - substs c = fromMaybe [c] $ M.lookup c spelling + substs c = fromMaybe [c] $ M.lookup c spelling' -wordVariants :: [(String, [String])] -> String -> [String] -wordVariants [] s = [s] +wordVariants :: [(String, [String])] -> String -> [Text] +wordVariants [] s = [T.pack s] wordVariants (sub : subs) s = concatMap (wordVariants subs) (replace sub) where replace (pat, tos) = go s diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index adaccf612f..89099ab9df 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -19,7 +19,7 @@ module Directory.Events ) where -import Control.Applicative ((<|>)) +import Control.Applicative (optional, (<|>)) import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A import Data.Char (isSpace) @@ -46,6 +46,8 @@ data DirectoryEvent | DEGroupInvitation {contact :: Contact, groupInfo :: GroupInfo, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} | DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember} | DEGroupUpdated {contactId :: ContactId, fromGroup :: GroupInfo, toGroup :: GroupInfo} + | DEPendingMember GroupInfo GroupMember + | DEPendingMemberMsg GroupInfo GroupMember ChatItemId Text | DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed | DEServiceRoleChanged GroupInfo GroupMemberRole | DEContactRemovedFromGroup ContactId GroupInfo @@ -65,6 +67,12 @@ crDirectoryEvent = \case CRReceivedGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} -> Just $ DEGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} CRUserJoinedGroup {groupInfo, hostMember} -> (\contactId -> DEServiceJoinedGroup {contactId, groupInfo, hostMember}) <$> memberContactId hostMember CRGroupUpdated {fromGroup, toGroup, member_} -> (\contactId -> DEGroupUpdated {contactId, fromGroup, toGroup}) <$> (memberContactId =<< member_) + CRJoinedGroupMember {groupInfo, member = m} + | pending m -> Just $ DEPendingMember groupInfo m + | otherwise -> Nothing + CRNewChatItems {chatItems = AChatItem _ _ (GroupChat g) ci : _} -> case ci of + ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent (MCText t)} | pending m -> Just $ DEPendingMemberMsg g m (chatItemId' ci) t + _ -> Nothing CRMemberRole {groupInfo, member, toRole} | groupMemberId' member == groupMemberId' (membership groupInfo) -> Just $ DEServiceRoleChanged groupInfo toRole | otherwise -> (\ctId -> DEContactRoleChanged groupInfo ctId toRole) <$> memberContactId member @@ -89,6 +97,8 @@ crDirectoryEvent = \case _ -> Just $ DELogChatResponse $ "chat error: " <> tshow chatError CRChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors) _ -> Nothing + where + pending m = memberStatus m == GSMemPendingApproval data DirectoryRole = DRUser | DRAdmin | DRSuperUser @@ -108,7 +118,8 @@ data DirectoryCmdTag (r :: DirectoryRole) where DCConfirmDuplicateGroup_ :: DirectoryCmdTag 'DRUser DCListUserGroups_ :: DirectoryCmdTag 'DRUser DCDeleteGroup_ :: DirectoryCmdTag 'DRUser - DCSetRole_ :: DirectoryCmdTag 'DRUser + DCMemberRole_ :: DirectoryCmdTag 'DRUser + DCGroupFilter_ :: DirectoryCmdTag 'DRUser DCApproveGroup_ :: DirectoryCmdTag 'DRAdmin DCRejectGroup_ :: DirectoryCmdTag 'DRAdmin DCSuspendGroup_ :: DirectoryCmdTag 'DRAdmin @@ -118,6 +129,8 @@ data DirectoryCmdTag (r :: DirectoryRole) where DCShowGroupLink_ :: DirectoryCmdTag 'DRAdmin DCSendToGroupOwner_ :: DirectoryCmdTag 'DRAdmin DCInviteOwnerToGroup_ :: DirectoryCmdTag 'DRAdmin + -- DCAddBlockedWord_ :: DirectoryCmdTag 'DRAdmin + -- DCRemoveBlockedWord_ :: DirectoryCmdTag 'DRAdmin DCExecuteCommand_ :: DirectoryCmdTag 'DRSuperUser deriving instance Show (DirectoryCmdTag r) @@ -134,7 +147,8 @@ data DirectoryCmd (r :: DirectoryRole) where DCConfirmDuplicateGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser DCListUserGroups :: DirectoryCmd 'DRUser DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser - DCSetRole :: GroupId -> GroupName -> GroupMemberRole -> DirectoryCmd 'DRUser + DCMemberRole :: UserGroupRegId -> Maybe GroupName -> Maybe GroupMemberRole -> DirectoryCmd 'DRUser + DCGroupFilter :: UserGroupRegId -> Maybe GroupName -> Maybe DirectoryMemberAcceptance -> DirectoryCmd 'DRUser DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId} -> DirectoryCmd 'DRAdmin DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin @@ -144,6 +158,8 @@ data DirectoryCmd (r :: DirectoryRole) where DCShowGroupLink :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCSendToGroupOwner :: GroupId -> GroupName -> Text -> DirectoryCmd 'DRAdmin DCInviteOwnerToGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin + -- DCAddBlockedWord :: Text -> DirectoryCmd 'DRAdmin + -- DCRemoveBlockedWord :: Text -> DirectoryCmd 'DRAdmin DCExecuteCommand :: String -> DirectoryCmd 'DRSuperUser DCUnknownCommand :: DirectoryCmd 'DRUser DCCommandError :: DirectoryCmdTag r -> DirectoryCmd r @@ -175,7 +191,8 @@ directoryCmdP = "list" -> u DCListUserGroups_ "ls" -> u DCListUserGroups_ "delete" -> u DCDeleteGroup_ - "role" -> u DCSetRole_ + "role" -> u DCMemberRole_ + "filter" -> u DCGroupFilter_ "approve" -> au DCApproveGroup_ "reject" -> au DCRejectGroup_ "suspend" -> au DCSuspendGroup_ @@ -185,6 +202,8 @@ directoryCmdP = "link" -> au DCShowGroupLink_ "owner" -> au DCSendToGroupOwner_ "invite" -> au DCInviteOwnerToGroup_ + -- "block_word" -> au DCAddBlockedWord_ + -- "unblock_word" -> au DCRemoveBlockedWord_ "exec" -> su DCExecuteCommand_ "x" -> su DCExecuteCommand_ _ -> fail "bad command tag" @@ -202,10 +221,36 @@ directoryCmdP = DCConfirmDuplicateGroup_ -> gc DCConfirmDuplicateGroup DCListUserGroups_ -> pure DCListUserGroups DCDeleteGroup_ -> gc DCDeleteGroup - DCSetRole_ -> do - (groupId, displayName) <- gc (,) - memberRole <- A.space *> ("member" $> GRMember <|> "observer" $> GRObserver) - pure $ DCSetRole groupId displayName memberRole + DCMemberRole_ -> do + (groupId, displayName_) <- gc_ (,) + memberRole_ <- optional $ spacesP *> ("member" $> GRMember <|> "observer" $> GRObserver) + pure $ DCMemberRole groupId displayName_ memberRole_ + DCGroupFilter_ -> do + (groupId, displayName_) <- gc_ (,) + acceptance_ <- + (A.takeWhile (== ' ') >> A.endOfInput) $> Nothing + <|> Just <$> (acceptancePresetsP <|> acceptanceFiltersP) + pure $ DCGroupFilter groupId displayName_ acceptance_ + where + acceptancePresetsP = + spacesP + *> A.choice + [ "no" $> noJoinFilter, + "basic" $> basicJoinFilter, + ("moderate" <|> "mod") $> moderateJoinFilter, + "strong" $> strongJoinFilter + ] + acceptanceFiltersP = do + rejectNames <- filterP "name" + passCaptcha <- filterP "captcha" + makeObserver <- filterP "observer" + pure DirectoryMemberAcceptance {rejectNames, passCaptcha, makeObserver} + filterP :: Text -> Parser (Maybe ProfileCondition) + filterP s = Just <$> (spacesP *> A.string s *> conditionP) <|> pure Nothing + conditionP = + "=all" $> PCAll + <|> ("=noimage" <|> "=no_image" <|> "=no-image") $> PCNoImage + <|> pure PCAll DCApproveGroup_ -> do (groupId, displayName) <- gc (,) groupApprovalId <- A.space *> A.decimal @@ -221,9 +266,14 @@ directoryCmdP = msg <- A.space *> A.takeText pure $ DCSendToGroupOwner groupId displayName msg DCInviteOwnerToGroup_ -> gc DCInviteOwnerToGroup - DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (A.space *> A.takeText) + -- DCAddBlockedWord_ -> DCAddBlockedWord <$> wordP + -- DCRemoveBlockedWord_ -> DCRemoveBlockedWord <$> wordP + DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (spacesP *> A.takeText) where - gc f = f <$> (A.space *> A.decimal <* A.char ':') <*> displayNameTextP + gc f = f <$> (spacesP *> A.decimal) <*> (A.char ':' *> displayNameTextP) + gc_ f = f <$> (spacesP *> A.decimal) <*> optional (A.char ':' *> displayNameTextP) + -- wordP = spacesP *> A.takeTill (== ' ') + spacesP = A.takeWhile1 (== ' ') viewName :: Text -> Text viewName n = if T.any (== ' ') n then "'" <> n <> "'" else n @@ -240,7 +290,8 @@ directoryCmdTag = \case DCListUserGroups -> "list" DCDeleteGroup {} -> "delete" DCApproveGroup {} -> "approve" - DCSetRole {} -> "role" + DCMemberRole {} -> "role" + DCGroupFilter {} -> "filter" DCRejectGroup {} -> "reject" DCSuspendGroup {} -> "suspend" DCResumeGroup {} -> "resume" @@ -249,6 +300,8 @@ directoryCmdTag = \case DCShowGroupLink {} -> "link" DCSendToGroupOwner {} -> "owner" DCInviteOwnerToGroup {} -> "invite" + -- DCAddBlockedWord _ -> "block_word" + -- DCRemoveBlockedWord _ -> "unblock_word" DCExecuteCommand _ -> "exec" DCUnknownCommand -> "unknown" DCCommandError _ -> "error" diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 277d332cf6..5ee52249ac 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -13,10 +13,9 @@ module Directory.Options where import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) import Options.Applicative import Simplex.Chat.Bot.KnownContacts -import Simplex.Chat.Controller (AcceptAsObserver (..), updateStr, versionNumber, versionString) +import Simplex.Chat.Controller (updateStr, versionNumber, versionString) import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreChatOptsP) data DirectoryOpts = DirectoryOpts @@ -25,10 +24,11 @@ data DirectoryOpts = DirectoryOpts superUsers :: [KnownContact], ownersGroup :: Maybe KnownGroup, blockedWordsFile :: Maybe FilePath, + blockedFragmentsFile :: Maybe FilePath, blockedExtensionRules :: Maybe FilePath, nameSpellingFile :: Maybe FilePath, profileNameLimit :: Int, - acceptAsObserver :: Maybe AcceptAsObserver, + captchaGenerator :: Maybe FilePath, directoryLog :: Maybe FilePath, serviceName :: T.Text, runCLI :: Bool, @@ -67,7 +67,14 @@ directoryOpts appDir defaultDbName = do strOption ( long "blocked-words-file" <> metavar "BLOCKED_WORDS_FILE" - <> help "File with the basic forms of words not allowed in profiles and groups" + <> help "File with the basic forms of words not allowed in profiles" + ) + blockedFragmentsFile <- + optional $ + strOption + ( long "blocked-fragments-file" + <> metavar "BLOCKED_WORDS_FILE" + <> help "File with the basic forms of word fragments not allowed in profiles" ) blockedExtensionRules <- optional $ @@ -91,13 +98,12 @@ directoryOpts appDir defaultDbName = do <> help "Max length of profile name that will be allowed to connect and to join groups" <> value maxBound ) - acceptAsObserver <- + captchaGenerator <- optional $ - option - parseAcceptAsObserver - ( long "accept-as-observer" - <> metavar "ACCEPT_AS_OBSERVER" - <> help "Whether to accept all or some of the joining members without posting rights ('all', 'no-image', 'incognito')" + strOption + ( long "captcha-generator" + <> metavar "CAPTCHA_GENERATOR" + <> help "Executable to generate captcha files, must accept text as parameter and save file to stdout as base64 up to 12500 bytes" ) directoryLog <- Just @@ -125,10 +131,11 @@ directoryOpts appDir defaultDbName = do superUsers, ownersGroup, blockedWordsFile, + blockedFragmentsFile, blockedExtensionRules, nameSpellingFile, profileNameLimit, - acceptAsObserver, + captchaGenerator, directoryLog, serviceName = T.pack serviceName, runCLI, @@ -165,12 +172,3 @@ mkChatOpts DirectoryOpts {coreOptions} = markRead = False, maintenance = False } - -parseAcceptAsObserver :: ReadM AcceptAsObserver -parseAcceptAsObserver = eitherReader $ decodeAAO . encodeUtf8 . T.pack - where - decodeAAO = \case - "all" -> Right AOAll - "name-only" -> Right AONameOnly - "incognito" -> Right AOIncognito - _ -> Left "bad AcceptAsObserver" diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 543ba2c84e..7c54f344ad 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -5,31 +5,38 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} module Directory.Service ( welcomeGetOpts, directoryService, directoryServiceCLI, - directoryChatConfig + newServiceState, + acceptMemberHook ) where import Control.Concurrent (forkIO) import Control.Concurrent.Async import Control.Concurrent.STM +import qualified Control.Exception as E import Control.Logger.Simple import Control.Monad -import Data.Composition ((.:)) -import Data.Containers.ListUtils (nubOrd) +import Control.Monad.Except +import Control.Monad.IO.Class +import Data.Int (Int64) import Data.List (find, intercalate) +import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isJust, maybeToList) import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T -import Data.Time.Clock (diffUTCTime, getCurrentTime) +import qualified Data.Text.IO as T +import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) import Directory.BlockedWords import Directory.Events @@ -43,17 +50,24 @@ import Simplex.Chat.Core import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) +import Simplex.Chat.Store.Direct (getContact) +import Simplex.Chat.Store.Groups (getGroupInfo, getGroupLink, getGroupSummary, setGroupCustomData) +import Simplex.Chat.Store.Profiles (GroupLinkInfo (..), getGroupLinkInfo) import Simplex.Chat.Store.Shared (StoreError (..)) import Simplex.Chat.Terminal (terminalChatConfig) import Simplex.Chat.Terminal.Main (simplexChatCLI') import Simplex.Chat.Types import Simplex.Chat.Types.Shared import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) +import Simplex.Messaging.Agent.Store.Common (withTransaction) +import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Encoding.String import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>)) import System.Directory (getAppUserDataDirectory) +import System.Process (readProcess) +import System.Random (randomRIO) data GroupProfileUpdate = GPNoServiceLink | GPServiceLinkAdded | GPServiceLinkRemoved | GPHasServiceLink | GPServiceLinkError @@ -70,13 +84,32 @@ data GroupRolesStatus deriving (Eq) data ServiceState = ServiceState - { searchRequests :: TMap ContactId SearchRequest + { searchRequests :: TMap ContactId SearchRequest, + blockedWordsCfg :: BlockedWordsConfig, + pendingCaptchas :: TMap GroupMemberId PendingCaptcha } -newServiceState :: IO ServiceState -newServiceState = do +data PendingCaptcha = PendingCaptcha + { captchaText :: Text, + sentAt :: UTCTime, + attempts :: Int + } + +captchaLength :: Int +captchaLength = 7 + +maxCaptchaAttempts :: Int +maxCaptchaAttempts = 5 + +captchaTTL :: NominalDiffTime +captchaTTL = 600 -- 10 minutes + +newServiceState :: DirectoryOpts -> IO ServiceState +newServiceState opts = do searchRequests <- TM.emptyIO - pure ServiceState {searchRequests} + blockedWordsCfg <- readBlockedWordsConfig opts + pendingCaptchas <- TM.emptyIO + pure ServiceState {searchRequests, blockedWordsCfg, pendingCaptchas} welcomeGetOpts :: IO DirectoryOpts welcomeGetOpts = do @@ -100,12 +133,12 @@ welcomeGetOpts = do directoryServiceCLI :: DirectoryStore -> DirectoryOpts -> IO () directoryServiceCLI st opts = do - env <- newServiceState + env <- newServiceState opts eventQ <- newTQueueIO let eventHook cc resp = atomically $ resp <$ writeTQueue eventQ (cc, resp) - cfg <- directoryChatConfig opts + chatHooks = defaultChatHooks {eventHook = Just eventHook, acceptMember = Just $ acceptMemberHook opts env} race_ - (simplexChatCLI' cfg {chatHooks = defaultChatHooks {eventHook}} (mkChatOpts opts) Nothing) + (simplexChatCLI' terminalChatConfig {chatHooks} (mkChatOpts opts) Nothing) (processEvents eventQ env) where processEvents eventQ env = forever $ do @@ -113,31 +146,63 @@ directoryServiceCLI st opts = do u_ <- readTVarIO (currentUser cc) forM_ u_ $ \user -> directoryServiceEvent st opts env user cc resp -directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO () -directoryService st opts@DirectoryOpts {testing} user cc = do +directoryService :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> IO () +directoryService st opts@DirectoryOpts {testing} env user cc = do initializeBotAddress' (not testing) cc - env <- newServiceState race_ (forever $ void getLine) . forever $ do (_, _, resp) <- atomically . readTBQueue $ outputQ cc directoryServiceEvent st opts env user cc resp -directoryChatConfig :: DirectoryOpts -> IO ChatConfig -directoryChatConfig DirectoryOpts {blockedWordsFile, nameSpellingFile, blockedExtensionRules, profileNameLimit, acceptAsObserver} = do - blockedWords <- mapM (fmap lines . readFile) blockedWordsFile +acceptMemberHook :: DirectoryOpts -> ServiceState -> GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole)) +acceptMemberHook + DirectoryOpts {profileNameLimit} + ServiceState {blockedWordsCfg} + g + GroupLinkInfo {memberRole} + Profile {displayName, image = img} = runExceptT $ do + let a = groupMemberAcceptance g + when (useMemberFilter img $ rejectNames a) checkName + pure $ + if + | useMemberFilter img (passCaptcha a) -> (GAPending, GRMember) + | useMemberFilter img (makeObserver a) -> (GAAccepted, GRObserver) + | otherwise -> (GAAccepted, memberRole) + where + checkName :: ExceptT GroupRejectionReason IO () + checkName + | T.length displayName > profileNameLimit = throwError GRRLongName + | otherwise = do + when (hasBlockedFragments blockedWordsCfg displayName) $ throwError GRRBlockedName + when (hasBlockedWords blockedWordsCfg displayName) $ throwError GRRBlockedName + +groupMemberAcceptance :: GroupInfo -> DirectoryMemberAcceptance +groupMemberAcceptance GroupInfo {customData} = memberAcceptance $ fromCustomData customData + +useMemberFilter :: Maybe ImageData -> Maybe ProfileCondition -> Bool +useMemberFilter img_ = \case + Just PCAll -> True + Just PCNoImage -> maybe True (\(ImageData i) -> i == "") img_ + Nothing -> False + +readBlockedWordsConfig :: DirectoryOpts -> IO BlockedWordsConfig +readBlockedWordsConfig DirectoryOpts {blockedFragmentsFile, blockedWordsFile, nameSpellingFile, blockedExtensionRules} = do + extensionRules <- maybe (pure []) (fmap read . readFile) blockedExtensionRules spelling <- maybe (pure M.empty) (fmap (M.fromList . read) . readFile) nameSpellingFile - extensionRules <- maybe (pure []) (fmap read . readFile) blockedExtensionRules - let !bws = nubOrd . concatMap (wordVariants extensionRules) <$> blockedWords - !allowedProfileName = not .: containsBlockedWords spelling <$> bws - putStrLn $ "Blocked words: " <> show (maybe 0 length bws) <> ", spelling rules: " <> show (M.size spelling) - pure terminalChatConfig {allowedProfileName, profileNameLimit, acceptAsObserver} + blockedFragments <- S.fromList <$> maybe (pure []) (fmap T.lines . T.readFile) blockedFragmentsFile + bws <- maybe (pure []) (fmap lines . readFile) blockedWordsFile + let blockedWords = S.fromList $ concatMap (wordVariants extensionRules) bws + putStrLn $ "Blocked fragments: " <> show (length blockedFragments) <> ", blocked words: " <> show (length blockedWords) <> ", spelling rules: " <> show (M.size spelling) + pure BlockedWordsConfig {blockedFragments, blockedWords, extensionRules, spelling} directoryServiceEvent :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> ChatResponse -> IO () -directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} ServiceState {searchRequests} user@User {userId} cc event = +directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} env@ServiceState {searchRequests} user@User {userId} cc event = forM_ (crDirectoryEvent event) $ \case DEContactConnected ct -> deContactConnected ct DEGroupInvitation {contact = ct, groupInfo = g, fromMemberRole, memberRole} -> deGroupInvitation ct g fromMemberRole memberRole DEServiceJoinedGroup ctId g owner -> deServiceJoinedGroup ctId g owner DEGroupUpdated {contactId, fromGroup, toGroup} -> deGroupUpdated contactId fromGroup toGroup + DEPendingMember g m -> dePendingMember g m + DEPendingMemberMsg g m ciId t -> dePendingMemberMsg g m ciId t DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role DEServiceRoleChanged g role -> deServiceRoleChanged g role DEContactRemovedFromGroup ctId g -> deContactRemovedFromGroup ctId g @@ -163,7 +228,7 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own notifyOwner GroupReg {dbContactId} = sendMessage' cc dbContactId ctId `isOwner` GroupReg {dbContactId} = ctId == dbContactId withGroupReg GroupInfo {groupId, localDisplayName} err action = do - atomically (getGroupReg st groupId) >>= \case + getGroupReg st groupId >>= \case Just gr -> action gr Nothing -> logError $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId groupInfoText GroupProfile {displayName = n, fullName = fn, description = d} = @@ -373,10 +438,91 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own Just (Just msg) -> notifyOwner gr msg Just Nothing -> sendToApprove toGroup gr gaId + dePendingMember :: GroupInfo -> GroupMember -> IO () + dePendingMember g@GroupInfo {groupProfile = GroupProfile {displayName}} m + | memberRequiresCaptcha a m = sendMemberCaptcha g m Nothing captchaNotice 0 + | otherwise = approvePendingMember a g m + where + a = groupMemberAcceptance g + captchaNotice = "Captcha is generated by SimpleX Directory service.\n\n*Send captcha text* to join the group " <> displayName <> "." + + sendMemberCaptcha :: GroupInfo -> GroupMember -> Maybe ChatItemId -> Text -> Int -> IO () + sendMemberCaptcha GroupInfo {groupId} m quotedId noticeText prevAttempts = do + s <- getCaptchaStr captchaLength "" + mc <- getCaptcha s + sentAt <- getCurrentTime + let captcha = PendingCaptcha {captchaText = T.pack s, sentAt, attempts = prevAttempts + 1} + atomically $ TM.insert gmId captcha $ pendingCaptchas env + sendCaptcha mc + where + getCaptchaStr 0 s = pure s + getCaptchaStr n s = do + i <- randomRIO (0, length chars - 1) + let c = chars !! i + getCaptchaStr (n - 1) (c : s) + chars = "23456789ABCDEFGHIJKLMNOPQRSTUVWXYZabdefghijkmnpqrsty" + getCaptcha s = case captchaGenerator opts of + Nothing -> pure textMsg + Just script -> content <$> readProcess script [s] "" + where + textMsg = MCText $ T.pack s + content r = case T.lines $ T.pack r of + [] -> textMsg + "" : _ -> textMsg + img : _ -> MCImage "" $ ImageData img + sendCaptcha mc = sendComposedMessages_ cc (SRGroup groupId $ Just gmId) [(quotedId, MCText noticeText), (Nothing, mc)] + gmId = groupMemberId' m + + approvePendingMember :: DirectoryMemberAcceptance -> GroupInfo -> GroupMember -> IO () + approvePendingMember a g@GroupInfo {groupId} m@GroupMember {memberProfile = LocalProfile {displayName, image}} = do + gli_ <- join <$> withDB' cc (\db -> getGroupLinkInfo db userId groupId) + let role = if useMemberFilter image (makeObserver a) then GRObserver else maybe GRMember (\GroupLinkInfo {memberRole} -> memberRole) gli_ + gmId = groupMemberId' m + sendChatCmd cc (APIAcceptMember groupId gmId role) >>= \case + CRJoinedGroupMember {} -> do + atomically $ TM.delete gmId $ pendingCaptchas env + logInfo $ "Member " <> viewName displayName <> " accepted, group " <> tshow groupId <> ":" <> viewGroupName g + r -> logError $ "unexpected accept member response: " <> tshow r + + dePendingMemberMsg :: GroupInfo -> GroupMember -> ChatItemId -> Text -> IO () + dePendingMemberMsg g@GroupInfo {groupId, groupProfile = GroupProfile {displayName = n}} m@GroupMember {memberProfile = LocalProfile {displayName}} ciId msgText + | memberRequiresCaptcha a m = do + ts <- getCurrentTime + atomically (TM.lookup (groupMemberId' m) $ pendingCaptchas env) >>= \case + Just PendingCaptcha {captchaText, sentAt, attempts} + | ts `diffUTCTime` sentAt > captchaTTL -> sendMemberCaptcha g m (Just ciId) captchaExpired $ attempts - 1 + | captchaText == msgText -> do + sendComposedMessages_ cc (SRGroup groupId $ Just $ groupMemberId' m) [(Just ciId, MCText $ "Correct, you joined the group " <> n)] + approvePendingMember a g m + | attempts >= maxCaptchaAttempts -> rejectPendingMember tooManyAttempts + | otherwise -> sendMemberCaptcha g m (Just ciId) (wrongCaptcha attempts) attempts + Nothing -> sendMemberCaptcha g m (Just ciId) noCaptcha 0 + | otherwise = approvePendingMember a g m + where + a = groupMemberAcceptance g + rejectPendingMember rjctNotice = do + let gmId = groupMemberId' m + sendComposedMessages cc (SRGroup groupId $ Just gmId) [MCText rjctNotice] + sendChatCmd cc (APIRemoveMembers groupId [gmId]) >>= \case + CRUserDeletedMembers _ _ (_ : _) -> do + atomically $ TM.delete gmId $ pendingCaptchas env + logInfo $ "Member " <> viewName displayName <> " rejected, group " <> tshow groupId <> ":" <> viewGroupName g + r -> logError $ "unexpected remove member response: " <> tshow r + captchaExpired = "Captcha expired, please try again." + wrongCaptcha attempts + | attempts == maxCaptchaAttempts - 1 = "Incorrect text, please try again - this is your last attempt." + | otherwise = "Incorrect text, please try again." + noCaptcha = "Unexpected message, please try again." + tooManyAttempts = "Too many failed attempts, you can't join group." + + memberRequiresCaptcha :: DirectoryMemberAcceptance -> GroupMember -> Bool + memberRequiresCaptcha a GroupMember {memberProfile = LocalProfile {image}} = + useMemberFilter image $ passCaptcha a + sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO () sendToApprove GroupInfo {groupProfile = p@GroupProfile {displayName, image = image'}} GroupReg {dbGroupId, dbContactId} gaId = do - ct_ <- getContact cc dbContactId - gr_ <- getGroupAndSummary cc dbGroupId + ct_ <- getContact' cc user dbContactId + gr_ <- getGroupAndSummary cc user dbGroupId let membersStr = maybe "" (\(_, s) -> "_" <> tshow (currentMembers s) <> " members_\n") gr_ text = maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_ @@ -518,41 +664,86 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own _ -> processInvitation ct g _ -> sendReply $ "Error: the group ID " <> tshow ugrId <> " (" <> displayName <> ") is not pending confirmation." DCListUserGroups -> - atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do + getUserGroupRegs st (contactId' ct) >>= \grs -> do sendReply $ tshow (length grs) <> " registered group(s)" + -- debug how it can be that user has 0 registered groups + when (length grs == 0) $ do + total <- length <$> readTVarIO (groupRegs st) + withSuperUsers $ \ctId -> sendMessage' cc ctId $ + "0 registered groups for " <> localDisplayName' ct <> " (" <> tshow (contactId' ct) <> ") out of " <> tshow total <> " registrations" void . forkIO $ forM_ (reverse grs) $ \gr@GroupReg {userGroupRegId} -> sendGroupInfo ct gr userGroupRegId Nothing DCDeleteGroup ugrId gName -> withUserGroupReg ugrId gName $ \GroupInfo {groupProfile = GroupProfile {displayName}} gr -> do delGroupReg st gr sendReply $ "Your group " <> displayName <> " is deleted from the directory" - DCSetRole gId gName mRole -> - (if isAdmin then withGroupAndReg sendReply else withUserGroupReg) gId gName $ - \GroupInfo {groupId, groupProfile = GroupProfile {displayName}} _gr -> do - gLink_ <- setGroupLinkRole cc groupId mRole - sendReply $ case gLink_ of - Nothing -> "Error: the initial member role for the group " <> displayName <> " was NOT upgated" - Just gLink -> - ("The initial member role for the group " <> displayName <> " is set to *" <> strEncodeTxt mRole <> "*\n\n") - <> ("*Please note*: it applies only to members joining via this link: " <> strEncodeTxt (simplexChatContact gLink)) + DCMemberRole gId gName_ mRole_ -> + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do + let GroupInfo {groupProfile = GroupProfile {displayName = n}} = g + case mRole_ of + Nothing -> + getGroupLinkRole cc user g >>= \case + Just (_, gLink, mRole) -> do + let anotherRole = case mRole of GRObserver -> GRMember; _ -> GRObserver + sendReply $ + initialRole n mRole + <> ("Send */role " <> tshow gId <> " " <> strEncodeTxt anotherRole <> "* to change it.\n\n") + <> onlyViaLink gLink + Nothing -> sendReply $ "Error: failed reading the initial member role for the group " <> n + Just mRole -> do + setGroupLinkRole cc g mRole >>= \case + Just gLink -> sendReply $ initialRole n mRole <> "\n" <> onlyViaLink gLink + Nothing -> sendReply $ "Error: the initial member role for the group " <> n <> " was NOT upgated." + where + initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> strEncodeTxt mRole <> "*\n" + onlyViaLink gLink = "*Please note*: it applies only to members joining via this link: " <> strEncodeTxt (simplexChatContact gLink) + DCGroupFilter gId gName_ acceptance_ -> + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do + let GroupInfo {groupProfile = GroupProfile {displayName = n}} = g + a = groupMemberAcceptance g + case acceptance_ of + Just a' | a /= a' -> do + let d = toCustomData $ DirectoryGroupData a' + withDB' cc (\db -> setGroupCustomData db user g $ Just d) >>= \case + Just () -> sendSettigns n a' " set to" + Nothing -> sendReply $ "Error changing spam filter settings for group " <> n + _ -> sendSettigns n a "" + where + sendSettigns n a setTo = + sendReply $ + T.unlines + [ "Spam filter settings for group " <> n <> setTo <> ":", + "- reject long/inappropriate names: " <> showCondition (rejectNames a), + "- pass captcha to join: " <> showCondition (passCaptcha a), + -- "- make observer: " <> showCondition (makeObserver a) <> (if isJust (makeObserver a) then "" else " (use default set with /role command)"), + "", + -- "Use */filter " <> tshow gId <> " * to change spam filter level: no (disable), basic, moderate, strong.", + -- "Or use */filter " <> tshow gId <> " [name[=noimage]] [captcha[=noimage]] [observer[=noimage]]* for advanced filter configuration." + "Or use */filter " <> tshow gId <> " [name] [captcha]* to configure filter." + ] + showCondition = \case + Nothing -> "_disabled_" + Just PCAll -> "_enabled_" + Just PCNoImage -> "_enabled for profiles without image_" DCUnknownCommand -> sendReply "Unknown command" DCCommandError tag -> sendReply $ "Command error: " <> tshow tag where knownCt = knownContact ct isAdmin = knownCt `elem` adminUsers || knownCt `elem` superUsers - withUserGroupReg ugrId gName action = - atomically (getUserGroupReg st (contactId' ct) ugrId) >>= \case + withUserGroupReg ugrId = withUserGroupReg_ ugrId . Just + withUserGroupReg_ ugrId gName_ action = + getUserGroupReg st (contactId' ct) ugrId >>= \case Nothing -> sendReply $ "Group ID " <> tshow ugrId <> " not found" Just gr@GroupReg {dbGroupId} -> do - getGroup cc dbGroupId >>= \case + getGroup cc user dbGroupId >>= \case Nothing -> sendReply $ "Group ID " <> tshow ugrId <> " not found" Just g@GroupInfo {groupProfile = GroupProfile {displayName}} - | displayName == gName -> action g gr + | maybe True (displayName ==) gName_ -> action g gr | otherwise -> sendReply $ "Group ID " <> tshow ugrId <> " has the display name " <> displayName sendReply = mkSendReply ct ciId withFoundListedGroups s_ action = getGroups_ s_ >>= \case - Just groups -> atomically (filterListedGroups st groups) >>= action + Just groups -> filterListedGroups st groups >>= action Nothing -> sendReply "Error: getGroups. Please notify the developers." sendSearchResults s = \case [] -> sendReply "No groups found" @@ -560,18 +751,18 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own let gs' = takeTop searchResults gs moreGroups = length gs - length gs' more = if moreGroups > 0 then ", sending top " <> tshow (length gs') else "" - sendReply $ "Found " <> tshow (length gs) <> " group(s)" <> more <> "." + reply = "Found " <> tshow (length gs) <> " group(s)" <> more <> "." updateSearchRequest (STSearch s) $ groupIds gs' - sendFoundGroups gs' moreGroups + sendFoundGroups reply gs' moreGroups sendAllGroups takeFirst sortName searchType = \case [] -> sendReply "No groups listed" gs -> do let gs' = takeFirst searchResults gs moreGroups = length gs - length gs' more = if moreGroups > 0 then ", sending " <> sortName <> " " <> tshow (length gs') else "" - sendReply $ tshow (length gs) <> " group(s) listed" <> more <> "." + reply = tshow (length gs) <> " group(s) listed" <> more <> "." updateSearchRequest searchType $ groupIds gs' - sendFoundGroups gs' moreGroups + sendFoundGroups reply gs' moreGroups sendNextSearchResults takeFirst SearchRequest {searchType, sentGroups} = \case [] -> do sendReply "Sorry, no more groups" @@ -580,33 +771,31 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own let gs' = takeFirst searchResults $ filterNotSent sentGroups gs sentGroups' = sentGroups <> groupIds gs' moreGroups = length gs - S.size sentGroups' - sendReply $ "Sending " <> tshow (length gs') <> " more group(s)." + reply = "Sending " <> tshow (length gs') <> " more group(s)." updateSearchRequest searchType sentGroups' - sendFoundGroups gs' moreGroups + sendFoundGroups reply gs' moreGroups updateSearchRequest :: SearchType -> Set GroupId -> IO () updateSearchRequest searchType sentGroups = do searchTime <- getCurrentTime let search = SearchRequest {searchType, searchTime, sentGroups} atomically $ TM.insert (contactId' ct) search searchRequests - sendFoundGroups gs moreGroups = - void . forkIO $ do - forM_ gs $ - \(GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do - let membersStr = "_" <> tshow currentMembers <> " members_" - showId = if isAdmin then tshow groupId <> ". " else "" - text = showId <> groupInfoText p <> "\n" <> membersStr - msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ - sendComposedMessage cc ct Nothing msg - when (moreGroups > 0) $ - sendComposedMessage cc ct Nothing $ - MCText $ - "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)." + sendFoundGroups reply gs moreGroups = + void . forkIO $ sendComposedMessages_ cc (SRDirect $ contactId' ct) msgs + where + msgs = replyMsg :| map foundGroup gs <> [moreMsg | moreGroups > 0] + replyMsg = (Just ciId, MCText reply) + foundGroup (GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) = + let membersStr = "_" <> tshow currentMembers <> " members_" + showId = if isAdmin then tshow groupId <> ". " else "" + text = showId <> groupInfoText p <> "\n" <> membersStr + in (Nothing, maybe (MCText text) (\image -> MCImage {text, image}) image_) + moreMsg = (Nothing, MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s).") deAdminCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRAdmin -> IO () deAdminCommand ct ciId cmd | knownCt `elem` adminUsers || knownCt `elem` superUsers = case cmd of DCApproveGroup {groupId, displayName = n, groupApprovalId} -> - withGroupAndReg sendReply groupId n $ \g gr -> + withGroupAndReg sendReply groupId n $ \g gr@GroupReg {userGroupRegId = ugrId} -> readTVarIO (groupRegStatus gr) >>= \case GRSPendingApproval gaId | gaId == groupApprovalId -> do @@ -618,7 +807,10 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own Just GRSOk -> do setGroupStatus st gr GRSActive let approved = "The group " <> userGroupReference' gr n <> " is approved" - notifyOwner gr $ approved <> " and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved." + notifyOwner gr $ + (approved <> " and listed in directory!\n") + <> "Please note: if you change the group profile it will be hidden from directory until it is re-approved.\n\n" + <> ("Use */filter " <> tshow ugrId <> "* to configure anti-spam filter and */role " <> tshow ugrId <> "* to set default member role.") invited <- forM ownersGroup $ \og@KnownGroup {localDisplayName = ogName} -> do inviteToOwnersGroup og gr $ \case @@ -699,6 +891,8 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own sendReply $ "you" <> invited Left err -> sendReply err Nothing -> sendReply "owners' group is not specified" + -- DCAddBlockedWord _word -> pure () + -- DCRemoveBlockedWord _word -> pure () DCCommandError tag -> sendReply $ "Command error: " <> tshow tag | otherwise = sendReply "You are not allowed to use this command" where @@ -713,7 +907,7 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own else pure groups sendReply $ tshow (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> tshow count else "") void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do - ct_ <- getContact cc dbContactId + ct_ <- getContact' cc user dbContactId let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_ sendGroupInfo ct gr dbGroupId $ Just ownerStr inviteToOwnersGroup :: KnownGroup -> GroupReg -> (Either Text () -> IO a) -> IO a @@ -735,7 +929,7 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own putStrLn $ T.unpack err cont $ Left err groupOwnerInfo groupRef dbContactId = do - owner_ <- getContact cc dbContactId + owner_ <- getContact' cc user dbContactId let ownerInfo = "the owner of the group " <> groupRef ownerName ct' = "@" <> viewName (localDisplayName' ct') <> ", " pure $ maybe "" ownerName owner_ <> ownerInfo @@ -760,12 +954,15 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own mkSendReply ct ciId = sendComposedMessage cc ct (Just ciId) . MCText withGroupAndReg :: (Text -> IO ()) -> GroupId -> GroupName -> (GroupInfo -> GroupReg -> IO ()) -> IO () - withGroupAndReg sendReply gId gName action = - getGroup cc gId >>= \case + withGroupAndReg sendReply gId = withGroupAndReg_ sendReply gId . Just + + withGroupAndReg_ :: (Text -> IO ()) -> GroupId -> Maybe GroupName -> (GroupInfo -> GroupReg -> IO ()) -> IO () + withGroupAndReg_ sendReply gId gName_ action = + getGroup cc user gId >>= \case Nothing -> sendReply $ "Group ID " <> tshow gId <> " not found (getGroup)" Just g@GroupInfo {groupProfile = GroupProfile {displayName}} - | displayName == gName -> - atomically (getGroupReg st gId) >>= \case + | maybe False (displayName ==) gName_ -> + getGroupReg st gId >>= \case Nothing -> sendReply $ "Registration for group ID " <> tshow gId <> " not found (getGroupReg)" Just gr -> action g gr | otherwise -> @@ -775,7 +972,7 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own sendGroupInfo ct gr@GroupReg {dbGroupId} useGroupId ownerStr_ = do grStatus <- readTVarIO $ groupRegStatus gr let statusStr = "Status: " <> groupRegStatusText grStatus - getGroupAndSummary cc dbGroupId >>= \case + getGroupAndSummary cc user dbGroupId >>= \case Just (GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do let membersStr = "_" <> tshow currentMembers <> " members_" text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText p] <> maybeToList ownerStr_ <> [membersStr, statusStr] @@ -785,31 +982,36 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own let text = T.unlines $ [tshow useGroupId <> ". Error: getGroup. Please notify the developers."] <> maybeToList ownerStr_ <> [statusStr] sendComposedMessage cc ct Nothing $ MCText text -getContact :: ChatController -> ContactId -> IO (Maybe Contact) -getContact cc ctId = resp <$> sendChatCmd cc (APIGetChat (ChatRef CTDirect ctId) Nothing (CPLast 0) Nothing) - where - resp :: ChatResponse -> Maybe Contact - resp = \case - CRApiChat _ (AChat SCTDirect Chat {chatInfo = DirectChat ct}) _ -> Just ct - _ -> Nothing +getContact' :: ChatController -> User -> ContactId -> IO (Maybe Contact) +getContact' cc user ctId = withDB cc $ \db -> getContact db (vr cc) user ctId -getGroup :: ChatController -> GroupId -> IO (Maybe GroupInfo) -getGroup cc gId = resp <$> sendChatCmd cc (APIGroupInfo gId) - where - resp :: ChatResponse -> Maybe GroupInfo - resp = \case - CRGroupInfo {groupInfo} -> Just groupInfo - _ -> Nothing +getGroup :: ChatController -> User -> GroupId -> IO (Maybe GroupInfo) +getGroup cc user gId = withDB cc $ \db -> getGroupInfo db (vr cc) user gId -getGroupAndSummary :: ChatController -> GroupId -> IO (Maybe (GroupInfo, GroupSummary)) -getGroupAndSummary cc gId = resp <$> sendChatCmd cc (APIGroupInfo gId) - where - resp = \case - CRGroupInfo {groupInfo, groupSummary} -> Just (groupInfo, groupSummary) - _ -> Nothing +withDB' :: ChatController -> (DB.Connection -> IO a) -> IO (Maybe a) +withDB' cc a = withDB cc $ ExceptT . fmap Right . a -setGroupLinkRole :: ChatController -> GroupId -> GroupMemberRole -> IO (Maybe ConnReqContact) -setGroupLinkRole cc gId mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole gId mRole) +withDB :: ChatController -> (DB.Connection -> ExceptT StoreError IO a) -> IO (Maybe a) +withDB ChatController {chatStore} action = do + r_ :: Either ChatError a <- withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors + case r_ of + Right r -> pure $ Just r + Left e -> Nothing <$ logError ("Database error: " <> tshow e) + +getGroupAndSummary :: ChatController -> User -> GroupId -> IO (Maybe (GroupInfo, GroupSummary)) +getGroupAndSummary cc user gId = + withDB cc $ \db -> (,) <$> getGroupInfo db (vr cc) user gId <*> liftIO (getGroupSummary db user gId) + +vr :: ChatController -> VersionRangeChat +vr ChatController {config = ChatConfig {chatVRange}} = chatVRange +{-# INLINE vr #-} + +getGroupLinkRole :: ChatController -> User -> GroupInfo -> IO (Maybe (Int64, ConnReqContact, GroupMemberRole)) +getGroupLinkRole cc user gInfo = + withDB cc $ \db -> getGroupLink db user gInfo + +setGroupLinkRole :: ChatController -> GroupInfo -> GroupMemberRole -> IO (Maybe ConnReqContact) +setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole) where resp = \case CRGroupLink _ _ gLink _ -> Just gLink diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index cecb253e8d..fed52f494f 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -3,6 +3,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} module Directory.Store ( DirectoryStore (..), @@ -10,6 +11,9 @@ module Directory.Store GroupRegStatus (..), UserGroupRegId, GroupApprovalId, + DirectoryGroupData (..), + DirectoryMemberAcceptance (..), + ProfileCondition (..), restoreDirectoryStore, addGroupReg, delGroupReg, @@ -21,25 +25,35 @@ module Directory.Store filterListedGroups, groupRegStatusText, pendingApproval, + fromCustomData, + toCustomData, + noJoinFilter, + basicJoinFilter, + moderateJoinFilter, + strongJoinFilter ) where import Control.Concurrent.STM import Control.Monad +import Data.Aeson ((.=), (.:)) +import qualified Data.Aeson.KeyMap as JM +import qualified Data.Aeson.TH as JQ +import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Composition ((.:)) import Data.Int (Int64) import Data.List (find, foldl', sortOn) import Data.Map (Map) import qualified Data.Map.Strict as M -import Data.Maybe (isJust) +import Data.Maybe (fromMaybe, isJust) import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) import Simplex.Chat.Types import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) import Simplex.Messaging.Util (ifM) import System.Directory (doesFileExist, renameFile) import System.IO (BufferMode (..), Handle, IOMode (..), hSetBuffering, openFile) @@ -67,6 +81,51 @@ data GroupRegData = GroupRegData groupRegStatus_ :: GroupRegStatus } +data DirectoryGroupData = DirectoryGroupData + { memberAcceptance :: DirectoryMemberAcceptance + } + +-- these filters are applied in the order of fields, depending on ProfileCondition: +-- Nothing - do not apply +-- Just +-- PCAll - apply to all profiles +-- PCNoImage - apply to profiles without images +data DirectoryMemberAcceptance = DirectoryMemberAcceptance + { rejectNames :: Maybe ProfileCondition, -- reject long names and names with profanity + passCaptcha :: Maybe ProfileCondition, -- run captcha challenge with joining members + makeObserver :: Maybe ProfileCondition -- the role assigned in the end, after captcha challenge + } + deriving (Eq, Show) + +data ProfileCondition = PCAll | PCNoImage deriving (Eq, Show) + +noJoinFilter :: DirectoryMemberAcceptance +noJoinFilter = DirectoryMemberAcceptance Nothing Nothing Nothing + +basicJoinFilter :: DirectoryMemberAcceptance +basicJoinFilter = + DirectoryMemberAcceptance + { rejectNames = Just PCNoImage, + passCaptcha = Nothing, + makeObserver = Nothing + } + +moderateJoinFilter :: DirectoryMemberAcceptance +moderateJoinFilter = + DirectoryMemberAcceptance + { rejectNames = Just PCAll, + passCaptcha = Just PCNoImage, + makeObserver = Nothing + } + +strongJoinFilter :: DirectoryMemberAcceptance +strongJoinFilter = + DirectoryMemberAcceptance + { rejectNames = Just PCAll, + passCaptcha = Just PCAll, + makeObserver = Nothing + } + type UserGroupRegId = Int64 type GroupApprovalId = Int64 @@ -106,16 +165,31 @@ grDirectoryStatus = \case GRSSuspendedBadRoles -> DSReserved _ -> DSRegistered +$(JQ.deriveJSON (enumJSON $ dropPrefix "PC") ''ProfileCondition) + +$(JQ.deriveJSON defaultJSON ''DirectoryMemberAcceptance) + +$(JQ.deriveJSON defaultJSON ''DirectoryGroupData) + +fromCustomData :: Maybe CustomData -> DirectoryGroupData +fromCustomData cd_ = + let memberAcceptance = fromMaybe noJoinFilter $ cd_ >>= \(CustomData o) -> JT.parseMaybe (.: "memberAcceptance") o + in DirectoryGroupData {memberAcceptance} + +toCustomData :: DirectoryGroupData -> CustomData +toCustomData DirectoryGroupData {memberAcceptance} = + CustomData $ JM.fromList ["memberAcceptance" .= memberAcceptance] + addGroupReg :: DirectoryStore -> Contact -> GroupInfo -> GroupRegStatus -> IO UserGroupRegId addGroupReg st ct GroupInfo {groupId} grStatus = do - grData <- atomically addGroupReg_ + grData <- addGroupReg_ logGCreate st grData pure $ userGroupRegId_ grData where addGroupReg_ = do let grData = GroupRegData {dbGroupId_ = groupId, userGroupRegId_ = 1, dbContactId_ = ctId, dbOwnerMemberId_ = Nothing, groupRegStatus_ = grStatus} gr <- dataToGroupReg grData - stateTVar (groupRegs st) $ \grs -> + atomically $ stateTVar (groupRegs st) $ \grs -> let ugrId = 1 + foldl' maxUgrId 0 grs grData' = grData {userGroupRegId_ = ugrId} gr' = gr {userGroupRegId = ugrId} @@ -149,18 +223,18 @@ setGroupRegOwner st gr owner = do logGUpdateOwner st (dbGroupId gr) memberId atomically $ writeTVar (dbOwnerMemberId gr) (Just memberId) -getGroupReg :: DirectoryStore -> GroupId -> STM (Maybe GroupReg) -getGroupReg st gId = find ((gId ==) . dbGroupId) <$> readTVar (groupRegs st) +getGroupReg :: DirectoryStore -> GroupId -> IO (Maybe GroupReg) +getGroupReg st gId = find ((gId ==) . dbGroupId) <$> readTVarIO (groupRegs st) -getUserGroupReg :: DirectoryStore -> ContactId -> UserGroupRegId -> STM (Maybe GroupReg) -getUserGroupReg st ctId ugrId = find (\r -> ctId == dbContactId r && ugrId == userGroupRegId r) <$> readTVar (groupRegs st) +getUserGroupReg :: DirectoryStore -> ContactId -> UserGroupRegId -> IO (Maybe GroupReg) +getUserGroupReg st ctId ugrId = find (\r -> ctId == dbContactId r && ugrId == userGroupRegId r) <$> readTVarIO (groupRegs st) -getUserGroupRegs :: DirectoryStore -> ContactId -> STM [GroupReg] -getUserGroupRegs st ctId = filter ((ctId ==) . dbContactId) <$> readTVar (groupRegs st) +getUserGroupRegs :: DirectoryStore -> ContactId -> IO [GroupReg] +getUserGroupRegs st ctId = filter ((ctId ==) . dbContactId) <$> readTVarIO (groupRegs st) -filterListedGroups :: DirectoryStore -> [(GroupInfo, GroupSummary)] -> STM [(GroupInfo, GroupSummary)] +filterListedGroups :: DirectoryStore -> [(GroupInfo, GroupSummary)] -> IO [(GroupInfo, GroupSummary)] filterListedGroups st gs = do - lgs <- readTVar $ listedGroups st + lgs <- readTVarIO $ listedGroups st pure $ filter (\(GroupInfo {groupId}, _) -> groupId `S.member` lgs) gs listGroup :: DirectoryStore -> GroupId -> STM () @@ -200,10 +274,10 @@ logGDelete :: DirectoryStore -> GroupId -> IO () logGDelete st = logDLR st . GRDelete logGUpdateStatus :: DirectoryStore -> GroupId -> GroupRegStatus -> IO () -logGUpdateStatus st = logDLR st .: GRUpdateStatus +logGUpdateStatus st gId = logDLR st . GRUpdateStatus gId logGUpdateOwner :: DirectoryStore -> GroupId -> GroupMemberId -> IO () -logGUpdateOwner st = logDLR st .: GRUpdateOwner +logGUpdateOwner st gId = logDLR st . GRUpdateOwner gId instance StrEncoding DLRTag where strEncode = \case @@ -271,10 +345,10 @@ instance StrEncoding GroupRegStatus where "removed" -> pure GRSRemoved _ -> fail "invalid GroupRegStatus" -dataToGroupReg :: GroupRegData -> STM GroupReg +dataToGroupReg :: GroupRegData -> IO GroupReg dataToGroupReg GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} = do - dbOwnerMemberId <- newTVar dbOwnerMemberId_ - groupRegStatus <- newTVar groupRegStatus_ + dbOwnerMemberId <- newTVarIO dbOwnerMemberId_ + groupRegStatus <- newTVarIO groupRegStatus_ pure GroupReg { dbGroupId = dbGroupId_, @@ -286,10 +360,9 @@ dataToGroupReg GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerM restoreDirectoryStore :: Maybe FilePath -> IO DirectoryStore restoreDirectoryStore = \case - Just f -> ifM (doesFileExist f) (restore f) (newFile f >>= new . Just) - Nothing -> new Nothing + Just f -> ifM (doesFileExist f) (restore f) (newFile f >>= newDirectoryStore . Just) + Nothing -> newDirectoryStore Nothing where - new = atomically . newDirectoryStore newFile f = do h <- openFile f WriteMode hSetBuffering h LineBuffering @@ -298,15 +371,15 @@ restoreDirectoryStore = \case grs <- readDirectoryData f renameFile f (f <> ".bak") h <- writeDirectoryData f grs -- compact - atomically $ mkDirectoryStore h grs + mkDirectoryStore h grs emptyStoreData :: ([GroupReg], Set GroupId, Set GroupId) emptyStoreData = ([], S.empty, S.empty) -newDirectoryStore :: Maybe Handle -> STM DirectoryStore +newDirectoryStore :: Maybe Handle -> IO DirectoryStore newDirectoryStore = (`mkDirectoryStore_` emptyStoreData) -mkDirectoryStore :: Handle -> [GroupRegData] -> STM DirectoryStore +mkDirectoryStore :: Handle -> [GroupRegData] -> IO DirectoryStore mkDirectoryStore h groups = foldM addGroupRegData emptyStoreData groups >>= mkDirectoryStore_ (Just h) where @@ -318,11 +391,11 @@ mkDirectoryStore h groups = DSReserved -> (grs', listed, S.insert gId reserved) DSRegistered -> (grs', listed, reserved) -mkDirectoryStore_ :: Maybe Handle -> ([GroupReg], Set GroupId, Set GroupId) -> STM DirectoryStore +mkDirectoryStore_ :: Maybe Handle -> ([GroupReg], Set GroupId, Set GroupId) -> IO DirectoryStore mkDirectoryStore_ h (grs, listed, reserved) = do - groupRegs <- newTVar grs - listedGroups <- newTVar listed - reservedGroups <- newTVar reserved + groupRegs <- newTVarIO grs + listedGroups <- newTVarIO listed + reservedGroups <- newTVarIO reserved pure DirectoryStore {groupRegs, listedGroups, reservedGroups, directoryLogFile = h} readDirectoryData :: FilePath -> IO [GroupRegData] diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 3f12ebd1af..f987162b78 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -416,13 +416,17 @@ executable simplex-directory-service Paths_simplex_chat ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: - async ==2.2.* + aeson ==2.2.* + , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 , composition ==1.0.* , containers ==0.6.* , directory ==1.3.* + , mtl >=2.3.1 && <3.0 , optparse-applicative >=0.15 && <0.17 + , process >=1.6 && <1.6.18 + , random >=1.1 && <1.3 , simple-logger ==0.1.* , simplex-chat , simplexmq >=6.3 @@ -470,6 +474,7 @@ test-suite simplex-chat-test ViewTests Broadcast.Bot Broadcast.Options + Directory.BlockedWords Directory.Events Directory.Options Directory.Search @@ -512,6 +517,7 @@ test-suite simplex-chat-test , mtl >=2.3.1 && <3.0 , network ==3.1.* , optparse-applicative >=0.15 && <0.17 + , random >=1.1 && <1.3 , silently ==1.2.* , simple-logger ==0.1.* , simplex-chat diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 89bd16b273..bf07e4ae51 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -112,9 +112,6 @@ defaultChatConfig = ntf = _defaultNtfServers, netCfg = defaultNetworkConfig }, - allowedProfileName = Nothing, - profileNameLimit = maxBound, - acceptAsObserver = Nothing, tbqSize = 1024, fileChunkSize = 15780, -- do not change xftpDescrPartSize = 14000, diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index e1f5ce1ef9..54e7baa194 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -4,6 +4,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE TupleSections #-} module Simplex.Chat.Bot where @@ -11,6 +12,8 @@ import Control.Concurrent.Async import Control.Concurrent.STM import Control.Monad import qualified Data.ByteString.Char8 as B +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M import Data.Text (Text) import qualified Data.Text as T @@ -68,10 +71,16 @@ sendComposedMessage :: ChatController -> Contact -> Maybe ChatItemId -> MsgConte sendComposedMessage cc = sendComposedMessage' cc . contactId' sendComposedMessage' :: ChatController -> ContactId -> Maybe ChatItemId -> MsgContent -> IO () -sendComposedMessage' cc ctId quotedItemId msgContent = do - let cm = ComposedMessage {fileSource = Nothing, quotedItemId, msgContent, mentions = M.empty} - sendChatCmd cc (APISendMessages (ChatRef CTDirect ctId) False Nothing [cm]) >>= \case - CRNewChatItems {} -> printLog cc CLLInfo $ "sent message to contact ID " <> show ctId +sendComposedMessage' cc ctId qiId mc = sendComposedMessages_ cc (SRDirect ctId) [(qiId, mc)] + +sendComposedMessages :: ChatController -> SendRef -> NonEmpty MsgContent -> IO () +sendComposedMessages cc sendRef = sendComposedMessages_ cc sendRef . L.map (Nothing,) + +sendComposedMessages_ :: ChatController -> SendRef -> NonEmpty (Maybe ChatItemId, MsgContent) -> IO () +sendComposedMessages_ cc sendRef qmcs = do + let cms = L.map (\(qiId, mc) -> ComposedMessage {fileSource = Nothing, quotedItemId = qiId, msgContent = mc, mentions = M.empty}) qmcs + sendChatCmd cc (APISendMessages sendRef False Nothing cms) >>= \case + CRNewChatItems {} -> printLog cc CLLInfo $ "sent " <> show (length cms) <> " messages to " <> show sendRef r -> putStrLn $ "unexpected send message response: " <> show r deleteMessage :: ChatController -> Contact -> ChatItemId -> IO () diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 71b57c72b2..e639771f41 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -19,7 +19,8 @@ module Simplex.Chat.Controller where import Control.Concurrent (ThreadId) import Control.Concurrent.Async (Async) -import Control.Exception +import Control.Exception (Exception, SomeException) +import qualified Control.Exception as E import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader @@ -60,7 +61,7 @@ import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion import Simplex.Chat.Remote.Types import Simplex.Chat.Stats (PresentedServersSummary) -import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserContactLink, UserMsgReceiptSettings) +import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserContactLink, GroupLinkInfo, UserMsgReceiptSettings) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared @@ -93,7 +94,6 @@ import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitatio import Simplex.RemoteControl.Types import System.IO (Handle) import System.Mem.Weak (Weak) -import qualified UnliftIO.Exception as E import UnliftIO.STM #if !defined(dbPostgres) import Database.SQLite.Simple (SQLError) @@ -137,9 +137,6 @@ data ChatConfig = ChatConfig chatVRange :: VersionRangeChat, confirmMigrations :: MigrationConfirmation, presetServers :: PresetServers, - allowedProfileName :: Maybe (ContactName -> Bool), - profileNameLimit :: Int, - acceptAsObserver :: Maybe AcceptAsObserver, tbqSize :: Natural, fileChunkSize :: Integer, xftpDescrPartSize :: Int, @@ -161,11 +158,6 @@ data ChatConfig = ChatConfig chatHooks :: ChatHooks } -data AcceptAsObserver - = AOAll -- all members - | AONameOnly -- members without image - | AOIncognito -- members with incognito-style names and without image - data RandomAgentServers = RandomAgentServers { smpServers :: NonEmpty (ServerCfg 'PSMP), xftpServers :: NonEmpty (ServerCfg 'PXFTP) @@ -177,18 +169,16 @@ data ChatHooks = ChatHooks { -- preCmdHook can be used to process or modify the commands before they are processed. -- This hook should be used to process CustomChatCommand. -- if this hook returns ChatResponse, the command processing will be skipped. - preCmdHook :: ChatController -> ChatCommand -> IO (Either ChatResponse ChatCommand), + preCmdHook :: Maybe (ChatController -> ChatCommand -> IO (Either ChatResponse ChatCommand)), -- eventHook can be used to additionally process or modify events, -- it is called before the event is sent to the user (or to the UI). - eventHook :: ChatController -> ChatResponse -> IO ChatResponse + eventHook :: Maybe (ChatController -> ChatResponse -> IO ChatResponse), + -- acceptMember hook can be used to accept or reject member connecting via group link without API calls + acceptMember :: Maybe (GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole))) } defaultChatHooks :: ChatHooks -defaultChatHooks = - ChatHooks - { preCmdHook = \_ -> pure . Right, - eventHook = \_ -> pure - } +defaultChatHooks = ChatHooks Nothing Nothing Nothing data PresetServers = PresetServers { operators :: NonEmpty PresetOperator, @@ -313,7 +303,7 @@ data ChatCommand | APIGetChat ChatRef (Maybe MsgContentTag) ChatPagination (Maybe String) | APIGetChatItems ChatPagination (Maybe String) | APIGetChatItemInfo ChatRef ChatItemId - | APISendMessages {chatRef :: ChatRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessages :: NonEmpty ComposedMessage} + | APISendMessages {sendRef :: SendRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessages :: NonEmpty ComposedMessage} | APICreateChatTag ChatTagData | APISetChatTags ChatRef (Maybe (NonEmpty ChatTagId)) | APIDeleteChatTag ChatTagId @@ -366,6 +356,7 @@ data ChatCommand | ApiGetConnNtfMessages {connIds :: NonEmpty AgentConnId} | APIAddMember GroupId ContactId GroupMemberRole | APIJoinGroup {groupId :: GroupId, enableNtfs :: MsgFilter} + | APIAcceptMember GroupId GroupMemberId GroupMemberRole | APIMembersRole GroupId (NonEmpty GroupMemberId) GroupMemberRole | APIBlockMembersForAll GroupId (NonEmpty GroupMemberId) Bool | APIRemoveMembers GroupId (NonEmpty GroupMemberId) @@ -906,6 +897,17 @@ logResponseToFile = \case CRMessageError {} -> True _ -> False +-- (Maybe GroupMemberId) can later be changed to GroupSndScope = GSSAll | GSSAdmins | GSSMember GroupMemberId +data SendRef + = SRDirect ContactId + | SRGroup GroupId (Maybe GroupMemberId) + deriving (Eq, Show) + +sendToChatRef :: SendRef -> ChatRef +sendToChatRef = \case + SRDirect cId -> ChatRef CTDirect cId + SRGroup gId _ -> ChatRef CTGroup gId + data ChatPagination = CPLast Int | CPAfter ChatItemId Int @@ -1509,7 +1511,9 @@ toView = lift . toView' toView' :: ChatResponse -> CM' () toView' ev = do cc@ChatController {outputQ = localQ, remoteCtrlSession = session, config = ChatConfig {chatHooks}} <- ask - event <- liftIO $ eventHook chatHooks cc ev + event <- case eventHook chatHooks of + Just hook -> liftIO $ hook cc ev + Nothing -> pure ev atomically $ readTVar session >>= \case Just (_, RCSessionConnected {remoteOutputQ}) @@ -1544,7 +1548,7 @@ withStoreBatch actions = do liftIO $ withTransaction chatStore $ mapM (`E.catches` handleDBErrors) . actions -- TODO [postgres] postgres specific error handling -handleDBErrors :: [E.Handler IO (Either ChatError a)] +handleDBErrors :: [E.Handler (Either ChatError a)] handleDBErrors = #if !defined(dbPostgres) ( E.Handler $ \(e :: SQLError) -> diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index f44dca9026..821bc3d6ce 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -277,7 +277,9 @@ execChatCommand rh s = do | otherwise -> pure $ CRChatCmdError u $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand _ -> do cc@ChatController {config = ChatConfig {chatHooks}} <- ask - liftIO (preCmdHook chatHooks cc cmd) >>= either pure (execChatCommand_ u) + case preCmdHook chatHooks of + Just hook -> liftIO (hook cc cmd) >>= either pure (execChatCommand_ u) + Nothing -> execChatCommand_ u cmd execChatCommand' :: ChatCommand -> CM' ChatResponse execChatCommand' cmd = asks currentUser >>= readTVarIO >>= (`execChatCommand_` cmd) @@ -536,20 +538,17 @@ processChatCommand' vr = \case Just (CIFFGroup _ _ (Just gId) (Just fwdItemId)) -> Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTGroup gId) fwdItemId) _ -> pure Nothing - APISendMessages (ChatRef cType chatId) live itemTTL cms -> withUser $ \user -> mapM_ assertAllowedContent' cms >> case cType of - CTDirect -> do + APISendMessages sendRef live itemTTL cms -> withUser $ \user -> mapM_ assertAllowedContent' cms >> case sendRef of + SRDirect chatId -> do mapM_ assertNoMentions cms withContactLock "sendMessage" chatId $ sendContactContentMessages user chatId live itemTTL (L.map composedMessageReq cms) - CTGroup -> + SRGroup chatId directMemId_ -> withGroupLock "sendMessage" chatId $ do (gInfo, cmrs) <- withFastStore $ \db -> do g <- getGroupInfo db vr user chatId (g,) <$> mapM (composedMessageReqMentions db user g) cms - sendGroupContentMessages user gInfo live itemTTL cmrs - CTLocal -> pure $ chatCmdError (Just user) "not supported" - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" + sendGroupContentMessages user gInfo directMemId_ live itemTTL cmrs APICreateChatTag (ChatTagData emoji text) -> withUser $ \user -> withFastStore' $ \db -> do _ <- createChatTag db user emoji text CRChatTags user <$> getUserChatTags db user @@ -583,7 +582,8 @@ processChatCommand' vr = \case mc = MCReport reportText reportReason cm = ComposedMessage {fileSource = Nothing, quotedItemId = Just reportedItemId, msgContent = mc, mentions = M.empty} when (null ms') $ throwChatError $ CECommandError "no moderators support receiving reports" - sendGroupContentMessages_ user gInfo ms' False Nothing [composedMessageReq cm] + let numFileInvs = length $ filter memberCurrent ms' + sendGroupContentMessages_ user gInfo Nothing ms' numFileInvs False Nothing [composedMessageReq cm] where compatibleModerator GroupMember {activeConn, memberChatVRange} = maxVersion (maybe memberChatVRange peerChatVRange activeConn) >= contentReportsVersion @@ -633,6 +633,7 @@ processChatCommand' vr = \case then do ciMentions <- withFastStore $ \db -> getCIMentions db user gInfo ft_ mentions let mentions' = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions + -- TODO [knocking] send separately to pending approval member SndMessage {msgId} <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc mentions' (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) ci' <- withFastStore' $ \db -> do currentTs <- liftIO getCurrentTime @@ -687,6 +688,7 @@ processChatCommand' vr = \case assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier let msgIds = itemsMsgIds items events = L.nonEmpty $ map (`XMsgDel` Nothing) msgIds + -- TODO [knocking] validate: only current members or only single pending approval member mapM_ (sendGroupMessages user gInfo ms) events delGroupChatItems user gInfo items False CTLocal -> do @@ -764,6 +766,7 @@ processChatCommand' vr = \case let GroupMember {memberId = itemMemberId} = chatItemMember g ci rs <- withFastStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True checkReactionAllowed rs + -- TODO [knocking] send separately to pending approval member SndMessage {msgId} <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) createdAt <- liftIO getCurrentTime reactions <- withFastStore' $ \db -> do @@ -847,7 +850,7 @@ processChatCommand' vr = \case Just cmrs' -> withGroupLock "forwardChatItem, to group" toChatId $ do gInfo <- withFastStore $ \db -> getGroupInfo db vr user toChatId - sendGroupContentMessages user gInfo False itemTTL cmrs' + sendGroupContentMessages user gInfo Nothing False itemTTL cmrs' Nothing -> pure $ CRNewChatItems user [] CTLocal -> do cmrs <- prepareForward user @@ -1084,6 +1087,7 @@ processChatCommand' vr = \case cancelFilesInProgress user filesInfo deleteFilesLocally filesInfo let doSendDel = memberActive membership && isOwner + -- TODO [knocking] send to pending approval members (move `memberCurrent` filter from sendGroupMessages_ to call sites) when doSendDel . void $ sendGroupMessage' user gInfo members XGrpDel deleteGroupLinkIfExists user gInfo deleteMembersConnections' user members doSendDel @@ -1127,7 +1131,7 @@ processChatCommand' vr = \case (user@User {userId}, cReq) <- withFastStore $ \db -> getContactRequest' db connReqId (ct, conn@Connection {connId}, sqSecured) <- acceptContactRequest user cReq incognito ucl <- withFastStore $ \db -> getUserContactLinkById db userId userContactLinkId - let contactUsed = (\(_, groupId_, _) -> isNothing groupId_) ucl + let contactUsed = (\(_, gLinkInfo_) -> isNothing gLinkInfo_) ucl ct' <- withStore' $ \db -> do deleteContactRequestRec db user cReq updateContactAccepted db user ct contactUsed @@ -1838,8 +1842,8 @@ processChatCommand' vr = \case CTDirect -> withFastStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case Right ctId -> do - let chatRef = ChatRef CTDirect ctId - processChatCommand $ APISendMessages chatRef False Nothing [composedMessage Nothing mc] + let sendRef = SRDirect ctId + processChatCommand $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] Left _ -> withFastStore' (\db -> runExceptT $ getActiveMembersByName db vr user name) >>= \case Right [(gInfo, member)] -> do @@ -1854,8 +1858,8 @@ processChatCommand' vr = \case (gId, mentions) <- withFastStore $ \db -> do gId <- getGroupIdByName db user name (gId,) <$> liftIO (getMessageMentions db user gId msg) - let chatRef = ChatRef CTGroup gId - processChatCommand $ APISendMessages chatRef False Nothing [ComposedMessage Nothing Nothing mc mentions] + let sendRef = SRGroup gId Nothing + processChatCommand $ APISendMessages sendRef False Nothing [ComposedMessage Nothing Nothing mc mentions] CTLocal | name == "" -> do folderId <- withFastStore (`getUserNoteFolderId` user) @@ -1877,12 +1881,13 @@ processChatCommand' vr = \case processChatCommand $ APISendMemberContactInvitation contactId (Just mc) cr -> pure cr Just ctId -> do - let chatRef = ChatRef CTDirect ctId - processChatCommand $ APISendMessages chatRef False Nothing [composedMessage Nothing mc] + let sendRef = SRDirect ctId + processChatCommand $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] SendLiveMessage chatName msg -> withUser $ \user -> do (chatRef, mentions) <- getChatRefAndMentions user chatName msg - let mc = MCText msg - processChatCommand $ APISendMessages chatRef True Nothing [ComposedMessage Nothing Nothing mc mentions] + withSendRef chatRef $ \sendRef -> do + let mc = MCText msg + processChatCommand $ APISendMessages sendRef True Nothing [ComposedMessage Nothing Nothing mc mentions] SendMessageBroadcast msg -> withUser $ \user -> do contacts <- withFastStore' $ \db -> getUserContacts db vr user withChatLock "sendMessageBroadcast" . procCmd $ do @@ -1922,12 +1927,12 @@ processChatCommand' vr = \case combineResults _ _ (Left e) = Left e createCI :: DB.Connection -> User -> UTCTime -> (Contact, SndMessage) -> IO () createCI db user createdAt (ct, sndMsg) = - void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False createdAt + void $ createNewSndChatItem db user (CDDirectSnd ct) Nothing sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False createdAt SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do contactId <- withFastStore $ \db -> getContactIdByName db user cName quotedItemId <- withFastStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg let mc = MCText msg - processChatCommand $ APISendMessages (ChatRef CTDirect contactId) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc M.empty] + processChatCommand $ APISendMessages (SRDirect contactId) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc M.empty] DeleteMessage chatName deletedMsg -> withUser $ \user -> do chatRef <- getChatRef user chatName deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg @@ -2023,14 +2028,27 @@ processChatCommand' vr = \case updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` (toView . CRChatError (Just user)) pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing Nothing -> throwChatError $ CEContactNotActive ct + APIAcceptMember groupId gmId role -> withUser $ \user -> do + (gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user gmId + assertUserGroupRole gInfo GRAdmin + when (memberStatus m /= GSMemPendingApproval) $ throwChatError $ CECommandError "member is not pending approval" + case memberConn m of + Just mConn -> do + let msg = XGrpLinkAcpt role + void $ sendDirectMemberMessage mConn msg groupId + m' <- withFastStore' $ \db -> updateGroupMemberAccepted db user m role + introduceToGroup vr user gInfo m' + pure $ CRJoinedGroupMember user gInfo m' + _ -> throwChatError CEGroupMemberNotActive APIMembersRole groupId memberIds newRole -> withUser $ \user -> withGroupLock "memberRole" groupId . procCmd $ do g@(Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId when (selfSelected gInfo) $ throwChatError $ CECommandError "can't change role for self" - let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin) = selectMembers members + let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending) = selectMembers members when (length invitedMems + length currentMems + length unchangedMems /= length memberIds) $ throwChatError CEGroupMemberNotFound when (length memberIds > 1 && (anyAdmin || newRole >= GRAdmin)) $ throwChatError $ CECommandError "can't change role of multiple members when admins selected, or new role is admin" + when anyPending $ throwChatError $ CECommandError "can't change role of members pending approval" assertUserGroupRole gInfo $ maximum ([GRAdmin, maxRole, newRole] :: [GroupMemberRole]) (errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems (errs2, changed2, acis) <- changeRoleCurrentMems user g currentMems @@ -2040,19 +2058,20 @@ processChatCommand' vr = \case pure $ CRMembersRoleUser {user, groupInfo = gInfo, members = changed1 <> changed2, toRole = newRole} -- same order is not guaranteed where selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds - selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool) - selectMembers = foldr' addMember ([], [], [], GRObserver, False) + selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool) + selectMembers = foldr' addMember ([], [], [], GRObserver, False, False) where - addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin) + addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin, anyPending) | groupMemberId `elem` memberIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin + anyPending' = anyPending || memberStatus == GSMemPendingApproval in if - | memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin') - | memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin') - | otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin') - | otherwise = (invited, current, unchanged, maxRole, anyAdmin) + | memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending') + | memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin', anyPending') + | otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin', anyPending') + | otherwise = (invited, current, unchanged, maxRole, anyAdmin, anyPending) changeRoleInvitedMems :: User -> GroupInfo -> [GroupMember] -> CM ([ChatError], [GroupMember]) changeRoleInvitedMems user gInfo memsToChange = do -- not batched, as we need to send different invitations to different connections anyway @@ -2074,7 +2093,7 @@ processChatCommand' vr = \case let events = L.map (\GroupMember {memberId} -> XGrpMemRole memberId newRole) memsToChange' (msgs_, _gsr) <- sendGroupMessages user gInfo members events let itemsData = zipWith (fmap . sndItemData) memsToChange (L.toList msgs_) - cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData Nothing False + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) Nothing itemsData Nothing False when (length cis_ /= length memsToChange) $ logError "changeRoleCurrentMems: memsToChange and cis_ length mismatch" (errs, changed) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updMember db) memsToChange) let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ @@ -2084,7 +2103,7 @@ processChatCommand' vr = \case sndItemData GroupMember {groupMemberId, memberProfile} msg = let content = CISndGroupEvent $ SGEMemberRole groupMemberId (fromLocalProfile memberProfile) newRole ts = ciContentTexts content - in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing + in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing updMember db m = do updateGroupMemberRole db user m newRole pure (m :: GroupMember) {memberRole = newRole} @@ -2092,22 +2111,24 @@ processChatCommand' vr = \case withGroupLock "blockForAll" groupId . procCmd $ do Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId when (selfSelected gInfo) $ throwChatError $ CECommandError "can't block/unblock self" - let (blockMems, remainingMems, maxRole, anyAdmin) = selectMembers members + let (blockMems, remainingMems, maxRole, anyAdmin, anyPending) = selectMembers members when (length blockMems /= length memberIds) $ throwChatError CEGroupMemberNotFound when (length memberIds > 1 && anyAdmin) $ throwChatError $ CECommandError "can't block/unblock multiple members when admins selected" + when anyPending $ throwChatError $ CECommandError "can't block/unblock members pending approval" assertUserGroupRole gInfo $ max GRModerator maxRole blockMembers user gInfo blockMems remainingMems where selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds - selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], GroupMemberRole, Bool) - selectMembers = foldr' addMember ([], [], GRObserver, False) + selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], GroupMemberRole, Bool, Bool) + selectMembers = foldr' addMember ([], [], GRObserver, False, False) where - addMember m@GroupMember {groupMemberId, memberRole} (block, remaining, maxRole, anyAdmin) + addMember m@GroupMember {groupMemberId, memberRole, memberStatus} (block, remaining, maxRole, anyAdmin, anyPending) | groupMemberId `elem` memberIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin - in (m : block, remaining, maxRole', anyAdmin') - | otherwise = (block, m : remaining, maxRole, anyAdmin) + anyPending' = anyPending || memberStatus == GSMemPendingApproval + in (m : block, remaining, maxRole', anyAdmin', anyPending') + | otherwise = (block, m : remaining, maxRole, anyAdmin, anyPending) blockMembers :: User -> GroupInfo -> [GroupMember] -> [GroupMember] -> CM ChatResponse blockMembers user gInfo blockMems remainingMems = case L.nonEmpty blockMems of Nothing -> throwChatError $ CECommandError "no members to block/unblock" @@ -2116,7 +2137,7 @@ processChatCommand' vr = \case events = L.map (\GroupMember {memberId} -> XGrpMemRestrict memberId MemberRestrictions {restriction = mrs}) blockMems' (msgs_, _gsr) <- sendGroupMessages user gInfo remainingMems events let itemsData = zipWith (fmap . sndItemData) blockMems (L.toList msgs_) - cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData Nothing False + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) Nothing itemsData Nothing False when (length cis_ /= length blockMems) $ logError "blockMembers: blockMems and cis_ length mismatch" let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ unless (null acis) $ toView $ CRNewChatItems user acis @@ -2130,33 +2151,37 @@ processChatCommand' vr = \case sndItemData GroupMember {groupMemberId, memberProfile} msg = let content = CISndGroupEvent $ SGEMemberBlocked groupMemberId (fromLocalProfile memberProfile) blockFlag ts = ciContentTexts content - in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing + in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing APIRemoveMembers groupId memberIds -> withUser $ \user -> withGroupLock "removeMembers" groupId . procCmd $ do - g@(Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId - let (invitedMems, currentMems, maxRole, anyAdmin) = selectMembers members - when (length invitedMems + length currentMems /= length memberIds) $ throwChatError CEGroupMemberNotFound + Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId + let (invitedMems, pendingMems, currentMems, maxRole, anyAdmin) = selectMembers members + when (length invitedMems + length pendingMems + length currentMems /= length memberIds) $ throwChatError CEGroupMemberNotFound when (length memberIds > 1 && anyAdmin) $ throwChatError $ CECommandError "can't remove multiple members when admins selected" assertUserGroupRole gInfo $ max GRAdmin maxRole (errs1, deleted1) <- deleteInvitedMems user invitedMems - (errs2, deleted2, acis) <- deleteCurrentMems user g currentMems + (errs2, deleted2, acis2) <- deleteMemsSend user gInfo members currentMems + rs <- forM pendingMems $ \m -> deleteMemsSend user gInfo [m] [m] + let (errs3, deleted3, acis3) = concatTuples rs + acis = acis2 <> acis3 + errs = errs1 <> errs2 <> errs3 unless (null acis) $ toView $ CRNewChatItems user acis - let errs = errs1 <> errs2 unless (null errs) $ toView $ CRChatErrors (Just user) errs - pure $ CRUserDeletedMembers user gInfo (deleted1 <> deleted2) -- same order is not guaranteed + pure $ CRUserDeletedMembers user gInfo (deleted1 <> deleted2 <> deleted3) -- same order is not guaranteed where - selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], GroupMemberRole, Bool) - selectMembers = foldr' addMember ([], [], GRObserver, False) + selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool) + selectMembers = foldr' addMember ([], [], [], GRObserver, False) where - addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, maxRole, anyAdmin) + addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, pending, current, maxRole, anyAdmin) | groupMemberId `elem` memberIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin in - if memberStatus == GSMemInvited - then (m : invited, current, maxRole', anyAdmin') - else (invited, m : current, maxRole', anyAdmin') - | otherwise = (invited, current, maxRole, anyAdmin) + case memberStatus of + GSMemInvited -> (m : invited, pending, current, maxRole', anyAdmin') + GSMemPendingApproval -> (invited, m : pending, current, maxRole', anyAdmin') + _ -> (invited, pending, m : current, maxRole', anyAdmin') + | otherwise = (invited, pending, current, maxRole, anyAdmin) deleteInvitedMems :: User -> [GroupMember] -> CM ([ChatError], [GroupMember]) deleteInvitedMems user memsToDelete = do deleteMembersConnections user memsToDelete @@ -2165,14 +2190,14 @@ processChatCommand' vr = \case delMember db m = do deleteGroupMember db user m pure m {memberStatus = GSMemRemoved} - deleteCurrentMems :: User -> Group -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem]) - deleteCurrentMems user (Group gInfo members) memsToDelete = case L.nonEmpty memsToDelete of + deleteMemsSend :: User -> GroupInfo -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem]) + deleteMemsSend user gInfo sendToMems memsToDelete = case L.nonEmpty memsToDelete of Nothing -> pure ([], [], []) Just memsToDelete' -> do let events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId) memsToDelete' - (msgs_, _gsr) <- sendGroupMessages user gInfo members events + (msgs_, _gsr) <- sendGroupMessages user gInfo sendToMems events let itemsData = zipWith (fmap . sndItemData) memsToDelete (L.toList msgs_) - cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData Nothing False + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) Nothing itemsData Nothing False when (length cis_ /= length memsToDelete) $ logError "deleteCurrentMems: memsToDelete and cis_ length mismatch" deleteMembersConnections' user memsToDelete True (errs, deleted) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (delMember db) memsToDelete) @@ -2183,15 +2208,19 @@ processChatCommand' vr = \case sndItemData GroupMember {groupMemberId, memberProfile} msg = let content = CISndGroupEvent $ SGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) ts = ciContentTexts content - in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing + in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing delMember db m = do deleteOrUpdateMemberRecordIO db user m pure m {memberStatus = GSMemRemoved} + concatTuples :: [([a], [b], [c])] -> ([a], [b], [c]) + concatTuples xs = (concat as, concat bs, concat cs) + where (as, bs, cs) = unzip3 xs APILeaveGroup groupId -> withUser $ \user@User {userId} -> do Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user groupId filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo withGroupLock "leaveGroup" groupId . procCmd $ do cancelFilesInProgress user filesInfo + -- TODO [knocking] send to pending approval members (move `memberCurrent` filter from sendGroupMessages_ to call sites) msg <- sendGroupMessage' user gInfo members XGrpLeave ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] @@ -2320,7 +2349,7 @@ processChatCommand' vr = \case qiId <- getGroupChatItemIdByText db user gId cName quotedMsg (gId, qiId,) <$> liftIO (getMessageMentions db user gId msg) let mc = MCText msg - processChatCommand $ APISendMessages (ChatRef CTGroup groupId) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions] + processChatCommand $ APISendMessages (SRGroup groupId Nothing) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions] ClearNoteFolder -> withUser $ \user -> do folderId <- withFastStore (`getUserNoteFolderId` user) processChatCommand $ APIClearChat (ChatRef CTLocal folderId) @@ -2361,15 +2390,16 @@ processChatCommand' vr = \case chatRef <- getChatRef user chatName case chatRef of ChatRef CTLocal folderId -> processChatCommand $ APICreateChatItems folderId [composedMessage (Just f) (MCFile "")] - _ -> processChatCommand $ APISendMessages chatRef False Nothing [composedMessage (Just f) (MCFile "")] + _ -> withSendRef chatRef $ \sendRef -> processChatCommand $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCFile "")] SendImage chatName f@(CryptoFile fPath _) -> withUser $ \user -> do chatRef <- getChatRef user chatName - filePath <- lift $ toFSFilePath fPath - unless (any (`isSuffixOf` map toLower fPath) imageExtensions) $ throwChatError CEFileImageType {filePath} - fileSize <- getFileSize filePath - unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath} - -- TODO include file description for preview - processChatCommand $ APISendMessages chatRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)] + withSendRef chatRef $ \sendRef -> do + filePath <- lift $ toFSFilePath fPath + unless (any (`isSuffixOf` map toLower fPath) imageExtensions) $ throwChatError CEFileImageType {filePath} + fileSize <- getFileSize filePath + unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath} + -- TODO include file description for preview + processChatCommand $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)] ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" @@ -2403,6 +2433,7 @@ processChatCommand' vr = \case void . sendDirectContactMessage user contact $ XFileCancel sharedMsgId Just (ChatRef CTGroup groupId) -> do (Group gInfo ms, sharedMsgId) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getSharedMsgIdByFileId db userId fileId + -- TODO [knocking] send separately to pending approval member void . sendGroupMessage user gInfo ms $ XFileCancel sharedMsgId Just _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" ci <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId @@ -2795,6 +2826,7 @@ processChatCommand' vr = \case GroupMember {memberProfile = LocalProfile {displayName, fullName, image}} <- withStore $ \db -> getGroupMemberByMemberId db vr user g businessId let p'' = p' {displayName, fullName, image} :: GroupProfile + -- TODO [knocking] send to pending approval members (move `memberCurrent` filter from sendGroupMessages_ to call sites) void $ sendGroupMessage user g' oldMs (XGrpInfo p'') let ps' = fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' sendGroupMessage user g' newMs $ XGrpPrefs ps' @@ -2823,6 +2855,8 @@ processChatCommand' vr = \case assertUserGroupRole gInfo GRAdmin -- TODO GRModerator when most users migrate let msgMemIds = itemsMsgMemIds gInfo items events = L.nonEmpty $ map (\(msgId, memId) -> XMsgDel msgId (Just memId)) msgMemIds + -- TODO [knocking] validate: only current members or only single pending approval member, + -- TODO or prohibit pending approval members (only moderation and reports use this) mapM_ (sendGroupMessages user gInfo ms) events delGroupChatItems user gInfo items True where @@ -3115,7 +3149,7 @@ processChatCommand' vr = \case msgs_ <- sendDirectContactMessages user ct $ L.map XMsgNew msgContainers let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) msgs_ when (length itemsData /= length cmrs) $ logError "sendContactContentMessages: cmrs and itemsData length mismatch" - r@(_, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) itemsData timed_ live + r@(_, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) Nothing itemsData timed_ live processSendErrs user r forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> forM_ cis $ \ci -> @@ -3151,14 +3185,26 @@ processChatCommand' vr = \case quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True) quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False) quoteData _ = throwError SEInvalidQuote - sendGroupContentMessages :: User -> GroupInfo -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse - sendGroupContentMessages user gInfo live itemTTL cmrs = do + sendGroupContentMessages :: User -> GroupInfo -> Maybe GroupMemberId -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse + sendGroupContentMessages user gInfo@GroupInfo {membership} directMemId_ live itemTTL cmrs = do assertMultiSendable live cmrs - ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo - sendGroupContentMessages_ user gInfo ms live itemTTL cmrs - sendGroupContentMessages_ :: User -> GroupInfo -> [GroupMember] -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse - sendGroupContentMessages_ user gInfo@GroupInfo {groupId, membership} ms live itemTTL cmrs = do - assertUserGroupRole gInfo GRAuthor + (ms, numFileInvs, notInHistory_) <- case directMemId_ of + Nothing -> do + ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + pure (ms, length $ filter memberCurrent ms, Nothing) + Just dmId -> do + when (dmId == groupMemberId' membership) $ throwChatError $ CECommandError "cannot send to self" + dm <- withFastStore $ \db -> getGroupMemberById db vr user dmId + unless (memberStatus dm == GSMemPendingApproval) $ throwChatError $ CECommandError "cannot send directly to member not pending approval" + pure ([dm], 1, Just NotInHistory) + sendGroupContentMessages_ user gInfo notInHistory_ ms numFileInvs live itemTTL cmrs + sendGroupContentMessages_ :: User -> GroupInfo -> Maybe NotInHistory -> [GroupMember] -> Int -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse + sendGroupContentMessages_ user gInfo@GroupInfo {groupId, membership} notInHistory_ ms numFileInvs live itemTTL cmrs = do + -- TODO [knocking] pass GroupSndScope? + let allowedRole = case ms of + [m] | memberCategory m == GCHostMember && memberStatus membership == GSMemPendingApproval -> Nothing + _ -> Just GRAuthor + forM_ allowedRole $ assertUserGroupRole gInfo assertGroupContentAllowed processComposedMessages where @@ -3175,12 +3221,12 @@ processChatCommand' vr = \case Nothing processComposedMessages :: CM ChatResponse processComposedMessages = do - (fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers (length $ filter memberCurrent ms) + (fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers numFileInvs timed_ <- sndGroupCITimed live gInfo itemTTL (chatMsgEvents, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_ (msgs_, gsr) <- sendGroupMessages user gInfo ms chatMsgEvents let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) (L.toList msgs_) - cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData timed_ live + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) notInHistory_ itemsData timed_ live when (length cis_ /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch" createMemberSndStatuses cis_ msgs_ gsr let r@(_, cis) = partitionEithers cis_ @@ -3351,6 +3397,11 @@ processChatCommand' vr = \case getConnQueueInfo user Connection {connId, agentConnId = AgentConnId acId} = do msgInfo <- withFastStore' (`getLastRcvMsgInfo` connId) CRQueueInfo user msgInfo <$> withAgent (`getConnectionQueueInfo` acId) + withSendRef :: ChatRef -> (SendRef -> CM ChatResponse) -> CM ChatResponse + withSendRef chatRef a = case chatRef of + ChatRef CTDirect cId -> a $ SRDirect cId + ChatRef CTGroup gId -> a $ SRGroup gId Nothing + _ -> throwChatError $ CECommandError "not supported" protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) protocolServers p (operators, smpServers, xftpServers) = case p of @@ -3833,7 +3884,7 @@ chatCommandP = "/_get chat " *> (APIGetChat <$> chatRefP <*> optional (" content=" *> strP) <* A.space <*> chatPaginationP <*> optional (" search=" *> stringP)), "/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> stringP)), "/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal), - "/_send " *> (APISendMessages <$> chatRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)), + "/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)), "/_create tag " *> (APICreateChatTag <$> jsonP), "/_tags " *> (APISetChatTags <$> chatRefP <*> optional _strP), "/_delete tag " *> (APIDeleteChatTag <$> A.decimal), @@ -3886,6 +3937,7 @@ chatCommandP = "/_ntf conn messages " *> (ApiGetConnNtfMessages <$> strP), "/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), "/_join #" *> (APIJoinGroup <$> A.decimal <*> pure MFAll), -- needs to be changed to support in UI + "/_accept member #" *> (APIAcceptMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), "/_member role #" *> (APIMembersRole <$> A.decimal <*> _strP <*> memberRole), "/_block #" *> (APIBlockMembersForAll <$> A.decimal <*> _strP <* A.space <* "blocked=" <*> onOffP), "/_remove #" *> (APIRemoveMembers <$> A.decimal <*> _strP), @@ -4206,6 +4258,9 @@ chatCommandP = ct -> ChatName ct <$> displayNameP chatNameP' = ChatName <$> (chatTypeP <|> pure CTDirect) <*> displayNameP chatRefP = ChatRef <$> chatTypeP <*> A.decimal + sendRefP = + (A.char '@' $> SRDirect <*> A.decimal) + <|> (A.char '#' $> SRGroup <*> A.decimal <*> optional (" @" *> A.decimal)) msgCountP = A.space *> A.decimal <|> pure 10 ciTTLDecimal = ("default" $> Nothing) <|> (Just <$> A.decimal) ciTTL = diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index eba1bf169f..1b18abda37 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -38,7 +38,7 @@ import Data.Functor (($>)) import Data.Functor.Identity import Data.Int (Int64) import Data.List (find, mapAccumL, partition) -import Data.List.NonEmpty (NonEmpty (..)) +import Data.List.NonEmpty (NonEmpty (..), (<|)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M @@ -78,7 +78,7 @@ import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent.Client (getFastNetworkConfig, ipAddressProtected, withLockMap) -import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..)) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), ServerCfg (..)) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) @@ -820,17 +820,19 @@ acceptContactRequestAsync user cReq@UserContactRequest {agentInvitationId = Agen setCommandConnId db user cmdId connId pure ct -acceptGroupJoinRequestAsync :: User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember +acceptGroupJoinRequestAsync :: User -> GroupInfo -> UserContactRequest -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember acceptGroupJoinRequestAsync user gInfo@GroupInfo {groupProfile, membership, businessChat} ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange} + gAccepted gLinkMemRole incognitoProfile = do gVar <- asks random + let initialStatus = acceptanceToStatus gAccepted (groupMemberId, memberId) <- withStore $ \db -> do liftIO $ deleteContactRequestRec db user ucr - createJoiningMember db gVar user gInfo ucr gLinkMemRole GSMemAccepted + createJoiningMember db gVar user gInfo ucr gLinkMemRole initialStatus currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo let Profile {displayName} = profileToSendOnAccept user incognitoProfile True GroupMember {memberRole = userRole, memberId = userMemberId} = membership @@ -841,6 +843,7 @@ acceptGroupJoinRequestAsync fromMemberName = displayName, invitedMember = MemberIdRole memberId gLinkMemRole, groupProfile, + accepted = Just gAccepted, business = businessChat, groupSize = Just currentMemCount } @@ -900,6 +903,7 @@ acceptBusinessJoinRequestAsync fromMemberName = displayName, invitedMember = MemberIdRole memberId GRMember, groupProfile = businessGroupProfile userProfile groupPreferences, + accepted = Just GAAccepted, -- This refers to the "title member" that defines the group name and profile. -- This coincides with fromMember to be current user when accepting the connecting user, -- but it will be different when inviting somebody else. @@ -926,6 +930,132 @@ profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> NewIncognito p -> p ExistingIncognito lp -> fromLocalProfile lp +introduceToGroup :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () +introduceToGroup _ _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" +introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {activeConn = Just conn} = do + members <- withStore' $ \db -> getGroupMembers db vr user gInfo + void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m + sendIntroductions members + when (groupFeatureAllowed SGFHistory gInfo) sendHistory + where + sendIntroductions members = do + intros <- withStore' $ \db -> createIntroductions db (maxVersion vr) members m + shuffledIntros <- liftIO $ shuffleIntros intros + if m `supportsVersion` batchSendVersion + then do + let events = map (memberIntro . reMember) shuffledIntros + forM_ (L.nonEmpty events) $ \events' -> + sendGroupMemberMessages user conn events' groupId + else forM_ shuffledIntros $ \intro -> + processIntro intro `catchChatError` (toView . CRChatError (Just user)) + memberIntro :: GroupMember -> ChatMsgEvent 'Json + memberIntro reMember = + let mInfo = memberInfo reMember + mRestrictions = memberRestrictions reMember + in XGrpMemIntro mInfo mRestrictions + shuffleIntros :: [GroupMemberIntro] -> IO [GroupMemberIntro] + shuffleIntros intros = do + let (admins, others) = partition isAdmin intros + (admPics, admNoPics) = partition hasPicture admins + (othPics, othNoPics) = partition hasPicture others + mconcat <$> mapM shuffle [admPics, admNoPics, othPics, othNoPics] + where + isAdmin GroupMemberIntro {reMember = GroupMember {memberRole}} = memberRole >= GRAdmin + hasPicture GroupMemberIntro {reMember = GroupMember {memberProfile = LocalProfile {image}}} = isJust image + processIntro intro@GroupMemberIntro {introId} = do + void $ sendDirectMemberMessage conn (memberIntro $ reMember intro) groupId + withStore' $ \db -> updateIntroStatus db introId GMIntroSent + sendHistory = + when (m `supportsVersion` batchSendVersion) $ do + (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) + (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items + let errors = map ChatErrorStore errs <> errs' + unless (null errors) $ toView $ CRChatErrors (Just user) errors + let events' = maybe (concat events) (\x -> concat events <> [x]) descrEvent_ + forM_ (L.nonEmpty events') $ \events'' -> + sendGroupMemberMessages user conn events'' groupId + descrEvent_ :: Maybe (ChatMsgEvent 'Json) + descrEvent_ + | m `supportsVersion` groupHistoryIncludeWelcomeVersion = do + let GroupInfo {groupProfile = GroupProfile {description}} = gInfo + fmap (\descr -> XMsgNew $ MCSimple $ extMsgContent (MCText descr) Nothing) description + | otherwise = Nothing + itemForwardEvents :: CChatItem 'CTGroup -> CM [ChatMsgEvent 'Json] + itemForwardEvents cci = case cci of + (CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv sender, content = CIRcvMsgContent mc, file}) + | not (blockedByAdmin sender) -> do + fInvDescr_ <- join <$> forM file getRcvFileInvDescr + processContentItem sender ci mc fInvDescr_ + (CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent mc, file}) -> do + fInvDescr_ <- join <$> forM file getSndFileInvDescr + processContentItem membership ci mc fInvDescr_ + _ -> pure [] + where + getRcvFileInvDescr :: CIFile 'MDRcv -> CM (Maybe (FileInvitation, RcvFileDescrText)) + getRcvFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do + expired <- fileExpired + if fileProtocol /= FPXFTP || fileStatus == CIFSRcvCancelled || expired + then pure Nothing + else do + rfd <- withStore $ \db -> getRcvFileDescrByRcvFileId db fileId + pure $ invCompleteDescr ciFile rfd + getSndFileInvDescr :: CIFile 'MDSnd -> CM (Maybe (FileInvitation, RcvFileDescrText)) + getSndFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do + expired <- fileExpired + if fileProtocol /= FPXFTP || fileStatus == CIFSSndCancelled || expired + then pure Nothing + else do + -- can also lookup in extra_xftp_file_descriptions, though it can be empty; + -- would be best if snd file had a single rcv description for all members saved in files table + rfd <- withStore $ \db -> getRcvFileDescrBySndFileId db fileId + pure $ invCompleteDescr ciFile rfd + fileExpired :: CM Bool + fileExpired = do + ttl <- asks $ rcvFilesTTL . agentConfig . config + cutoffTs <- addUTCTime (-ttl) <$> liftIO getCurrentTime + pure $ chatItemTs cci < cutoffTs + invCompleteDescr :: CIFile d -> RcvFileDescr -> Maybe (FileInvitation, RcvFileDescrText) + invCompleteDescr CIFile {fileName, fileSize} RcvFileDescr {fileDescrText, fileDescrComplete} + | fileDescrComplete = + let fInvDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} + fInv = xftpFileInvitation fileName fileSize fInvDescr + in Just (fInv, fileDescrText) + | otherwise = Nothing + processContentItem :: GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText) -> CM [ChatMsgEvent 'Json] + processContentItem sender ChatItem {formattedText, meta, quotedItem, mentions} mc fInvDescr_ = + if isNothing fInvDescr_ && not (msgContentHasText mc) + then pure [] + else do + let CIMeta {itemTs, itemSharedMsgId, itemTimed} = meta + quotedItemId_ = quoteItemId =<< quotedItem + fInv_ = fst <$> fInvDescr_ + (mc', _, mentions') = updatedMentionNames mc formattedText mentions + mentions'' = M.map (\CIMention {memberId} -> MsgMention {memberId}) mentions' + (chatMsgEvent, _) <- withStore $ \db -> prepareGroupMsg db user gInfo mc' mentions'' quotedItemId_ Nothing fInv_ itemTimed False + let senderVRange = memberChatVRange' sender + xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent} + fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of + (Just fileDescrText, Just msgId) -> do + partSize <- asks $ xftpDescrPartSize . config + let parts = splitFileDescr partSize fileDescrText + pure . L.toList $ L.map (XMsgFileDescr msgId) parts + _ -> pure [] + let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents + GroupMember {memberId} = sender + msgForwardEvents = map (\cm -> XGrpMsgForward memberId cm itemTs) (xMsgNewChatMsg : fileDescrChatMsgs) + pure msgForwardEvents + +splitFileDescr :: Int -> RcvFileDescrText -> NonEmpty FileDescr +splitFileDescr partSize rfdText = splitParts 1 rfdText + where + splitParts partNo remText = + let (part, rest) = T.splitAt partSize remText + complete = T.null rest + fileDescr = FileDescr {fileDescrText = part, fileDescrPartNo = partNo, fileDescrComplete = complete} + in if complete + then fileDescr :| [] + else fileDescr <| splitParts (partNo + 1) rest + deleteGroupLink' :: User -> GroupInfo -> CM () deleteGroupLink' user gInfo = do vr <- chatVersionRange @@ -1459,6 +1589,7 @@ sendGroupMessage' user gInfo members chatMsgEvent = sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) sendGroupMessages user gInfo members events = do + -- TODO [knocking] when sending to all, send profile update to pending approval members too, then filter for next step? when shouldSendProfileUpdate $ sendProfileUpdate `catchChatError` (toView . CRChatError (Just user)) sendGroupMessages_ user gInfo members events @@ -1489,7 +1620,10 @@ sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> No sendGroupMessages_ _user gInfo@GroupInfo {groupId} members events = do let idsEvts = L.map (GroupId groupId,) events sndMsgs_ <- lift $ createSndMessages idsEvts - recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) + -- TODO [knocking] Possibly we need to pass GroupSndScope through all functions to here to avoid ad-hoc filtering. + recipientMembers <- case members of + [m] | memberStatus m == GSMemPendingApproval -> pure [m] + _ -> liftIO $ shuffleMembers (filter memberCurrent members) let msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} (toSendSeparate, toSendBatched, toPending, forwarded, _, dups) = foldr' addMember ([], [], [], [], S.empty, 0 :: Int) recipientMembers @@ -1691,7 +1825,7 @@ saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothi saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDSnd) saveSndChatItem' user cd msg content ciFile quotedItem itemForwarded itemTimed live = do let itemTexts = ciContentTexts content - saveSndChatItems user cd [Right NewSndChatItemData {msg, content, itemTexts, itemMentions = M.empty, ciFile, quotedItem, itemForwarded}] itemTimed live >>= \case + saveSndChatItems user cd Nothing [Right NewSndChatItemData {msg, content, itemTexts, itemMentions = M.empty, ciFile, quotedItem, itemForwarded}] itemTimed live >>= \case [Right ci] -> pure ci _ -> throwChatError $ CEInternalError "saveSndChatItem': expected 1 item" @@ -1710,11 +1844,12 @@ saveSndChatItems :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> + Maybe NotInHistory -> [Either ChatError (NewSndChatItemData c)] -> Maybe CITimed -> Bool -> CM [Either ChatError (ChatItem c 'MDSnd)] -saveSndChatItems user cd itemsData itemTimed live = do +saveSndChatItems user cd notInHistory_ itemsData itemTimed live = do createdAt <- liftIO getCurrentTime when (contactChatDeleted cd || any (\NewSndChatItemData {content} -> ciRequiresAttention content) (rights itemsData)) $ withStore' (\db -> updateChatTs db user cd createdAt) @@ -1722,7 +1857,7 @@ saveSndChatItems user cd itemsData itemTimed live = do where createItem :: DB.Connection -> UTCTime -> NewSndChatItemData c -> IO (Either ChatError (ChatItem c 'MDSnd)) createItem db createdAt NewSndChatItemData {msg = msg@SndMessage {sharedMsgId}, content, itemTexts, itemMentions, ciFile, quotedItem, itemForwarded} = do - ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt + ciId <- createNewSndChatItem db user cd notInHistory_ msg content quotedItem itemForwarded itemTimed live createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt let ci = mkChatItem_ cd ciId content itemTexts ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live False createdAt Nothing createdAt Right <$> case cd of @@ -1734,13 +1869,13 @@ saveRcvChatItemNoParse user cd msg brokerTs = saveRcvChatItem user cd msg broker saveRcvChatItem :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> CM (ChatItem c 'MDRcv) saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content = - saveRcvChatItem' user cd msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty + saveRcvChatItem' user cd Nothing msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty ciContentNoParse :: CIContent 'MDRcv -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) ciContentNoParse content = (content, (ciContentToText content, Nothing)) -saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> Map MemberName MsgMention -> CM (ChatItem c 'MDRcv) -saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, forwardedByMember} sharedMsgId_ brokerTs (content, (t, ft_)) ciFile itemTimed live mentions = do +saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> Maybe NotInHistory -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> Map MemberName MsgMention -> CM (ChatItem c 'MDRcv) +saveRcvChatItem' user cd notInHistory_ msg@RcvMessage {chatMsgEvent, forwardedByMember} sharedMsgId_ brokerTs (content, (t, ft_)) ciFile itemTimed live mentions = do createdAt <- liftIO getCurrentTime withStore' $ \db -> do when (ciRequiresAttention content || contactChatDeleted cd) $ updateChatTs db user cd createdAt @@ -1753,7 +1888,7 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, forwardedByMember} shared userMention' = userReply || any (\CIMention {memberId} -> sameMemberId memberId membership) mentions' in pure (mentions', userMention') CDDirectRcv _ -> pure (M.empty, False) - (ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live userMention brokerTs createdAt + (ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd notInHistory_ msg sharedMsgId_ content itemTimed live userMention brokerTs createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt let ci = mkChatItem_ cd ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention brokerTs forwardedByMember createdAt case cd of @@ -1999,7 +2134,7 @@ createLocalChatItems user cd itemsData createdAt = do where createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> IO (ChatItem 'CTLocal 'MDSnd) createItem db (content, ciFile, itemForwarded, ts) = do - ciId <- createNewChatItem_ db user cd Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False createdAt Nothing createdAt + ciId <- createNewChatItem_ db user cd Nothing Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False createdAt Nothing createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt pure $ mkChatItem_ cd ciId content ts ciFile Nothing Nothing itemForwarded Nothing False False createdAt Nothing createdAt diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 087d49e49a..739ab25ea9 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -27,8 +27,8 @@ import qualified Data.ByteString.Lazy.Char8 as LB import Data.Either (lefts, partitionEithers, rights) import Data.Functor (($>)) import Data.Int (Int64) -import Data.List (foldl', partition) -import Data.List.NonEmpty (NonEmpty (..), (<|)) +import Data.List (foldl') +import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M @@ -36,8 +36,7 @@ import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) -import Data.Time (addUTCTime) -import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime) +import Data.Time.Clock (UTCTime, diffUTCTime) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as V4 import Data.Word (Word32) @@ -47,7 +46,7 @@ import Simplex.Chat.Library.Internal import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events -import Simplex.Chat.ProfileGenerator (generateRandomProfile, isRandomName) +import Simplex.Chat.ProfileGenerator (generateRandomProfile) import Simplex.Chat.Protocol import Simplex.Chat.Store import Simplex.Chat.Store.Connections @@ -60,14 +59,12 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared -import Simplex.Chat.Util (shuffle) import Simplex.FileTransfer.Description (ValidFileDescription) import qualified Simplex.FileTransfer.Description as FD import Simplex.FileTransfer.Protocol (FilePartyI) import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent as Agent -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..)) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -296,17 +293,6 @@ agentFileError = \case SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion e -> srvErr . SrvErrOther $ tshow e -splitFileDescr :: Int -> RcvFileDescrText -> NonEmpty FileDescr -splitFileDescr partSize rfdText = splitParts 1 rfdText - where - splitParts partNo remText = - let (part, rest) = T.splitAt partSize remText - complete = T.null rest - fileDescr = FileDescr {fileDescrText = part, fileDescrPartNo = partNo, fileDescrComplete = complete} - in if complete - then fileDescr :| [] - else fileDescr <| splitParts (partNo + 1) rest - processAgentMsgRcvFile :: ACorrId -> RcvFileId -> AEvent 'AERcvFile -> CM () processAgentMsgRcvFile _corrId aFileId msg = do (cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId) @@ -592,14 +578,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> resetContactConnInitiated db user conn' forM_ viaUserContactLink $ \userContactLinkId -> do ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - let (UserContactLink {autoAccept}, groupId_, gLinkMemRole) = ucl + let (UserContactLink {autoAccept}, gli_) = ucl when (connChatVersion < batchSend2Version) $ sendAutoReply ct' autoAccept - forM_ groupId_ $ \groupId -> do + -- TODO REMOVE LEGACY vvv + forM_ gli_ $ \GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do groupInfo <- withStore $ \db -> getGroupInfo db vr user groupId subMode <- chatReadVar subscriptionMode groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode gVar <- asks random withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct' gLinkMemRole groupConnIds connChatVersion peerChatVRange subMode + -- TODO REMOVE LEGACY ^^^ Just (gInfo, m@GroupMember {activeConn}) -> when (maybe False ((== ConnReady) . connStatus) activeConn) $ do notifyMemberConnected gInfo m $ Just ct @@ -658,7 +646,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CRContactSndReady user ct forM_ viaUserContactLink $ \userContactLinkId -> do ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - let (UserContactLink {autoAccept}, _, _) = ucl + let (UserContactLink {autoAccept}, _) = ucl when (connChatVersion >= batchSend2Version) $ sendAutoReply ct autoAccept QCONT -> void $ continueSending connEntity conn @@ -703,6 +691,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = liftIO $ setConnConnReqInv db user connId cReq getHostConnId db user groupId sendXGrpMemInv hostConnId Nothing XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} + -- TODO REMOVE LEGACY vvv -- [async agent commands] group link auto-accept continuation on receiving INV CFCreateConnGrpInv -> do ct <- withStore $ \db -> getContactViaMember db vr user m @@ -728,6 +717,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (_msg, _) <- sendDirectContactMessage user ct $ XGrpInv groupInv -- we could link chat item with sent group invitation message (_msg) createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing + -- TODO REMOVE LEGACY ^^^ _ -> throwChatError $ CECommandError "unexpected cmdFunction" CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" CONF confId _pqSupport _ connInfo -> do @@ -765,7 +755,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise -> messageError "x.grp.mem.info: memberId is different from expected" -- sent when connecting via group link XInfo _ -> - -- TODO [group rejection] Keep rejected member record and connection for ability to start dialogue. + -- TODO Keep rejected member to allow them to appeal against rejection. when (memberStatus m == GSMemRejected) $ do deleteMemberConnection' user m True withStore' $ \db -> deleteGroupMember db user m @@ -773,16 +763,20 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> messageError "INFO from member must have x.grp.mem.info, x.info or x.ok" pure () CON _pqEnc -> unless (memberStatus m == GSMemRejected) $ do - withStore' $ \db -> do - updateGroupMemberStatus db userId m GSMemConnected - unless (memberActive membership) $ - updateGroupMemberStatus db userId membership GSMemConnected - -- possible improvement: check for each pending message, requires keeping track of connection state - unless (connDisabled conn) $ sendPendingGroupMessages user m conn + status' <- case memberStatus m of + GSMemPendingApproval -> pure GSMemPendingApproval + _ -> do + withStore' $ \db -> do + updateGroupMemberStatus db userId m GSMemConnected + unless (memberActive membership) $ + updateGroupMemberStatus db userId membership GSMemConnected + -- possible improvement: check for each pending message, requires keeping track of connection state + unless (connDisabled conn) $ sendPendingGroupMessages user m conn + pure GSMemConnected withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ chatHasNtfs chatSettings case memberCategory m of GCHostMember -> do - toView $ CRUserJoinedGroup user gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected} + toView $ CRUserJoinedGroup user gInfo {membership = membership {memberStatus = status'}} m {memberStatus = status'} let cd = CDGroupRcv gInfo m createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing createGroupFeatureItems user cd CIRcvGroupFeature gInfo @@ -793,125 +787,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = expectHistory = groupFeatureAllowed SGFHistory gInfo && m `supportsVersion` groupHistoryIncludeWelcomeVersion GCInviteeMember -> do memberConnectedChatItem gInfo m - toView $ CRJoinedGroupMember user gInfo m {memberStatus = GSMemConnected} + toView $ CRJoinedGroupMember user gInfo m {memberStatus = status'} let Connection {viaUserContactLink} = conn when (isJust viaUserContactLink && isNothing (memberContactId m)) sendXGrpLinkMem - members <- withStore' $ \db -> getGroupMembers db vr user gInfo - void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m - sendIntroductions members - when (groupFeatureAllowed SGFHistory gInfo) sendHistory when (connChatVersion < batchSend2Version) sendGroupAutoReply + unless (status' == GSMemPendingApproval) $ introduceToGroup vr user gInfo m where sendXGrpLinkMem = do let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo profileToSend = profileToSendOnAccept user profileMode True void $ sendDirectMemberMessage conn (XGrpLinkMem profileToSend) groupId - sendIntroductions members = do - intros <- withStore' $ \db -> createIntroductions db (maxVersion vr) members m - shuffledIntros <- liftIO $ shuffleIntros intros - if m `supportsVersion` batchSendVersion - then do - let events = map (memberIntro . reMember) shuffledIntros - forM_ (L.nonEmpty events) $ \events' -> - sendGroupMemberMessages user conn events' groupId - else forM_ shuffledIntros $ \intro -> - processIntro intro `catchChatError` (toView . CRChatError (Just user)) - memberIntro :: GroupMember -> ChatMsgEvent 'Json - memberIntro reMember = - let mInfo = memberInfo reMember - mRestrictions = memberRestrictions reMember - in XGrpMemIntro mInfo mRestrictions - shuffleIntros :: [GroupMemberIntro] -> IO [GroupMemberIntro] - shuffleIntros intros = do - let (admins, others) = partition isAdmin intros - (admPics, admNoPics) = partition hasPicture admins - (othPics, othNoPics) = partition hasPicture others - mconcat <$> mapM shuffle [admPics, admNoPics, othPics, othNoPics] - where - isAdmin GroupMemberIntro {reMember = GroupMember {memberRole}} = memberRole >= GRAdmin - hasPicture GroupMemberIntro {reMember = GroupMember {memberProfile = LocalProfile {image}}} = isJust image - processIntro intro@GroupMemberIntro {introId} = do - void $ sendDirectMemberMessage conn (memberIntro $ reMember intro) groupId - withStore' $ \db -> updateIntroStatus db introId GMIntroSent - sendHistory = - when (m `supportsVersion` batchSendVersion) $ do - (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) - (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items - let errors = map ChatErrorStore errs <> errs' - unless (null errors) $ toView $ CRChatErrors (Just user) errors - let events' = maybe (concat events) (\x -> concat events <> [x]) descrEvent_ - forM_ (L.nonEmpty events') $ \events'' -> - sendGroupMemberMessages user conn events'' groupId - descrEvent_ :: Maybe (ChatMsgEvent 'Json) - descrEvent_ - | m `supportsVersion` groupHistoryIncludeWelcomeVersion = do - let GroupInfo {groupProfile = GroupProfile {description}} = gInfo - fmap (\descr -> XMsgNew $ MCSimple $ extMsgContent (MCText descr) Nothing) description - | otherwise = Nothing - itemForwardEvents :: CChatItem 'CTGroup -> CM [ChatMsgEvent 'Json] - itemForwardEvents cci = case cci of - (CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv sender, content = CIRcvMsgContent mc, file}) - | not (blockedByAdmin sender) -> do - fInvDescr_ <- join <$> forM file getRcvFileInvDescr - processContentItem sender ci mc fInvDescr_ - (CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent mc, file}) -> do - fInvDescr_ <- join <$> forM file getSndFileInvDescr - processContentItem membership ci mc fInvDescr_ - _ -> pure [] - where - getRcvFileInvDescr :: CIFile 'MDRcv -> CM (Maybe (FileInvitation, RcvFileDescrText)) - getRcvFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do - expired <- fileExpired - if fileProtocol /= FPXFTP || fileStatus == CIFSRcvCancelled || expired - then pure Nothing - else do - rfd <- withStore $ \db -> getRcvFileDescrByRcvFileId db fileId - pure $ invCompleteDescr ciFile rfd - getSndFileInvDescr :: CIFile 'MDSnd -> CM (Maybe (FileInvitation, RcvFileDescrText)) - getSndFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do - expired <- fileExpired - if fileProtocol /= FPXFTP || fileStatus == CIFSSndCancelled || expired - then pure Nothing - else do - -- can also lookup in extra_xftp_file_descriptions, though it can be empty; - -- would be best if snd file had a single rcv description for all members saved in files table - rfd <- withStore $ \db -> getRcvFileDescrBySndFileId db fileId - pure $ invCompleteDescr ciFile rfd - fileExpired :: CM Bool - fileExpired = do - ttl <- asks $ rcvFilesTTL . agentConfig . config - cutoffTs <- addUTCTime (-ttl) <$> liftIO getCurrentTime - pure $ chatItemTs cci < cutoffTs - invCompleteDescr :: CIFile d -> RcvFileDescr -> Maybe (FileInvitation, RcvFileDescrText) - invCompleteDescr CIFile {fileName, fileSize} RcvFileDescr {fileDescrText, fileDescrComplete} - | fileDescrComplete = - let fInvDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} - fInv = xftpFileInvitation fileName fileSize fInvDescr - in Just (fInv, fileDescrText) - | otherwise = Nothing - processContentItem :: GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText) -> CM [ChatMsgEvent 'Json] - processContentItem sender ChatItem {formattedText, meta, quotedItem, mentions} mc fInvDescr_ = - if isNothing fInvDescr_ && not (msgContentHasText mc) - then pure [] - else do - let CIMeta {itemTs, itemSharedMsgId, itemTimed} = meta - quotedItemId_ = quoteItemId =<< quotedItem - fInv_ = fst <$> fInvDescr_ - (mc', _, mentions') = updatedMentionNames mc formattedText mentions - mentions'' = M.map (\CIMention {memberId} -> MsgMention {memberId}) mentions' - (chatMsgEvent, _) <- withStore $ \db -> prepareGroupMsg db user gInfo mc' mentions'' quotedItemId_ Nothing fInv_ itemTimed False - let senderVRange = memberChatVRange' sender - xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent} - fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of - (Just fileDescrText, Just msgId) -> do - partSize <- asks $ xftpDescrPartSize . config - let parts = splitFileDescr partSize fileDescrText - pure . L.toList $ L.map (XMsgFileDescr msgId) parts - _ -> pure [] - let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents - GroupMember {memberId} = sender - msgForwardEvents = map (\cm -> XGrpMsgForward memberId cm itemTs) (xMsgNewChatMsg : fileDescrChatMsgs) - pure msgForwardEvents _ -> do let memCategory = memberCategory m withStore' (\db -> getViaGroupContact db vr user m) >>= \case @@ -974,6 +859,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m' sharedMsgId fileConnReq_ fName XInfo p -> xInfoMember gInfo m' p brokerTs XGrpLinkMem p -> xGrpLinkMem gInfo m' conn' p + XGrpLinkAcpt role -> xGrpLinkAcpt gInfo m' role XGrpMemNew memInfo -> xGrpMemNew gInfo m' memInfo msg brokerTs XGrpMemIntro memInfo memRestrictions_ -> xGrpMemIntro gInfo m' memInfo memRestrictions_ XGrpMemInv memId introInv -> xGrpMemInv gInfo m' memId introInv @@ -1294,13 +1180,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> pure () where profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> CM () - profileContactRequest invId chatVRange p@Profile {displayName, image} xContactId_ reqPQSup = do + profileContactRequest invId chatVRange p@Profile {displayName} xContactId_ reqPQSup = do withStore (\db -> createOrUpdateContactRequest db vr user userContactLinkId invId chatVRange p xContactId_ reqPQSup) >>= \case CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact CORGroup gInfo -> toView $ CRBusinessRequestAlreadyAccepted user gInfo CORRequest cReq -> do ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - let (UserContactLink {connReqContact, autoAccept}, groupId_, gLinkMemRole) = ucl + let (UserContactLink {connReqContact, autoAccept}, gLinkInfo_) = ucl isSimplexTeam = sameConnReqContact connReqContact adminContactReq v = maxVersion chatVRange case autoAccept of @@ -1313,49 +1199,37 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = else do gInfo <- acceptBusinessJoinRequestAsync user cReq toView $ CRAcceptingBusinessRequest user gInfo - | otherwise -> case groupId_ of + | otherwise -> case gLinkInfo_ of Nothing -> do -- [incognito] generate profile to send, create connection with incognito profile incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing ct <- acceptContactRequestAsync user cReq incognitoProfile reqPQSup toView $ CRAcceptingContactRequest user ct - Just groupId -> do + Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do gInfo <- withStore $ \db -> getGroupInfo db vr user groupId - cfg <- asks config - case rejectionReason cfg of - Nothing + acceptMember_ <- asks $ acceptMember . chatHooks . config + maybe (pure $ Right (GAAccepted, gLinkMemRole)) (\am -> liftIO $ am gInfo gli p) acceptMember_ >>= \case + Right (acceptance, useRole) | v < groupFastLinkJoinVersion -> messageError "processUserContactRequest: chat version range incompatible for accepting group join request" | otherwise -> do let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo - useRole = userMemberRole gLinkMemRole $ acceptAsObserver cfg - mem <- acceptGroupJoinRequestAsync user gInfo cReq useRole profileMode + mem <- acceptGroupJoinRequestAsync user gInfo cReq acceptance useRole profileMode createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing toView $ CRAcceptingGroupJoinRequestMember user gInfo mem - Just rjctReason + Left rjctReason | v < groupJoinRejectVersion -> messageWarning $ "processUserContactRequest (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked" | otherwise -> do mem <- acceptGroupJoinSendRejectAsync user gInfo cReq rjctReason toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason _ -> toView $ CRReceivedContactRequest user cReq - where - rejectionReason ChatConfig {profileNameLimit, allowedProfileName} - | T.length displayName > profileNameLimit = Just GRRLongName - | maybe False (\f -> not $ f displayName) allowedProfileName = Just GRRBlockedName - | otherwise = Nothing - userMemberRole linkRole = \case - Just AOAll -> GRObserver - Just AONameOnly | noImage -> GRObserver - Just AOIncognito | noImage && isRandomName displayName -> GRObserver - _ -> linkRole - where - noImage = maybe True (\(ImageData i) -> i == "") image + -- TODO [knocking] review memberCanSend :: GroupMember -> CM () -> CM () - memberCanSend GroupMember {memberRole} a - | memberRole <= GRObserver = messageError "member is not allowed to send messages" - | otherwise = a + memberCanSend GroupMember {memberRole, memberStatus} a + | memberRole > GRObserver || memberStatus == GSMemPendingApproval = a + | otherwise = messageError "member is not allowed to send messages" processConnMERR :: ConnectionEntity -> Connection -> AgentErrorType -> CM () processConnMERR connEntity conn err = do @@ -1576,7 +1450,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where brokerTs = metaBrokerTs msgMeta newChatItem content ciFile_ timed_ live = do - ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs content ciFile_ timed_ live M.empty + ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content ciFile_ timed_ live M.empty reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getDirectCIReactions db ct sharedMsgId) sharedMsgId_ toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci {reactions}] @@ -1643,7 +1517,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- Chat item and update message which created it will have different sharedMsgId in this case... let timed_ = rcvContactCITimed ct ttl ts = ciContentTexts content - ci <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live M.empty + ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live M.empty ci' <- withStore' $ \db -> do createChatItemVersion db (chatItemId' ci) brokerTs mc updateDirectChatItem' db user contactId ci content True live Nothing Nothing @@ -1760,10 +1634,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = live' = fromMaybe False live_ ExtMsgContent content mentions fInv_ itemTTL live_ = mcExtMsgContent mc ts@(_, ft_) = msgContentTexts content + saveRcvCI = saveRcvChatItem' user (CDGroupRcv gInfo m) (memberNotInHistory m) msg sharedMsgId_ brokerTs createBlockedByAdmin | groupFeatureAllowed SGFFullDelete gInfo = do -- ignores member role when blocked by admin - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs (ciContentNoParse CIRcvBlocked) Nothing timed' False M.empty + ci <- saveRcvCI (ciContentNoParse CIRcvBlocked) Nothing timed' False M.empty ci' <- withStore' $ \db -> updateGroupCIBlockedByAdmin db user gInfo ci brokerTs groupMsgToView gInfo ci' | otherwise = do @@ -1775,7 +1650,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | moderatorRole < GRModerator || moderatorRole < memberRole = createContentItem | groupFeatureMemberAllowed SGFFullDelete moderator gInfo = do - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs (ciContentNoParse CIRcvModerated) Nothing timed' False M.empty + ci <- saveRcvCI (ciContentNoParse CIRcvModerated) Nothing timed' False M.empty ci' <- withStore' $ \db -> updateGroupChatItemModerated db user gInfo ci moderator moderatedAt groupMsgToView gInfo ci' | otherwise = do @@ -1783,7 +1658,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ci <- createNonLive file_ toView =<< markGroupCIsDeleted user gInfo [CChatItem SMDRcv ci] False (Just moderator) moderatedAt createNonLive file_ = - saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs (CIRcvMsgContent content, ts) (snd <$> file_) timed' False mentions + saveRcvCI (CIRcvMsgContent content, ts) (snd <$> file_) timed' False mentions createContentItem = do file_ <- processFileInv newChatItem (CIRcvMsgContent content, ts) (snd <$> file_) timed' live' @@ -1792,7 +1667,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m newChatItem ciContent ciFile_ timed_ live = do let mentions' = if showMessages (memberSettings m) then mentions else [] - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs ciContent ciFile_ timed_ live mentions' + ci <- saveRcvCI ciContent ciFile_ timed_ live mentions' ci' <- blockedMember m ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo ci reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getGroupCIReactions db gInfo memberId sharedMsgId) sharedMsgId_ groupMsgToView gInfo ci' {reactions} @@ -1808,7 +1683,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- Chat item and update message which created it will have different sharedMsgId in this case... let timed_ = rcvGroupCITimed gInfo ttl_ mentions' = if showMessages (memberSettings m) then mentions else [] - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live mentions' + ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) (memberNotInHistory m) msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live mentions' ci' <- withStore' $ \db -> do createChatItemVersion db (chatItemId' ci) brokerTs mc ci' <- updateGroupChatItem db user groupId ci content True live Nothing @@ -1841,6 +1716,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = else messageError "x.msg.update: group member attempted to update a message of another member" _ -> messageError "x.msg.update: group member attempted invalid message update" + memberNotInHistory :: GroupMember -> Maybe NotInHistory + memberNotInHistory = \case + GroupMember {memberStatus = GSMemPendingApproval} -> Just NotInHistory + _ -> Nothing + groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> RcvMessage -> UTCTime -> CM () groupMessageDelete gInfo@GroupInfo {membership} m@GroupMember {memberId, memberRole = senderRole} sharedMsgId sndMemberId_ RcvMessage {msgId} brokerTs = do let msgMemberId = fromMaybe memberId sndMemberId_ @@ -1896,7 +1776,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} content = ciContentNoParse $ CIRcvMsgContent $ MCFile "" - ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty + ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] where brokerTs = metaBrokerTs msgMeta @@ -1910,7 +1790,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} content = ciContentNoParse $ CIRcvMsgContent $ MCFile "" - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty + ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) Nothing msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty ci' <- blockedMember m ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo ci groupMsgToView gInfo ci' @@ -2173,16 +2053,27 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xInfoMember gInfo m p' brokerTs = void $ processMemberProfileUpdate gInfo m p' True (Just brokerTs) xGrpLinkMem :: GroupInfo -> GroupMember -> Connection -> Profile -> CM () - xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' = do + xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory, memberStatus} Connection {viaGroupLink} p' = do xGrpLinkMemReceived <- withStore $ \db -> getXGrpLinkMemReceived db groupMemberId if (viaGroupLink || isJust businessChat) && isNothing (memberContactId m) && memberCategory == GCHostMember && not xGrpLinkMemReceived then do m' <- processMemberProfileUpdate gInfo m p' False Nothing withStore' $ \db -> setXGrpLinkMemReceived db groupMemberId True - let connectedIncognito = memberIncognito membership - probeMatchingMemberContact m' connectedIncognito + unless (memberStatus == GSMemPendingApproval) $ do + let connectedIncognito = memberIncognito membership + probeMatchingMemberContact m' connectedIncognito else messageError "x.grp.link.mem error: invalid group link host profile update" + xGrpLinkAcpt :: GroupInfo -> GroupMember -> GroupMemberRole -> CM () + xGrpLinkAcpt gInfo@GroupInfo {membership} m role = do + membership' <- withStore' $ \db -> do + updateGroupMemberStatus db userId m GSMemConnected + updateGroupMemberAccepted db user membership role + let m' = m {memberStatus = GSMemConnected} + toView $ CRUserJoinedGroup user gInfo {membership = membership'} m' + let connectedIncognito = memberIncognito membership + probeMatchingMemberContact m' connectedIncognito + processMemberProfileUpdate :: GroupInfo -> GroupMember -> Profile -> Bool -> Maybe UTCTime -> CM GroupMember processMemberProfileUpdate gInfo m@GroupMember {memberProfile = p, memberContactId} p' createItems itemTs_ | redactedMemberProfile (fromLocalProfile p) /= redactedMemberProfile p' = do @@ -2330,7 +2221,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = saveCallItem status = saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs (CIRcvCall status 0) featureRejected f = do let content = ciContentNoParse $ CIRcvChatFeatureRejected f - ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty + ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] -- to party initiating call diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 5562f016c9..ae88bc796b 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -162,6 +162,8 @@ data ChatItem (c :: ChatType) (d :: MsgDirection) = ChatItem } deriving (Show) +data NotInHistory = NotInHistory + data CIMention = CIMention { memberId :: MemberId, -- member record can be created later than the mention is received diff --git a/src/Simplex/Chat/ProfileGenerator.hs b/src/Simplex/Chat/ProfileGenerator.hs index 8380bb58f1..5b6c9222cc 100644 --- a/src/Simplex/Chat/ProfileGenerator.hs +++ b/src/Simplex/Chat/ProfileGenerator.hs @@ -2,8 +2,6 @@ module Simplex.Chat.ProfileGenerator where -import qualified Data.Attoparsec.Text as A -import Data.Either (isRight) import Data.Text (Text) import Simplex.Chat.Types (Profile (..)) import System.Random (randomRIO) @@ -25,15 +23,6 @@ generateRandomProfile = do then pickNoun adjective (n - 1) else pure noun --- This function does not check for exact match with this disctionary, --- it only checks for the WordWord style. -isRandomName :: Text -> Bool -isRandomName = isRight . A.parseOnly randomNameP - where - randomNameP = A.satisfy upper >> A.takeWhile1 lower >> A.satisfy upper >> A.takeWhile1 lower >> A.endOfInput - upper c = c >= 'A' && c <= 'Z' - lower c = c >= 'a' && c <= 'z' - adjectives :: [Text] adjectives = [ "Abatic", @@ -1503,7 +1492,6 @@ adjectives = "Recommendable", "Rectangular", "Recuperative", - "Red", "Refined", "Reflecting", "Reflective", @@ -2940,7 +2928,6 @@ nouns = "Sister", "Size", "Skill", - "Skin", "Skipper", "Sleek", "Slick", diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 94e08a0897..a145914ce7 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -333,6 +333,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpLinkInv :: GroupLinkInvitation -> ChatMsgEvent 'Json XGrpLinkReject :: GroupLinkRejection -> ChatMsgEvent 'Json XGrpLinkMem :: Profile -> ChatMsgEvent 'Json + XGrpLinkAcpt :: GroupMemberRole -> ChatMsgEvent 'Json XGrpMemNew :: MemberInfo -> ChatMsgEvent 'Json XGrpMemIntro :: MemberInfo -> Maybe MemberRestrictions -> ChatMsgEvent 'Json XGrpMemInv :: MemberId -> IntroInvitation -> ChatMsgEvent 'Json @@ -823,6 +824,7 @@ data CMEventTag (e :: MsgEncoding) where XGrpLinkInv_ :: CMEventTag 'Json XGrpLinkReject_ :: CMEventTag 'Json XGrpLinkMem_ :: CMEventTag 'Json + XGrpLinkAcpt_ :: CMEventTag 'Json XGrpMemNew_ :: CMEventTag 'Json XGrpMemIntro_ :: CMEventTag 'Json XGrpMemInv_ :: CMEventTag 'Json @@ -875,6 +877,7 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XGrpLinkInv_ -> "x.grp.link.inv" XGrpLinkReject_ -> "x.grp.link.reject" XGrpLinkMem_ -> "x.grp.link.mem" + XGrpLinkAcpt_ -> "x.grp.link.acpt" XGrpMemNew_ -> "x.grp.mem.new" XGrpMemIntro_ -> "x.grp.mem.intro" XGrpMemInv_ -> "x.grp.mem.inv" @@ -928,6 +931,7 @@ instance StrEncoding ACMEventTag where "x.grp.link.inv" -> XGrpLinkInv_ "x.grp.link.reject" -> XGrpLinkReject_ "x.grp.link.mem" -> XGrpLinkMem_ + "x.grp.link.acpt" -> XGrpLinkAcpt_ "x.grp.mem.new" -> XGrpMemNew_ "x.grp.mem.intro" -> XGrpMemIntro_ "x.grp.mem.inv" -> XGrpMemInv_ @@ -977,6 +981,7 @@ toCMEventTag msg = case msg of XGrpLinkInv _ -> XGrpLinkInv_ XGrpLinkReject _ -> XGrpLinkReject_ XGrpLinkMem _ -> XGrpLinkMem_ + XGrpLinkAcpt _ -> XGrpLinkAcpt_ XGrpMemNew _ -> XGrpMemNew_ XGrpMemIntro _ _ -> XGrpMemIntro_ XGrpMemInv _ _ -> XGrpMemInv_ @@ -1079,6 +1084,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpLinkInv_ -> XGrpLinkInv <$> p "groupLinkInvitation" XGrpLinkReject_ -> XGrpLinkReject <$> p "groupLinkRejection" XGrpLinkMem_ -> XGrpLinkMem <$> p "profile" + XGrpLinkAcpt_ -> XGrpLinkAcpt <$> p "role" XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo" XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo" <*> opt "memberRestrictions" XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro" @@ -1142,6 +1148,7 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @ XGrpLinkInv groupLinkInv -> o ["groupLinkInvitation" .= groupLinkInv] XGrpLinkReject groupLinkRjct -> o ["groupLinkRejection" .= groupLinkRjct] XGrpLinkMem profile -> o ["profile" .= profile] + XGrpLinkAcpt role -> o ["role" .= role] XGrpMemNew memInfo -> o ["memberInfo" .= memInfo] XGrpMemIntro memInfo memRestrictions -> o $ ("memberRestrictions" .=? memRestrictions) ["memberInfo" .= memInfo] XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro] diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 03b4d7a640..a41641f88d 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -6,6 +6,7 @@ module Simplex.Chat.Store ChatLockEntity (..), UserMsgReceiptSettings (..), UserContactLink (..), + GroupLinkInfo (..), AutoAccept (..), createChatStore, migrations, -- used in tests diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index a1ce3ab269..0094e20cb8 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -78,6 +78,7 @@ module Simplex.Chat.Store.Groups createMemberConnectionAsync, updateGroupMemberStatus, updateGroupMemberStatusById, + updateGroupMemberAccepted, createNewGroupMember, checkGroupMemberHasItems, deleteGroupMember, @@ -520,9 +521,10 @@ deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile { DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId) createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) -createGroupInvitedViaLink db vr user conn GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, business} = do +createGroupInvitedViaLink db vr user conn GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted, business} = do let fromMemberProfile = profileFromName fromMemberName - createGroupViaLink' db vr user conn fromMember fromMemberProfile invitedMember groupProfile business GSMemAccepted + initialStatus = maybe GSMemAccepted acceptanceToStatus accepted + createGroupViaLink' db vr user conn fromMember fromMemberProfile invitedMember groupProfile business initialStatus createGroupRejectedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember) createGroupRejectedViaLink db vr user conn GroupLinkRejection {fromMember = fromMember@MemberIdRole {memberId}, invitedMember, groupProfile} = do @@ -1201,6 +1203,19 @@ updateGroupMemberStatusById db userId groupMemberId memStatus = do |] (memStatus, currentTs, userId, groupMemberId) +updateGroupMemberAccepted :: DB.Connection -> User -> GroupMember -> GroupMemberRole -> IO GroupMember +updateGroupMemberAccepted db User {userId} m@GroupMember {groupMemberId} role = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE group_members + SET member_status = ?, member_role = ?, updated_at = ? + WHERE user_id = ? AND group_member_id = ? + |] + (GSMemConnected, role, currentTs, userId, groupMemberId) + pure m {memberStatus = GSMemConnected, memberRole = role, updatedAt = currentTs} + -- | add new member with profile createNewGroupMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> ExceptT StoreError IO GroupMember createNewGroupMember db user gInfo invitingMember memInfo@MemberInfo {profile} memCategory memStatus = do diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 1902fd002e..00158e0054 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -142,7 +142,7 @@ import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Maybe (catMaybes, fromMaybe, isJust, mapMaybe) +import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe) import Data.Ord (Down (..), comparing) import Data.Text (Text) import qualified Data.Text as T @@ -372,9 +372,9 @@ updateChatTs db User {userId} chatDirection chatTs = case toChatInfo chatDirecti (chatTs, userId, noteFolderId) _ -> pure () -createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId -createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live createdAt = - createNewChatItem_ db user chatDirection createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False createdAt Nothing createdAt +createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> Maybe NotInHistory -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId +createNewSndChatItem db user chatDirection notInHistory_ SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live createdAt = + createNewChatItem_ db user chatDirection notInHistory_ createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False createdAt Nothing createdAt where createdByMsgId = if msgId == 0 then Nothing else Just msgId quoteRow :: NewQuoteRow @@ -388,9 +388,9 @@ createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciCon CIQGroupRcv (Just GroupMember {memberId}) -> (Just False, Just memberId) CIQGroupRcv Nothing -> (Just False, Nothing) -createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom) -createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live userMention itemTs createdAt = do - ciId <- createNewChatItem_ db user chatDirection (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt +createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> Maybe NotInHistory -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom) +createNewRcvChatItem db user chatDirection notInHistory_ RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live userMention itemTs createdAt = do + ciId <- createNewChatItem_ db user chatDirection notInHistory_ (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg pure (ciId, quotedItem, itemForwarded) where @@ -407,13 +407,13 @@ createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forw createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> CIContent d -> UTCTime -> UTCTime -> IO ChatItemId createNewChatItemNoMsg db user chatDirection ciContent itemTs = - createNewChatItem_ db user chatDirection Nothing Nothing ciContent quoteRow Nothing Nothing False False itemTs Nothing + createNewChatItem_ db user chatDirection Nothing Nothing Nothing ciContent quoteRow Nothing Nothing False False itemTs Nothing where quoteRow :: NewQuoteRow quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing) -createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId -createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt = do +createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> Maybe NotInHistory -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId +createNewChatItem_ db User {userId} chatDirection notInHistory_ msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt = do DB.execute db [sql| @@ -448,7 +448,7 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q includeInHistory :: Bool includeInHistory = let (_, groupId_, _, _) = idsRow - in isJust groupId_ && isJust (ciMsgContent ciContent) && ((msgContentTag <$> ciMsgContent ciContent) /= Just MCReport_) + in isJust groupId_ && isNothing notInHistory_ && isJust (ciMsgContent ciContent) && ((msgContentTag <$> ciMsgContent ciContent) /= Just MCReport_) forwardedFromRow :: (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64) forwardedFromRow = case itemForwarded of Nothing -> @@ -2319,9 +2319,9 @@ updateGroupCIMentions db g ci@ChatItem {mentions} mentions' unless (null mentions) $ deleteMentions if null mentions' then pure ci - -- This is a fallback for the error that should not happen in practice. + else -- This is a fallback for the error that should not happen in practice. -- In theory, it may happen in item mentions in database are different from item record. - else createMentions `E.catch` \e -> if constraintError e then deleteMentions >> createMentions else E.throwIO e + createMentions `E.catch` \e -> if constraintError e then deleteMentions >> createMentions else E.throwIO e where deleteMentions = DB.execute db "DELETE FROM chat_item_mentions WHERE chat_item_id = ?" (Only $ chatItemId' ci) createMentions = createGroupCIMentions db g ci mentions' @@ -3138,6 +3138,7 @@ getGroupSndStatusCounts db itemId = |] (Only itemId) +-- TODO [knocking] filter out messages sent to member only getGroupHistoryItems :: DB.Connection -> User -> GroupInfo -> GroupMember -> Int -> IO [Either StoreError (CChatItem 'CTGroup)] getGroupHistoryItems db user@User {userId} g@GroupInfo {groupId} m count = do ciIds <- getLastItemIds_ diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index a8d1c094d4..22d2a7b1f5 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -18,6 +18,7 @@ module Simplex.Chat.Store.Profiles ( AutoAccept (..), UserMsgReceiptSettings (..), UserContactLink (..), + GroupLinkInfo (..), createUserRecord, createUserRecordAt, getUsersInfo, @@ -47,6 +48,7 @@ module Simplex.Chat.Store.Profiles deleteUserAddress, getUserAddress, getUserContactLinkById, + getGroupLinkInfo, getUserContactLinkByConnReq, getContactWithoutConnViaAddress, updateUserAddressAutoAccept, @@ -453,6 +455,12 @@ data UserContactLink = UserContactLink } deriving (Show) +data GroupLinkInfo = GroupLinkInfo + { groupId :: GroupId, + memberRole :: GroupMemberRole + } + deriving (Show) + data AutoAccept = AutoAccept { businessAddress :: Bool, -- possibly, it can be wrapped together with acceptIncognito, or AutoAccept made sum type acceptIncognito :: IncognitoEnabled, @@ -481,18 +489,28 @@ getUserAddress db User {userId} = |] (Only userId) -getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO (UserContactLink, Maybe GroupId, GroupMemberRole) +getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO (UserContactLink, Maybe GroupLinkInfo) getUserContactLinkById db userId userContactLinkId = - ExceptT . firstRow (\(ucl :. (groupId_, mRole_)) -> (toUserContactLink ucl, groupId_, fromMaybe GRMember mRole_)) SEUserContactLinkNotFound $ - DB.query - db - [sql| - SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role - FROM user_contact_links - WHERE user_id = ? - AND user_contact_link_id = ? - |] - (userId, userContactLinkId) + ExceptT . firstRow (\(ucl :. gli) -> (toUserContactLink ucl, toGroupLinkInfo gli)) SEUserContactLinkNotFound $ + DB.query db (groupLinkInfoQuery <> " AND user_contact_link_id = ?") (userId, userContactLinkId) + +groupLinkInfoQuery :: Query +groupLinkInfoQuery = + [sql| + SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role + FROM user_contact_links + WHERE user_id = ? + |] + +toGroupLinkInfo :: (Maybe GroupId, Maybe GroupMemberRole) -> Maybe GroupLinkInfo +toGroupLinkInfo (groupId_, mRole_) = + (\groupId -> GroupLinkInfo {groupId, memberRole = fromMaybe GRMember mRole_}) + <$> groupId_ + +getGroupLinkInfo :: DB.Connection -> UserId -> GroupId -> IO (Maybe GroupLinkInfo) +getGroupLinkInfo db userId groupId = + fmap join $ maybeFirstRow toGroupLinkInfo $ + DB.query db (groupLinkInfoQuery <> " AND group_id = ?") (userId, groupId) getUserContactLinkByConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe UserContactLink) getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) = diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 72f8e4b8fd..01f4cf3f68 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -4870,7 +4870,7 @@ Query: Plan: SCAN usage_conditions -Query: SELECT chat_item_id FROM chat_items WHERE ( user_id = ? AND group_id = ? AND item_ts < ? ) OR ( user_id = ? AND group_id = ? AND item_ts = ? AND chat_item_id < ? ) ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND item_ts < ? ) OR ( user_id = ? AND group_id = ? AND item_ts = ? AND chat_item_id < ? )) ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? Plan: MULTI-INDEX OR INDEX 1 @@ -4879,7 +4879,7 @@ INDEX 2 SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=? AND item_ts=? AND rowid ? ) OR ( user_id = ? AND group_id = ? AND item_ts = ? AND chat_item_id > ? ) ORDER BY item_ts ASC, chat_item_id ASC LIMIT ? +Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND item_ts > ? ) OR ( user_id = ? AND group_id = ? AND item_ts = ? AND chat_item_id > ? )) ORDER BY item_ts ASC, chat_item_id ASC LIMIT ? Plan: MULTI-INDEX OR INDEX 1 diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 94b67d8349..5ba7565611 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -668,6 +668,7 @@ data GroupLinkInvitation = GroupLinkInvitation fromMemberName :: ContactName, invitedMember :: MemberIdRole, groupProfile :: GroupProfile, + accepted :: Maybe GroupAcceptance, business :: Maybe BusinessChatInfo, groupSize :: Maybe Int } @@ -997,6 +998,7 @@ data GroupMemberStatus | GSMemGroupDeleted -- user member of the deleted group | GSMemUnknown -- unknown member, whose message was forwarded by an admin (likely member wasn't introduced due to not being a current member, but message was included in history) | GSMemInvited -- member is sent to or received invitation to join the group + | GSMemPendingApproval -- member is connected to host but pending host approval before connecting to other members ("knocking") | GSMemIntroduced -- user received x.grp.mem.intro for this member (only with GCPreMember) | GSMemIntroInvited -- member is sent to or received from intro invitation | GSMemAccepted -- member accepted invitation (only User and Invitee) @@ -1017,6 +1019,11 @@ instance ToJSON GroupMemberStatus where toJSON = J.String . textEncode toEncoding = JE.text . textEncode +acceptanceToStatus :: GroupAcceptance -> GroupMemberStatus +acceptanceToStatus = \case + GAAccepted -> GSMemAccepted + GAPending -> GSMemPendingApproval + memberActive :: GroupMember -> Bool memberActive m = case memberStatus m of GSMemRejected -> False @@ -1025,6 +1032,7 @@ memberActive m = case memberStatus m of GSMemGroupDeleted -> False GSMemUnknown -> False GSMemInvited -> False + GSMemPendingApproval -> True GSMemIntroduced -> False GSMemIntroInvited -> False GSMemAccepted -> False @@ -1045,6 +1053,7 @@ memberCurrent' = \case GSMemGroupDeleted -> False GSMemUnknown -> False GSMemInvited -> False + GSMemPendingApproval -> False GSMemIntroduced -> True GSMemIntroInvited -> True GSMemAccepted -> True @@ -1061,6 +1070,7 @@ memberRemoved m = case memberStatus m of GSMemGroupDeleted -> True GSMemUnknown -> False GSMemInvited -> False + GSMemPendingApproval -> False GSMemIntroduced -> False GSMemIntroInvited -> False GSMemAccepted -> False @@ -1077,6 +1087,7 @@ instance TextEncoding GroupMemberStatus where "deleted" -> Just GSMemGroupDeleted "unknown" -> Just GSMemUnknown "invited" -> Just GSMemInvited + "pending_approval" -> Just GSMemPendingApproval "introduced" -> Just GSMemIntroduced "intro-inv" -> Just GSMemIntroInvited "accepted" -> Just GSMemAccepted @@ -1092,6 +1103,7 @@ instance TextEncoding GroupMemberStatus where GSMemGroupDeleted -> "deleted" GSMemUnknown -> "unknown" GSMemInvited -> "invited" + GSMemPendingApproval -> "pending_approval" GSMemIntroduced -> "introduced" GSMemIntroInvited -> "intro-inv" GSMemAccepted -> "accepted" diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs index d5c8f48776..e22610cfe5 100644 --- a/src/Simplex/Chat/Types/Shared.hs +++ b/src/Simplex/Chat/Types/Shared.hs @@ -48,3 +48,27 @@ instance FromJSON GroupMemberRole where instance ToJSON GroupMemberRole where toJSON = strToJSON toEncoding = strToJEncoding + +data GroupAcceptance = GAAccepted | GAPending deriving (Eq, Show) + +-- TODO [knocking] encoding doesn't match field type +instance FromField GroupAcceptance where fromField = blobFieldDecoder strDecode + +instance ToField GroupAcceptance where toField = toField . strEncode + +instance StrEncoding GroupAcceptance where + strEncode = \case + GAAccepted -> "accepted" + GAPending -> "pending" + strDecode = \case + "accepted" -> Right GAAccepted + "pending" -> Right GAPending + r -> Left $ "bad GroupAcceptance " <> B.unpack r + strP = strDecode <$?> A.takeByteString + +instance FromJSON GroupAcceptance where + parseJSON = strParseJSON "GroupAcceptance" + +instance ToJSON GroupAcceptance where + toJSON = strToJSON + toEncoding = strToJEncoding diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 7a20cb2fb0..1c4f4258d8 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -1076,14 +1076,22 @@ viewNetworkStatuses = map viewStatuses . L.groupBy ((==) `on` netStatus) . sortO viewStatuses ss@(s :| _) = plain $ show (L.length ss) <> " connections " <> netStatusStr (netStatus s) viewUserJoinedGroup :: GroupInfo -> [StyledString] -viewUserJoinedGroup g = +viewUserJoinedGroup g@GroupInfo {membership} = case incognitoMembershipProfile g of - Just mp -> [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile mp)] - Nothing -> [ttyGroup' g <> ": you joined the group"] + Just mp -> [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile mp) <> pendingApproval_] + Nothing -> [ttyGroup' g <> ": you joined the group" <> pendingApproval_] + where + pendingApproval_ = case memberStatus membership of + GSMemPendingApproval -> ", pending approval" + _ -> "" viewJoinedGroupMember :: GroupInfo -> GroupMember -> [StyledString] -viewJoinedGroupMember g m = - [ttyGroup' g <> ": " <> ttyMember m <> " joined the group "] +viewJoinedGroupMember g@GroupInfo {groupId} m@GroupMember {groupMemberId, memberStatus} = case memberStatus of + GSMemPendingApproval -> + [ (ttyGroup' g <> ": " <> ttyMember m <> " connected and pending approval, ") + <> ("use " <> highlight ("/_accept member #" <> show groupId <> " " <> show groupMemberId <> " ") <> " to accept member") + ] + _ -> [ttyGroup' g <> ": " <> ttyMember m <> " joined the group "] viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString] viewReceivedGroupInvitation g c role = diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 2a9ad30dd2..31b36159fd 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -86,12 +86,13 @@ mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup = adminUsers = [], superUsers, ownersGroup, - directoryLog = Just $ ps "directory_service.log", + blockedFragmentsFile = Nothing, blockedWordsFile = Nothing, blockedExtensionRules = Nothing, nameSpellingFile = Nothing, profileNameLimit = maxBound, - acceptAsObserver = Nothing, + captchaGenerator = Nothing, + directoryLog = Just $ ps "directory_service.log", serviceName = "SimpleX-Directory", runCLI = False, searchResults = 3, @@ -182,6 +183,8 @@ testDirectoryService ps = superUser <## " Group approved!" bob <# "SimpleX-Directory> The group ID 1 (PSA) is approved and listed in directory!" bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." + bob <## "" + bob <## "Use /filter 1 to configure anti-spam filter and /role 1 to set default member role." search bob "privacy" welcomeWithLink' search bob "security" welcomeWithLink' cath `connectVia` dsLink @@ -1045,6 +1048,8 @@ reapproveGroup count superUser bob = do superUser <## " Group approved!" bob <# "SimpleX-Directory> The group ID 1 (privacy) is approved and listed in directory!" bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." + bob <## "" + bob <## "Use /filter 1 to configure anti-spam filter and /role 1 to set default member role." addCathAsOwner :: HasCallStack => TestCC -> TestCC -> IO () addCathAsOwner bob cath = do @@ -1114,7 +1119,9 @@ runDirectory cfg opts@DirectoryOpts {directoryLog} action = do threadDelay 500000 action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t) where - bot st = simplexChatCore cfg (mkChatOpts opts) $ directoryService st opts + bot st = do + env <- newServiceState opts + simplexChatCore cfg (mkChatOpts opts) $ directoryService st opts env registerGroup :: TestCC -> TestCC -> String -> String -> IO () registerGroup su u n fn = registerGroupId su u n fn 1 1 @@ -1187,6 +1194,8 @@ approveRegistrationId su u n gId ugId = do su <## " Group approved!" u <# ("SimpleX-Directory> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory!") u <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." + u <## "" + u <## ("Use /filter " <> show ugId <> " to configure anti-spam filter and /role " <> show ugId <> " to set default member role.") connectVia :: TestCC -> String -> IO () u `connectVia` dsLink = do diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 14539ac219..7ec4033046 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -20,14 +20,14 @@ import qualified Data.ByteString.Char8 as B import Data.List (intercalate, isInfixOf) import qualified Data.Map.Strict as M import qualified Data.Text as T -import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) import Simplex.Chat.Library.Internal (uniqueMsgMentions, updatedMentionNames) import Simplex.Chat.Markdown (parseMaybeMarkdownList) import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId) import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgMention (..), MsgContent (..), msgContentText) import Simplex.Chat.Types -import Simplex.Chat.Types.Shared (GroupMemberRole (..)) +import Simplex.Chat.Types.Shared (GroupMemberRole (..), GroupAcceptance (..)) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.RetryInterval import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -98,7 +98,11 @@ chatGroupTests = do it "group link member role" testGroupLinkMemberRole it "host profile received" testGroupLinkHostProfileReceived it "existing contact merged" testGroupLinkExistingContactMerged - it "reject member joining via group link - blocked name" testGroupLinkRejectBlockedName + describe "group links - join rejection" $ do + it "reject member joining via group link - blocked name" testGLinkRejectBlockedName + describe "group links - manual acceptance" $ do + it "manually accept member joining via group link" testGLinkManualAcceptMember + it "delete pending member" testGLinkDeletePendingMember describe "group link connection plan" $ do it "ok to connect; known group" testPlanGroupLinkKnown it "own group link" testPlanGroupLinkOwn @@ -185,6 +189,8 @@ chatGroupTests = do it "should send updated mentions in history" testGroupHistoryWithMentions describe "uniqueMsgMentions" testUniqueMsgMentions describe "updatedMentionNames" testUpdatedMentionNames + describe "group direct messages" $ do + it "should send group direct messages" testGroupDirectMessages testGroupCheckMessages :: HasCallStack => TestParams -> IO () testGroupCheckMessages = @@ -2867,8 +2873,8 @@ testGroupLinkExistingContactMerged = bob #> "#team hi there" alice <# "#team bob> hi there" -testGroupLinkRejectBlockedName :: HasCallStack => TestParams -> IO () -testGroupLinkRejectBlockedName = +testGLinkRejectBlockedName :: HasCallStack => TestParams -> IO () +testGLinkRejectBlockedName = testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do alice ##> "/g team" @@ -2894,7 +2900,92 @@ testGroupLinkRejectBlockedName = bob <## "group link: known group #team" bob <## "use #team to send messages" where - cfg = testCfg {allowedProfileName = Just (const False)} + cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Left GRRBlockedName)}} + +testGLinkManualAcceptMember :: HasCallStack => TestParams -> IO () +testGLinkManualAcceptMember = + testChatCfg3 cfg aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + cath ##> ("/c " <> gLink) + cath <## "connection request sent!" + alice <## "cath (Catherine): accepting request to join group #team..." + concurrentlyN_ + [ alice <## "#team: cath connected and pending approval, use /_accept member #1 3 to accept member", + do + cath <## "#team: joining the group..." + cath <## "#team: you joined the group, pending approval" + ] + + -- pending approval member doesn't see messages sent in group + alice #> "#team hi group" + bob <# "#team alice> hi group" + + bob #> "#team hey" + alice <# "#team bob> hey" + + -- pending approval member and host can send messages to each other + alice ##> "/_send #1 @3 text send me proofs" + alice <# "#team send me proofs" + cath <# "#team alice> send me proofs" + + cath ##> "/_send #1 @1 text proofs" + cath <# "#team proofs" + alice <# "#team cath> proofs" + + -- accept member + alice ##> "/_accept member #1 3 member" + concurrentlyN_ + [ alice <## "#team: cath joined the group", + cath + <### [ "#team: you joined the group", + WithTime "#team alice> hi group [>>]", + WithTime "#team bob> hey [>>]", + "#team: member bob (Bob) is connected" + ], + do + bob <## "#team: alice added cath (Catherine) to the group (connecting...)" + bob <## "#team: new member cath is connected" + ] + + alice #> "#team welcome cath" + [bob, cath] *<# "#team alice> welcome cath" + + bob #> "#team hi cath" + [alice, cath] *<# "#team bob> hi cath" + + cath #> "#team hi group" + [alice, bob] *<# "#team cath> hi group" + where + cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPending, GRObserver))}} + +testGLinkDeletePendingMember :: HasCallStack => TestParams -> IO () +testGLinkDeletePendingMember = + testChatCfg3 cfg aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + cath ##> ("/c " <> gLink) + cath <## "connection request sent!" + alice <## "cath (Catherine): accepting request to join group #team..." + concurrentlyN_ + [ alice <## "#team: cath connected and pending approval, use /_accept member #1 3 to accept member", + do + cath <## "#team: joining the group..." + cath <## "#team: you joined the group, pending approval" + ] + + alice ##> "/rm team cath" + alice <## "#team: you removed cath from the group" + cath <## "#team: alice removed you from the group" + cath <## "use /d #team to delete the group" + where + cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPending, GRObserver))}} testPlanGroupLinkKnown :: HasCallStack => TestParams -> IO () testPlanGroupLinkKnown = @@ -6457,3 +6548,37 @@ testUpdatedMentionNames = do mentionedMember name_ = CIMention {memberId = MemberId "abcd", memberRef = ciMentionMember <$> name_} where ciMentionMember name = CIMentionMember {groupMemberId = 1, displayName = name, localAlias = Nothing, memberRole = GRMember} + +testGroupDirectMessages :: HasCallStack => TestParams -> IO () +testGroupDirectMessages = + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + createGroup3 "team" alice bob cath + + alice #> "#team 1" + [bob, cath] *<# "#team alice> 1" + + bob #> "#team 2" + [alice, cath] *<# "#team bob> 2" + + void $ withCCTransaction alice $ \db -> + DB.execute_ db "UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 2" + + alice ##> "/_send #1 @2 text 3" + alice <# "#team 3" + bob <# "#team alice> 3" + + void $ withCCTransaction bob $ \db -> + DB.execute_ db "UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 1" + + bob ##> "/_send #1 @1 text 4" + bob <# "#team 4" + alice <# "#team bob> 4" + + -- GSMemPendingApproval members don't receive messages sent to group. + -- Though in test we got here synthetically, in reality this status + -- means they are not yet part of group (not memberCurrent). + alice #> "#team 5" + cath <# "#team alice> 5" + + bob #> "#team 6" + cath <# "#team bob> 6" diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 6c429e2a56..807fa4a0cb 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -125,7 +125,9 @@ skipComparisonForDownMigrations = -- indexes move down to the end of the file "20241125_indexes", -- indexes move down to the end of the file - "20250130_indexes" + "20250130_indexes", + -- index moves down to the end of the file + "20250227_member_acceptance" ] getSchema :: FilePath -> FilePath -> IO String From 8b030075d77c5d27d40eb2774d116895b7e887fc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 3 Mar 2025 19:58:00 +0000 Subject: [PATCH 21/24] core: update query plans --- .../SQLite/Migrations/chat_query_plans.txt | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 01f4cf3f68..88836035e5 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -2950,15 +2950,6 @@ Query: Plan: SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) -Query: - SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role - FROM user_contact_links - WHERE user_id = ? - AND user_contact_link_id = ? - -Plan: -SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) - Query: SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, @@ -4294,6 +4285,14 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_members + SET member_status = ?, member_role = ?, updated_at = ? + WHERE user_id = ? AND group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_members SET member_status = ?, updated_at = ? @@ -4611,6 +4610,14 @@ SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) +Query: + SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role + FROM user_contact_links + WHERE user_id = ? + AND user_contact_link_id = ? +Plan: +SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT f.file_id, f.ci_file_status, f.file_path FROM chat_items i @@ -5781,6 +5788,14 @@ Query: UPDATE group_members SET member_role = ? WHERE user_id = ? AND group_memb Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 1 +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 2 +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) From 1ddf7a62adbbccf05d0df865dca33db542543f2b Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 4 Mar 2025 03:53:08 +0700 Subject: [PATCH 22/24] ios: fix running in simulator (#5704) --- apps/ios/SimpleXChat/dummy.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/ios/SimpleXChat/dummy.m b/apps/ios/SimpleXChat/dummy.m index 64fbc32dd3..d26e108520 100644 --- a/apps/ios/SimpleXChat/dummy.m +++ b/apps/ios/SimpleXChat/dummy.m @@ -21,4 +21,13 @@ DIR *opendir$INODE64(const char *name) { return opendir(name); } +int readdir$INODE64(DIR *restrict dirp, struct dirent *restrict entry, + struct dirent **restrict result) { + return readdir_r(dirp, entry, result); +} + +DIR *fdopendir$INODE64(const char *name) { + return opendir(name); +} + #endif From 2788a1dbb36e066a76babf1b9a0ae63d1fa2d336 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 4 Mar 2025 03:53:23 +0700 Subject: [PATCH 23/24] ios: fix showing first unread (#5703) --- apps/ios/Shared/Views/Chat/EndlessScrollView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ios/Shared/Views/Chat/EndlessScrollView.swift b/apps/ios/Shared/Views/Chat/EndlessScrollView.swift index 3d4fb381a6..f04987e5f8 100644 --- a/apps/ios/Shared/Views/Chat/EndlessScrollView.swift +++ b/apps/ios/Shared/Views/Chat/EndlessScrollView.swift @@ -476,7 +476,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu if index >= listState.items.count || listState.isScrolling || listState.isAnimatedScrolling { return } - if bounds.height == 0 { + if bounds.height == 0 || contentSize.height == 0 { scrollToItemIndexDelayed = index return } From 0a97218440c1d8fc6174c0f8a27ee801e11eefae Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 3 Mar 2025 20:57:25 +0000 Subject: [PATCH 24/24] core: 6.3.0.7 (simplexmq 6.3.0.7) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- src/Simplex/Chat/Remote.hs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cabal.project b/cabal.project index aa96704e62..477ba9c601 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: f9d7b1eebc7e825423ee0d3b995a69c4006ac99c + tag: 9fece9ce3df24d1b006d98e44a6b4e654861428b source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 20b0b781d7..1a9de205f3 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."f9d7b1eebc7e825423ee0d3b995a69c4006ac99c" = "1yi4igwrgj5n0ljh06fhzzf2yprrwx9nrzp56s305z9g7mgx7gqm"; + "https://github.com/simplex-chat/simplexmq.git"."9fece9ce3df24d1b006d98e44a6b4e654861428b" = "0vf4kkj2jc4bg3bc9vsl0gv2z0l7z48626sqlk5wh7il6k14pn17"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index f987162b78..950b689234 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.3.0.6 +version: 6.3.0.7 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 5a6ef3b847..3e64b2bcea 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -75,11 +75,11 @@ remoteFilesFolder = "simplex_v1_files" -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 3, 0, 4] +minRemoteCtrlVersion = AppVersion [6, 3, 0, 7] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 3, 0, 4] +minRemoteHostVersion = AppVersion [6, 3, 0, 7] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version