diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift
index 69587c0152..24a52b4b60 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift
@@ -42,7 +42,7 @@ struct GroupProfileView: View {
Section {
HStack {
- TextField("Group display name", text: $groupProfile.displayName)
+ TextField(groupInfo.useRelays ? "Channel display name" : "Group display name", text: $groupProfile.displayName)
.focused($focusDisplayName)
if !validNewProfileName {
Button {
@@ -54,7 +54,7 @@ struct GroupProfileView: View {
}
let fullName = groupInfo.groupProfile.fullName
if fullName != "" && fullName != groupProfile.displayName {
- TextField("Group full name (optional)", text: $groupProfile.fullName)
+ TextField(groupInfo.useRelays ? "Channel full name (optional)" : "Group full name (optional)", text: $groupProfile.fullName)
}
HStack {
TextField("Short description", text: $shortDescr)
@@ -67,7 +67,7 @@ struct GroupProfileView: View {
}
}
} footer: {
- Text("Group profile is stored on members' devices, not on the servers.")
+ Text(groupInfo.useRelays ? "Channel profile is stored on subscribers' devices and on the chat relays." : "Group profile is stored on members' devices, not on the servers.")
}
Section {
@@ -80,11 +80,11 @@ struct GroupProfileView: View {
currentProfileHash == groupProfile.hashValue &&
(groupInfo.groupProfile.shortDescr ?? "") == shortDescr.trimmingCharacters(in: .whitespaces)
)
- Button("Save group profile", action: saveProfile)
+ Button(groupInfo.useRelays ? "Save channel profile" : "Save group profile", action: saveProfile)
.disabled(!canUpdateProfile)
}
}
- .confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) {
+ .confirmationDialog(groupInfo.useRelays ? "Channel image" : "Group image", isPresented: $showChooseSource, titleVisibility: .visible) {
Button("Take picture") {
showTakePhoto = true
}
@@ -130,9 +130,15 @@ struct GroupProfileView: View {
.onDisappear {
if canUpdateProfile {
showAlert(
- title: NSLocalizedString("Save group profile?", comment: "alert title"),
- message: NSLocalizedString("Group profile was changed. If you save it, the updated profile will be sent to group members.", comment: "alert message"),
- buttonTitle: NSLocalizedString("Save (and notify members)", comment: "alert button"),
+ title: groupInfo.useRelays
+ ? NSLocalizedString("Save channel profile?", comment: "alert title")
+ : NSLocalizedString("Save group profile?", comment: "alert title"),
+ message: groupInfo.useRelays
+ ? NSLocalizedString("Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers.", comment: "alert message")
+ : NSLocalizedString("Group profile was changed. If you save it, the updated profile will be sent to group members.", comment: "alert message"),
+ buttonTitle: groupInfo.useRelays
+ ? NSLocalizedString("Save (and notify subscribers)", comment: "alert button")
+ : NSLocalizedString("Save (and notify members)", comment: "alert button"),
buttonAction: saveProfile,
cancelButton: true
)
@@ -142,14 +148,14 @@ struct GroupProfileView: View {
switch a {
case let .saveError(err):
return Alert(
- title: Text("Error saving group profile"),
+ title: Text(groupInfo.useRelays ? "Error saving channel profile" : "Error saving group profile"),
message: Text(err)
)
case let .invalidName(name):
return createInvalidNameAlert(name, $groupProfile.displayName)
}
}
- .navigationBarTitle("Group profile")
+ .navigationBarTitle(groupInfo.useRelays ? "Channel profile" : "Group profile")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(focusDisplayName ? .inline : .large)
}
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 cb42ee2aba..661b7e767f 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
@@ -2217,18 +2217,19 @@ object ChatController {
return emptyList()
}
- suspend fun apiUpdateGroup(rh: Long?, groupId: Long, groupProfile: GroupProfile): GroupInfo? {
+ suspend fun apiUpdateGroup(rh: Long?, groupId: Long, groupProfile: GroupProfile, isChannel: Boolean): GroupInfo? {
val r = sendCmd(rh, CC.ApiUpdateGroupProfile(groupId, groupProfile))
+ val errorTitle = if (isChannel) MR.strings.error_saving_channel_profile else MR.strings.error_saving_group_profile
return when {
r is API.Result && r.res is CR.GroupUpdated -> r.res.toGroup
r is API.Error -> {
- AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_group_profile), "$r.err")
+ AlertManager.shared.showAlertMsg(generalGetString(errorTitle), "$r.err")
null
}
else -> {
Log.e(TAG, "apiUpdateGroup bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(
- generalGetString(MR.strings.error_saving_group_profile),
+ generalGetString(errorTitle),
"${r.responseType}: ${r.details}"
)
null
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt
index b8db5969a1..ddf0456822 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt
@@ -43,7 +43,7 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () ->
fun savePrefs(afterSave: () -> Unit = {}) {
withBGApi {
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
- val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp)
+ val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp, gInfo.useRelays)
if (g != null) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateGroup(rhId, g)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt
index f15f70673a..d144065399 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt
@@ -32,10 +32,11 @@ import java.net.URI
fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) {
GroupProfileLayout(
close = close,
+ groupInfo = groupInfo,
groupProfile = groupInfo.groupProfile,
saveProfile = { p ->
withBGApi {
- val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p)
+ val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p, groupInfo.useRelays)
if (gInfo != null) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateGroup(rhId, gInfo)
@@ -50,9 +51,11 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
@Composable
fun GroupProfileLayout(
close: () -> Unit,
+ groupInfo: GroupInfo,
groupProfile: GroupProfile,
saveProfile: (GroupProfile) -> Unit,
) {
+ val isChannel = groupInfo.useRelays
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val displayName = rememberSaveable { mutableStateOf(groupProfile.displayName) }
val fullName = rememberSaveable { mutableStateOf(groupProfile.fullName) }
@@ -71,7 +74,7 @@ fun GroupProfileLayout(
if (dataUnchanged || !canUpdateProfile(displayName.value, shortDescr.value, groupProfile)) {
close()
} else {
- showUnsavedChangesAlert({
+ showUnsavedChangesAlert(isChannel, {
saveProfile(
groupProfile.copy(
displayName = displayName.value.trim(),
@@ -103,7 +106,11 @@ fun GroupProfileLayout(
Modifier.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING)
) {
- ReadableText(MR.strings.group_profile_is_stored_on_members_devices, TextAlign.Center)
+ ReadableText(
+ if (isChannel) MR.strings.channel_profile_is_stored_on_subscribers_devices
+ else MR.strings.group_profile_is_stored_on_members_devices,
+ TextAlign.Center
+ )
Box(
Modifier
.fillMaxWidth()
@@ -122,7 +129,7 @@ fun GroupProfileLayout(
}
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
- stringResource(MR.strings.group_display_name_field),
+ stringResource(if (isChannel) MR.strings.channel_display_name_field else MR.strings.group_display_name_field),
fontSize = 16.sp
)
if (!isValidNewProfileName(displayName.value, groupProfile)) {
@@ -136,7 +143,7 @@ fun GroupProfileLayout(
if (groupProfile.fullName.trim().isNotEmpty() && groupProfile.fullName.trim() != groupProfile.displayName.trim()) {
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
- stringResource(MR.strings.group_full_name_field),
+ stringResource(if (isChannel) MR.strings.channel_full_name_field else MR.strings.group_full_name_field),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
@@ -164,9 +171,10 @@ fun GroupProfileLayout(
Spacer(Modifier.height(DEFAULT_PADDING))
val enabled = !dataUnchanged && canUpdateProfile(displayName.value, shortDescr.value, groupProfile)
+ val saveProfileLabel = if (isChannel) MR.strings.save_channel_profile else MR.strings.save_group_profile
if (enabled) {
Text(
- stringResource(MR.strings.save_group_profile),
+ stringResource(saveProfileLabel),
modifier = Modifier.clickable {
saveProfile(
groupProfile.copy(
@@ -181,7 +189,7 @@ fun GroupProfileLayout(
)
} else {
Text(
- stringResource(MR.strings.save_group_profile),
+ stringResource(saveProfileLabel),
color = MaterialTheme.colors.secondary
)
}
@@ -204,10 +212,10 @@ private fun canUpdateProfile(displayName: String, shortDescr: String, groupProfi
private fun isValidNewProfileName(displayName: String, groupProfile: GroupProfile): Boolean =
displayName == groupProfile.displayName || isValidDisplayName(displayName.trim())
-private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
+private fun showUnsavedChangesAlert(isChannel: Boolean, save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.save_preferences_question),
- confirmText = generalGetString(MR.strings.save_and_notify_group_members),
+ confirmText = generalGetString(if (isChannel) MR.strings.save_and_notify_channel_subscribers else MR.strings.save_and_notify_group_members),
dismissText = generalGetString(MR.strings.exit_without_saving),
onConfirm = save,
onDismiss = revert,
@@ -224,6 +232,7 @@ fun PreviewGroupProfileLayout() {
SimpleXTheme {
GroupProfileLayout(
close = {},
+ groupInfo = GroupInfo.sampleData,
groupProfile = GroupProfile.sampleData,
saveProfile = { _ -> }
)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt
index 48171bfeb7..7c9db58316 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt
@@ -34,7 +34,7 @@ fun MemberAdmissionView(m: ChatModel, rhId: Long?, chatId: String, close: () ->
fun saveAdmission(afterSave: () -> Unit = {}) {
withBGApi {
val gp = gInfo.groupProfile.copy(memberAdmission = admission)
- val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp)
+ val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp, gInfo.useRelays)
if (g != null) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateGroup(rhId, g)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt
index 1e99c7f527..927e9940b5 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt
@@ -45,7 +45,7 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
welcome = null
}
val groupProfileUpdated = gInfo.groupProfile.copy(description = welcome)
- val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated)
+ val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated, gInfo.useRelays)
if (res != null) {
gInfo = res
withContext(Dispatchers.Main) {
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 ac9f9b2fc8..f2872f3f0a 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -1171,6 +1171,7 @@
Save and notify contact
Save and notify contacts
Save and notify group members
+ Save and notify channel subscribers
Exit without saving
@@ -1999,6 +2000,7 @@
Fully decentralized – visible only to members.
Enter group name:
Group full name:
+ Channel full name:
Short description:
Description too large
Your chat profile will be sent to group members
@@ -2007,8 +2009,11 @@
Group profile is stored on members\' devices, not on the servers.
+ Channel profile is stored on subscribers\' devices and on the chat relays.
Save group profile
+ Save channel profile
Error saving group profile
+ Error saving channel profile
Preset servers
diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json
index a135b286c2..8d05eb460c 100644
--- a/packages/simplex-chat-client/types/typescript/package.json
+++ b/packages/simplex-chat-client/types/typescript/package.json
@@ -1,6 +1,6 @@
{
"name": "@simplex-chat/types",
- "version": "0.3.0",
+ "version": "0.4.0",
"description": "TypeScript types for SimpleX Chat bot libraries",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json
index 498d502edd..657c6e05f2 100644
--- a/packages/simplex-chat-nodejs/package.json
+++ b/packages/simplex-chat-nodejs/package.json
@@ -24,7 +24,7 @@
"docs": "typedoc"
},
"dependencies": {
- "@simplex-chat/types": "^0.3.0",
+ "@simplex-chat/types": "^0.4.0",
"extract-zip": "^2.0.1",
"fast-deep-equal": "^3.1.3",
"node-addon-api": "^8.5.0"
diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts
index c3e85b3915..8bc56db41c 100644
--- a/packages/simplex-chat-nodejs/src/api.ts
+++ b/packages/simplex-chat-nodejs/src/api.ts
@@ -813,7 +813,7 @@ export class ChatApi {
* Network usage: no.
*/
async apiCreateActiveUser(profile?: T.Profile): Promise {
- const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false}}))
+ const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false, userChatRelay: false}}))
if (r.type === "activeUser") return r.user
throw new ChatCommandError("unexpected response", r)
}
@@ -872,4 +872,34 @@ export class ChatApi {
const r = await this.sendChatCmd(CC.APISetContactPrefs.cmdString({contactId, preferences}))
if (r.type !== "contactPrefsUpdated") throw new ChatCommandError("error setting contact prefs", r)
}
+
+ /**
+ * Create a direct message contact with a group member.
+ * Returns the created contact.
+ * Network usage: interactive.
+ */
+ async apiCreateMemberContact(groupId: number, groupMemberId: number): Promise {
+ const r: any = await this.sendChatCmd(`/_create member contact #${groupId} ${groupMemberId}`)
+ if (r.type === "newMemberContact") return r.contact
+ throw new ChatCommandError("error creating member contact", r)
+ }
+
+ /**
+ * Send a direct message invitation to a group member contact.
+ * The contact must have been created with {@link apiCreateMemberContact}.
+ * Network usage: interactive.
+ */
+ async apiSendMemberContactInvitation(contactId: number, message?: T.MsgContent | string): Promise {
+ let cmd = `/_invite member contact @${contactId}`
+ if (message !== undefined) {
+ if (typeof message === "string") {
+ cmd += ` text ${message}`
+ } else {
+ cmd += ` json ${JSON.stringify(message)}`
+ }
+ }
+ const r: any = await this.sendChatCmd(cmd)
+ if (r.type === "newMemberContactSentInv") return r.contact
+ throw new ChatCommandError("error sending member contact invitation", r)
+ }
}
diff --git a/packages/simplex-chat-nodejs/tests/api.test.ts b/packages/simplex-chat-nodejs/tests/api.test.ts
index 52153ecfed..7bc1a89b86 100644
--- a/packages/simplex-chat-nodejs/tests/api.test.ts
+++ b/packages/simplex-chat-nodejs/tests/api.test.ts
@@ -64,4 +64,89 @@ describe("API tests (use preset servers)", () => {
expect(servers[0] !== servers[1]).toBe(true)
expect(eventCount > 0).toBe(true)
}, 30000)
+
+ it("should create member contact and send invitation", async () => {
+ // create 3 users and start chat controllers
+ const alice = await api.ChatApi.init(alicePath)
+ const bob = await api.ChatApi.init(bobPath)
+ const carolPath = path.join(tmpDir, "carol")
+ const carol = await api.ChatApi.init(carolPath)
+ const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""})
+ await bob.apiCreateActiveUser({displayName: "bob", fullName: ""})
+ await carol.apiCreateActiveUser({displayName: "carol", fullName: ""})
+ await alice.startChat()
+ await bob.startChat()
+ await carol.startChat()
+ // connect alice <-> bob
+ const aliceLink1 = await alice.apiCreateLink(aliceUser.userId)
+ await expect(bob.apiConnectActiveUser(aliceLink1)).resolves.toBe(api.ConnReqType.Invitation)
+ const [bobContact] = await Promise.all([
+ (await alice.wait("contactConnected")).contact,
+ (await bob.wait("contactConnected")).contact
+ ])
+ // connect alice <-> carol
+ const aliceLink2 = await alice.apiCreateLink(aliceUser.userId)
+ await expect(carol.apiConnectActiveUser(aliceLink2)).resolves.toBe(api.ConnReqType.Invitation)
+ const [carolContact] = await Promise.all([
+ (await alice.wait("contactConnected")).contact,
+ (await carol.wait("contactConnected")).contact
+ ])
+ // create group with direct messages enabled
+ const group = await alice.apiNewGroup(aliceUser.userId, {
+ displayName: "test-group",
+ fullName: "",
+ groupPreferences: {
+ directMessages: {enable: T.GroupFeatureEnabled.On},
+ },
+ })
+ const groupId = group.groupId
+ // add bob to the group
+ const bobInvP = bob.wait("receivedGroupInvitation", 15000)
+ await alice.apiAddMember(groupId, bobContact.contactId, T.GroupMemberRole.Member)
+ const bobInvEvt = await bobInvP
+ expect(bobInvEvt).toBeDefined()
+ const aliceBobConnP = alice.wait("connectedToGroupMember", 15000)
+ const bobAliceConnP = bob.wait("connectedToGroupMember", 15000)
+ await bob.apiJoinGroup(bobInvEvt!.groupInfo.groupId)
+ await Promise.all([aliceBobConnP, bobAliceConnP])
+ // add carol to the group
+ const carolInvP = carol.wait("receivedGroupInvitation", 30000)
+ await alice.apiAddMember(groupId, carolContact.contactId, T.GroupMemberRole.Member)
+ const carolInvEvt = await carolInvP
+ expect(carolInvEvt).toBeDefined()
+ // wait for carol to connect to both alice and bob (and vice versa)
+ const bobCarolConnP = bob.wait("connectedToGroupMember",
+ (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "carol", 30000)
+ const carolAliceConnP = carol.wait("connectedToGroupMember",
+ (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "alice", 30000)
+ const carolBobConnP = carol.wait("connectedToGroupMember",
+ (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "bob", 30000)
+ const aliceCarolConnP = alice.wait("connectedToGroupMember",
+ (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "carol", 30000)
+ await carol.apiJoinGroup(carolInvEvt!.groupInfo.groupId)
+ await Promise.all([bobCarolConnP, carolAliceConnP, carolBobConnP, aliceCarolConnP])
+ // find carol's memberId from bob's perspective
+ const members = await bob.apiListMembers(groupId)
+ const carolMember = members.find(m => m.memberProfile.displayName === "carol")
+ expect(carolMember).toBeDefined()
+ // test apiCreateMemberContact
+ const dmContact = await bob.apiCreateMemberContact(groupId, carolMember!.groupMemberId)
+ expect(dmContact).toBeDefined()
+ expect(dmContact.contactId).toBeDefined()
+ // test apiSendMemberContactInvitation
+ const carolDmP = carol.wait("newMemberContactReceivedInv" as CEvt.Tag, 30000)
+ const invContact = await bob.apiSendMemberContactInvitation(dmContact.contactId, "hello from bob")
+ expect(invContact).toBeDefined()
+ // carol should receive the member contact invitation
+ const carolDmEvt = await carolDmP
+ expect(carolDmEvt).toBeDefined()
+ expect((carolDmEvt as any).contact).toBeDefined()
+ // cleanup
+ await alice.stopChat()
+ await bob.stopChat()
+ await carol.stopChat()
+ await alice.close()
+ await bob.close()
+ await carol.close()
+ }, 90000)
})
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index da1a9a7dcc..ddb1ae29fe 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.5.0.12
+version: 6.5.0.14
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat