Merge pull request #2022 from simplex-chat/ep/v454
v4.5.4: add support for observer role
This commit is contained in:
commit
9127b1bbc6
@ -486,6 +486,24 @@ data class Chat (
|
|||||||
val chatItems: List<ChatItem>,
|
val chatItems: List<ChatItem>,
|
||||||
val chatStats: ChatStats = ChatStats(),
|
val chatStats: ChatStats = ChatStats(),
|
||||||
) {
|
) {
|
||||||
|
val userCanSend: Boolean
|
||||||
|
get() = when (chatInfo) {
|
||||||
|
is ChatInfo.Direct -> true
|
||||||
|
is ChatInfo.Group -> {
|
||||||
|
val m = chatInfo.groupInfo.membership
|
||||||
|
m.memberActive && m.memberRole >= GroupMemberRole.Member
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
val userIsObserver: Boolean get() = when(chatInfo) {
|
||||||
|
is ChatInfo.Group -> {
|
||||||
|
val m = chatInfo.groupInfo.membership
|
||||||
|
m.memberActive && m.memberRole == GroupMemberRole.Observer
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
val id: String get() = chatInfo.id
|
val id: String get() = chatInfo.id
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -932,7 +950,7 @@ data class GroupMember (
|
|||||||
fun canChangeRoleTo(groupInfo: GroupInfo): List<GroupMemberRole>? =
|
fun canChangeRoleTo(groupInfo: GroupInfo): List<GroupMemberRole>? =
|
||||||
if (!canBeRemoved(groupInfo)) null
|
if (!canBeRemoved(groupInfo)) null
|
||||||
else groupInfo.membership.memberRole.let { userRole ->
|
else groupInfo.membership.memberRole.let { userRole ->
|
||||||
GroupMemberRole.values().filter { it <= userRole }
|
GroupMemberRole.values().filter { it <= userRole && it != GroupMemberRole.Observer }
|
||||||
}
|
}
|
||||||
|
|
||||||
val memberIncognito = memberProfile.profileId != memberContactProfileId
|
val memberIncognito = memberProfile.profileId != memberContactProfileId
|
||||||
@ -963,11 +981,13 @@ class GroupMemberRef(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class GroupMemberRole(val memberRole: String) {
|
enum class GroupMemberRole(val memberRole: String) {
|
||||||
@SerialName("member") Member("member"), // order matters in comparisons
|
@SerialName("observer") Observer("observer"), // order matters in comparisons
|
||||||
|
@SerialName("member") Member("member"),
|
||||||
@SerialName("admin") Admin("admin"),
|
@SerialName("admin") Admin("admin"),
|
||||||
@SerialName("owner") Owner("owner");
|
@SerialName("owner") Owner("owner");
|
||||||
|
|
||||||
val text: String get() = when (this) {
|
val text: String get() = when (this) {
|
||||||
|
Observer -> generalGetString(R.string.group_member_role_observer)
|
||||||
Member -> generalGetString(R.string.group_member_role_member)
|
Member -> generalGetString(R.string.group_member_role_member)
|
||||||
Admin -> generalGetString(R.string.group_member_role_admin)
|
Admin -> generalGetString(R.string.group_member_role_admin)
|
||||||
Owner -> generalGetString(R.string.group_member_role_owner)
|
Owner -> generalGetString(R.string.group_member_role_owner)
|
||||||
|
@ -549,7 +549,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
|||||||
|
|
||||||
suspend fun testSMPServer(smpServer: String): SMPTestFailure? {
|
suspend fun testSMPServer(smpServer: String): SMPTestFailure? {
|
||||||
val userId = chatModel.currentUser.value?.userId ?: run { throw Exception("testSMPServer: no current user") }
|
val userId = chatModel.currentUser.value?.userId ?: run { throw Exception("testSMPServer: no current user") }
|
||||||
val r = sendCmd(CC.TestSMPServer(userId, smpServer))
|
val r = sendCmd(CC.APITestSMPServer(userId, smpServer))
|
||||||
return when (r) {
|
return when (r) {
|
||||||
is CR.SmpTestResult -> r.smpTestFailure
|
is CR.SmpTestResult -> r.smpTestFailure
|
||||||
else -> {
|
else -> {
|
||||||
@ -1060,9 +1060,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiCreateGroupLink(groupId: Long): String? {
|
suspend fun apiCreateGroupLink(groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<String, GroupMemberRole>? {
|
||||||
return when (val r = sendCmd(CC.APICreateGroupLink(groupId))) {
|
return when (val r = sendCmd(CC.APICreateGroupLink(groupId, memberRole))) {
|
||||||
is CR.GroupLinkCreated -> r.connReqContact
|
is CR.GroupLinkCreated -> r.connReqContact to r.memberRole
|
||||||
else -> {
|
else -> {
|
||||||
if (!(networkErrorAlert(r))) {
|
if (!(networkErrorAlert(r))) {
|
||||||
apiErrorAlert("apiCreateGroupLink", generalGetString(R.string.error_creating_link_for_group), r)
|
apiErrorAlert("apiCreateGroupLink", generalGetString(R.string.error_creating_link_for_group), r)
|
||||||
@ -1072,6 +1072,18 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun apiGroupLinkMemberRole(groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<String, GroupMemberRole>? {
|
||||||
|
return when (val r = sendCmd(CC.APIGroupLinkMemberRole(groupId, memberRole))) {
|
||||||
|
is CR.GroupLink -> r.connReqContact to r.memberRole
|
||||||
|
else -> {
|
||||||
|
if (!(networkErrorAlert(r))) {
|
||||||
|
apiErrorAlert("apiGroupLinkMemberRole", generalGetString(R.string.error_updating_link_for_group), r)
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiDeleteGroupLink(groupId: Long): Boolean {
|
suspend fun apiDeleteGroupLink(groupId: Long): Boolean {
|
||||||
return when (val r = sendCmd(CC.APIDeleteGroupLink(groupId))) {
|
return when (val r = sendCmd(CC.APIDeleteGroupLink(groupId))) {
|
||||||
is CR.GroupLinkDeleted -> true
|
is CR.GroupLinkDeleted -> true
|
||||||
@ -1084,9 +1096,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiGetGroupLink(groupId: Long): String? {
|
suspend fun apiGetGroupLink(groupId: Long): Pair<String, GroupMemberRole>? {
|
||||||
return when (val r = sendCmd(CC.APIGetGroupLink(groupId))) {
|
return when (val r = sendCmd(CC.APIGetGroupLink(groupId))) {
|
||||||
is CR.GroupLink -> r.connReqContact
|
is CR.GroupLink -> r.connReqContact to r.memberRole
|
||||||
else -> {
|
else -> {
|
||||||
Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}")
|
Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}")
|
||||||
null
|
null
|
||||||
@ -1343,6 +1355,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
|||||||
if (active(r.user)) {
|
if (active(r.user)) {
|
||||||
chatModel.updateGroup(r.toGroup)
|
chatModel.updateGroup(r.toGroup)
|
||||||
}
|
}
|
||||||
|
is CR.MemberRole ->
|
||||||
|
if (active(r.user)) {
|
||||||
|
chatModel.updateGroup(r.groupInfo)
|
||||||
|
}
|
||||||
is CR.RcvFileStart ->
|
is CR.RcvFileStart ->
|
||||||
chatItemSimpleUpdate(r.user, r.chatItem)
|
chatItemSimpleUpdate(r.user, r.chatItem)
|
||||||
is CR.RcvFileComplete ->
|
is CR.RcvFileComplete ->
|
||||||
@ -1752,12 +1768,13 @@ sealed class CC {
|
|||||||
class ApiLeaveGroup(val groupId: Long): CC()
|
class ApiLeaveGroup(val groupId: Long): CC()
|
||||||
class ApiListMembers(val groupId: Long): CC()
|
class ApiListMembers(val groupId: Long): CC()
|
||||||
class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC()
|
class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC()
|
||||||
class APICreateGroupLink(val groupId: Long): CC()
|
class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole): CC()
|
||||||
|
class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC()
|
||||||
class APIDeleteGroupLink(val groupId: Long): CC()
|
class APIDeleteGroupLink(val groupId: Long): CC()
|
||||||
class APIGetGroupLink(val groupId: Long): CC()
|
class APIGetGroupLink(val groupId: Long): CC()
|
||||||
class APIGetUserSMPServers(val userId: Long): CC()
|
class APIGetUserSMPServers(val userId: Long): CC()
|
||||||
class APISetUserSMPServers(val userId: Long, val smpServers: List<ServerCfg>): CC()
|
class APISetUserSMPServers(val userId: Long, val smpServers: List<ServerCfg>): CC()
|
||||||
class TestSMPServer(val userId: Long, val smpServer: String): CC()
|
class APITestSMPServer(val userId: Long, val smpServer: String): CC()
|
||||||
class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC()
|
class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC()
|
||||||
class APIGetChatItemTTL(val userId: Long): CC()
|
class APIGetChatItemTTL(val userId: Long): CC()
|
||||||
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
|
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
|
||||||
@ -1827,12 +1844,13 @@ sealed class CC {
|
|||||||
is ApiLeaveGroup -> "/_leave #$groupId"
|
is ApiLeaveGroup -> "/_leave #$groupId"
|
||||||
is ApiListMembers -> "/_members #$groupId"
|
is ApiListMembers -> "/_members #$groupId"
|
||||||
is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}"
|
is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}"
|
||||||
is APICreateGroupLink -> "/_create link #$groupId"
|
is APICreateGroupLink -> "/_create link #$groupId ${memberRole.name.lowercase()}"
|
||||||
|
is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}"
|
||||||
is APIDeleteGroupLink -> "/_delete link #$groupId"
|
is APIDeleteGroupLink -> "/_delete link #$groupId"
|
||||||
is APIGetGroupLink -> "/_get link #$groupId"
|
is APIGetGroupLink -> "/_get link #$groupId"
|
||||||
is APIGetUserSMPServers -> "/_smp $userId"
|
is APIGetUserSMPServers -> "/_smp $userId"
|
||||||
is APISetUserSMPServers -> "/_smp $userId ${smpServersStr(smpServers)}"
|
is APISetUserSMPServers -> "/_smp $userId ${smpServersStr(smpServers)}"
|
||||||
is TestSMPServer -> "/smp test $userId $smpServer"
|
is APITestSMPServer -> "/_smp test $userId $smpServer"
|
||||||
is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}"
|
is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}"
|
||||||
is APIGetChatItemTTL -> "/_ttl $userId"
|
is APIGetChatItemTTL -> "/_ttl $userId"
|
||||||
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
|
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
|
||||||
@ -1904,11 +1922,12 @@ sealed class CC {
|
|||||||
is ApiListMembers -> "apiListMembers"
|
is ApiListMembers -> "apiListMembers"
|
||||||
is ApiUpdateGroupProfile -> "apiUpdateGroupProfile"
|
is ApiUpdateGroupProfile -> "apiUpdateGroupProfile"
|
||||||
is APICreateGroupLink -> "apiCreateGroupLink"
|
is APICreateGroupLink -> "apiCreateGroupLink"
|
||||||
|
is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole"
|
||||||
is APIDeleteGroupLink -> "apiDeleteGroupLink"
|
is APIDeleteGroupLink -> "apiDeleteGroupLink"
|
||||||
is APIGetGroupLink -> "apiGetGroupLink"
|
is APIGetGroupLink -> "apiGetGroupLink"
|
||||||
is APIGetUserSMPServers -> "apiGetUserSMPServers"
|
is APIGetUserSMPServers -> "apiGetUserSMPServers"
|
||||||
is APISetUserSMPServers -> "apiSetUserSMPServers"
|
is APISetUserSMPServers -> "apiSetUserSMPServers"
|
||||||
is TestSMPServer -> "testSMPServer"
|
is APITestSMPServer -> "testSMPServer"
|
||||||
is APISetChatItemTTL -> "apiSetChatItemTTL"
|
is APISetChatItemTTL -> "apiSetChatItemTTL"
|
||||||
is APIGetChatItemTTL -> "apiGetChatItemTTL"
|
is APIGetChatItemTTL -> "apiGetChatItemTTL"
|
||||||
is APISetNetworkConfig -> "/apiSetNetworkConfig"
|
is APISetNetworkConfig -> "/apiSetNetworkConfig"
|
||||||
@ -2925,8 +2944,8 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||||
@Serializable @SerialName("groupRemoved") class GroupRemoved(val user: User, val groupInfo: GroupInfo): CR() // unused
|
@Serializable @SerialName("groupRemoved") class GroupRemoved(val user: User, val groupInfo: GroupInfo): CR() // unused
|
||||||
@Serializable @SerialName("groupUpdated") class GroupUpdated(val user: User, val toGroup: GroupInfo): CR()
|
@Serializable @SerialName("groupUpdated") class GroupUpdated(val user: User, val toGroup: GroupInfo): CR()
|
||||||
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: User, val groupInfo: GroupInfo, val connReqContact: String): CR()
|
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: User, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR()
|
||||||
@Serializable @SerialName("groupLink") class GroupLink(val user: User, val groupInfo: GroupInfo, val connReqContact: String): CR()
|
@Serializable @SerialName("groupLink") class GroupLink(val user: User, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR()
|
||||||
@Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: User, val groupInfo: GroupInfo): CR()
|
@Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: User, val groupInfo: GroupInfo): CR()
|
||||||
// receiving file events
|
// receiving file events
|
||||||
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: User, val chatItem: AChatItem): CR()
|
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: User, val chatItem: AChatItem): CR()
|
||||||
@ -3129,8 +3148,8 @@ sealed class CR {
|
|||||||
is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
|
is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
|
||||||
is GroupRemoved -> withUser(user, json.encodeToString(groupInfo))
|
is GroupRemoved -> withUser(user, json.encodeToString(groupInfo))
|
||||||
is GroupUpdated -> withUser(user, json.encodeToString(toGroup))
|
is GroupUpdated -> withUser(user, json.encodeToString(toGroup))
|
||||||
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact")
|
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
|
||||||
is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact")
|
is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
|
||||||
is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo))
|
is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo))
|
||||||
is RcvFileAcceptedSndCancelled -> withUser(user, noDetails())
|
is RcvFileAcceptedSndCancelled -> withUser(user, noDetails())
|
||||||
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
|
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
|
||||||
|
@ -17,6 +17,7 @@ enum class DefaultTheme {
|
|||||||
val DEFAULT_PADDING = 16.dp
|
val DEFAULT_PADDING = 16.dp
|
||||||
val DEFAULT_SPACE_AFTER_ICON = 4.dp
|
val DEFAULT_SPACE_AFTER_ICON = 4.dp
|
||||||
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
|
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
|
||||||
|
val DEFAULT_BOTTOM_PADDING = 48.dp
|
||||||
|
|
||||||
val DarkColorPalette = darkColors(
|
val DarkColorPalette = darkColors(
|
||||||
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
|
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
|
||||||
|
@ -83,6 +83,8 @@ fun TerminalLayout(
|
|||||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||||
needToAllowVoiceToContact = false,
|
needToAllowVoiceToContact = false,
|
||||||
allowedVoiceByPrefs = false,
|
allowedVoiceByPrefs = false,
|
||||||
|
userIsObserver = false,
|
||||||
|
userCanSend = true,
|
||||||
allowVoiceToContact = {},
|
allowVoiceToContact = {},
|
||||||
sendMessage = sendCommand,
|
sendMessage = sendCommand,
|
||||||
sendLiveMessage = null,
|
sendLiveMessage = null,
|
||||||
|
@ -37,6 +37,7 @@ import androidx.webkit.WebViewClientCompat
|
|||||||
import chat.simplex.app.*
|
import chat.simplex.app.*
|
||||||
import chat.simplex.app.R
|
import chat.simplex.app.R
|
||||||
import chat.simplex.app.model.*
|
import chat.simplex.app.model.*
|
||||||
|
import chat.simplex.app.ui.theme.DEFAULT_BOTTOM_PADDING
|
||||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||||
import chat.simplex.app.views.helpers.ProfileImage
|
import chat.simplex.app.views.helpers.ProfileImage
|
||||||
import chat.simplex.app.views.helpers.withApi
|
import chat.simplex.app.views.helpers.withApi
|
||||||
@ -240,7 +241,7 @@ private fun ActiveCallOverlayLayout(
|
|||||||
CallInfoView(call, alignment = Alignment.CenterHorizontally)
|
CallInfoView(call, alignment = Alignment.CenterHorizontally)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||||
Box(Modifier.fillMaxWidth().padding(bottom = 48.dp), contentAlignment = Alignment.CenterStart) {
|
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
|
||||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
IconButton(onClick = dismiss) {
|
IconButton(onClick = dismiss) {
|
||||||
Icon(Icons.Filled.CallEnd, stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
|
Icon(Icons.Filled.CallEnd, stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
|
||||||
|
@ -152,9 +152,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
|||||||
}
|
}
|
||||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||||
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
|
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
|
||||||
var groupLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
|
val link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
|
||||||
|
var groupLink = link?.first
|
||||||
|
var groupLinkMemberRole = link?.second
|
||||||
ModalManager.shared.showModalCloseable(true) { close ->
|
ModalManager.shared.showModalCloseable(true) { close ->
|
||||||
GroupChatInfoView(chatModel, groupLink, { groupLink = it }, close)
|
GroupChatInfoView(chatModel, groupLink, groupLinkMemberRole, {
|
||||||
|
groupLink = it.first;
|
||||||
|
groupLinkMemberRole = it.second
|
||||||
|
}, close)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import android.util.Log
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
@ -32,6 +33,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import chat.simplex.app.*
|
import chat.simplex.app.*
|
||||||
@ -645,6 +647,9 @@ fun ComposeView(
|
|||||||
chatModel.sharedContent.value = null
|
chatModel.sharedContent.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val userCanSend = rememberUpdatedState(chat.userCanSend)
|
||||||
|
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
contextItemView()
|
contextItemView()
|
||||||
when {
|
when {
|
||||||
@ -656,11 +661,11 @@ fun ComposeView(
|
|||||||
modifier = Modifier.padding(end = 8.dp),
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
) {
|
) {
|
||||||
IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled) {
|
IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.AttachFile,
|
Icons.Filled.AttachFile,
|
||||||
contentDescription = stringResource(R.string.attach),
|
contentDescription = stringResource(R.string.attach),
|
||||||
tint = if (!composeState.value.attachmentDisabled) MaterialTheme.colors.primary else HighOrLowlight,
|
tint = if (!composeState.value.attachmentDisabled && userCanSend.value) MaterialTheme.colors.primary else HighOrLowlight,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(28.dp)
|
.size(28.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
@ -698,6 +703,13 @@ fun ComposeView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) {
|
||||||
|
if (!chat.userCanSend) {
|
||||||
|
clearCurrentDraft()
|
||||||
|
clearState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val activity = LocalContext.current as Activity
|
val activity = LocalContext.current as Activity
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
val orientation = activity.resources.configuration.orientation
|
val orientation = activity.resources.configuration.orientation
|
||||||
@ -733,6 +745,8 @@ fun ComposeView(
|
|||||||
needToAllowVoiceToContact,
|
needToAllowVoiceToContact,
|
||||||
allowedVoiceByPrefs,
|
allowedVoiceByPrefs,
|
||||||
allowVoiceToContact = ::allowVoiceToContact,
|
allowVoiceToContact = ::allowVoiceToContact,
|
||||||
|
userIsObserver = userIsObserver.value,
|
||||||
|
userCanSend = userCanSend.value,
|
||||||
sendMessage = {
|
sendMessage = {
|
||||||
sendMessage()
|
sendMessage()
|
||||||
resetLinkPreview()
|
resetLinkPreview()
|
||||||
|
@ -60,6 +60,8 @@ fun SendMsgView(
|
|||||||
liveMessageAlertShown: SharedPreference<Boolean>,
|
liveMessageAlertShown: SharedPreference<Boolean>,
|
||||||
needToAllowVoiceToContact: Boolean,
|
needToAllowVoiceToContact: Boolean,
|
||||||
allowedVoiceByPrefs: Boolean,
|
allowedVoiceByPrefs: Boolean,
|
||||||
|
userIsObserver: Boolean,
|
||||||
|
userCanSend: Boolean,
|
||||||
allowVoiceToContact: () -> Unit,
|
allowVoiceToContact: () -> Unit,
|
||||||
sendMessage: () -> Unit,
|
sendMessage: () -> Unit,
|
||||||
sendLiveMessage: (suspend () -> Unit)? = null,
|
sendLiveMessage: (suspend () -> Unit)? = null,
|
||||||
@ -74,10 +76,18 @@ fun SendMsgView(
|
|||||||
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
|
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
|
||||||
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
|
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
|
||||||
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
|
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
|
||||||
NativeKeyboard(composeState, textStyle, showDeleteTextButton, onMessageChange)
|
NativeKeyboard(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange)
|
||||||
// Disable clicks on text field
|
// Disable clicks on text field
|
||||||
if (cs.preview is ComposePreview.VoicePreview) {
|
if (cs.preview is ComposePreview.VoicePreview || !userCanSend) {
|
||||||
Box(Modifier.matchParentSize().clickable(enabled = false, onClick = { }))
|
Box(Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(R.string.observer_cant_send_message_title),
|
||||||
|
text = generalGetString(R.string.observer_cant_send_message_desc)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (showDeleteTextButton.value) {
|
if (showDeleteTextButton.value) {
|
||||||
DeleteTextButton(composeState)
|
DeleteTextButton(composeState)
|
||||||
@ -99,11 +109,11 @@ fun SendMsgView(
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
val stopRecOnNextClick = remember { mutableStateOf(false) }
|
val stopRecOnNextClick = remember { mutableStateOf(false) }
|
||||||
when {
|
when {
|
||||||
needToAllowVoiceToContact || !allowedVoiceByPrefs -> {
|
needToAllowVoiceToContact || !allowedVoiceByPrefs || !userCanSend -> {
|
||||||
DisallowedVoiceButton {
|
DisallowedVoiceButton(userCanSend) {
|
||||||
if (needToAllowVoiceToContact) {
|
if (needToAllowVoiceToContact) {
|
||||||
showNeedToAllowVoiceAlert(allowVoiceToContact)
|
showNeedToAllowVoiceAlert(allowVoiceToContact)
|
||||||
} else {
|
} else if (!allowedVoiceByPrefs) {
|
||||||
showDisabledVoiceAlert(isDirectChat)
|
showDisabledVoiceAlert(isDirectChat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,7 +128,7 @@ fun SendMsgView(
|
|||||||
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
||||||
&& cs.contextItem is ComposeContextItem.NoContextItem) {
|
&& cs.contextItem is ComposeContextItem.NoContextItem) {
|
||||||
Spacer(Modifier.width(10.dp))
|
Spacer(Modifier.width(10.dp))
|
||||||
StartLiveMessageButton {
|
StartLiveMessageButton(userCanSend) {
|
||||||
if (composeState.value.preview is ComposePreview.NoPreview) {
|
if (composeState.value.preview is ComposePreview.NoPreview) {
|
||||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||||
}
|
}
|
||||||
@ -173,6 +183,7 @@ private fun NativeKeyboard(
|
|||||||
composeState: MutableState<ComposeState>,
|
composeState: MutableState<ComposeState>,
|
||||||
textStyle: MutableState<TextStyle>,
|
textStyle: MutableState<TextStyle>,
|
||||||
showDeleteTextButton: MutableState<Boolean>,
|
showDeleteTextButton: MutableState<Boolean>,
|
||||||
|
userIsObserver: Boolean,
|
||||||
onMessageChange: (String) -> Unit
|
onMessageChange: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val cs = composeState.value
|
val cs = composeState.value
|
||||||
@ -253,13 +264,20 @@ private fun NativeKeyboard(
|
|||||||
showDeleteTextButton.value = it.lineCount >= 4
|
showDeleteTextButton.value = it.lineCount >= 4
|
||||||
}
|
}
|
||||||
if (composeState.value.preview is ComposePreview.VoicePreview) {
|
if (composeState.value.preview is ComposePreview.VoicePreview) {
|
||||||
|
ComposeOverlay(R.string.voice_message_send_text, textStyle, padding)
|
||||||
|
} else if (userIsObserver) {
|
||||||
|
ComposeOverlay(R.string.you_are_observer, textStyle, padding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ComposeOverlay(textId: Int, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
|
||||||
Text(
|
Text(
|
||||||
generalGetString(R.string.voice_message_send_text),
|
generalGetString(textId),
|
||||||
Modifier.padding(padding),
|
Modifier.padding(padding),
|
||||||
color = HighOrLowlight,
|
color = HighOrLowlight,
|
||||||
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
|
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -322,8 +340,8 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DisallowedVoiceButton(onClick: () -> Unit) {
|
private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) {
|
||||||
IconButton(onClick, Modifier.size(36.dp)) {
|
IconButton(onClick, Modifier.size(36.dp), enabled = enabled) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.KeyboardVoice,
|
Icons.Outlined.KeyboardVoice,
|
||||||
stringResource(R.string.icon_descr_record_voice_message),
|
stringResource(R.string.icon_descr_record_voice_message),
|
||||||
@ -454,13 +472,13 @@ private fun SendMsgButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StartLiveMessageButton(onClick: () -> Unit) {
|
private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.requiredSize(36.dp)
|
modifier = Modifier.requiredSize(36.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
enabled = true,
|
enabled = enabled,
|
||||||
role = Role.Button,
|
role = Role.Button,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = rememberRipple(bounded = false, radius = 24.dp)
|
indication = rememberRipple(bounded = false, radius = 24.dp)
|
||||||
@ -470,7 +488,7 @@ private fun StartLiveMessageButton(onClick: () -> Unit) {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Bolt,
|
Icons.Filled.Bolt,
|
||||||
stringResource(R.string.icon_descr_send_message),
|
stringResource(R.string.icon_descr_send_message),
|
||||||
tint = MaterialTheme.colors.primary,
|
tint = if (enabled) MaterialTheme.colors.primary else HighOrLowlight,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(36.dp)
|
.size(36.dp)
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
@ -571,6 +589,8 @@ fun PreviewSendMsgView() {
|
|||||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||||
needToAllowVoiceToContact = false,
|
needToAllowVoiceToContact = false,
|
||||||
allowedVoiceByPrefs = true,
|
allowedVoiceByPrefs = true,
|
||||||
|
userIsObserver = false,
|
||||||
|
userCanSend = true,
|
||||||
allowVoiceToContact = {},
|
allowVoiceToContact = {},
|
||||||
sendMessage = {},
|
sendMessage = {},
|
||||||
onMessageChange = { _ -> },
|
onMessageChange = { _ -> },
|
||||||
@ -599,6 +619,8 @@ fun PreviewSendMsgViewEditing() {
|
|||||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||||
needToAllowVoiceToContact = false,
|
needToAllowVoiceToContact = false,
|
||||||
allowedVoiceByPrefs = true,
|
allowedVoiceByPrefs = true,
|
||||||
|
userIsObserver = false,
|
||||||
|
userCanSend = true,
|
||||||
allowVoiceToContact = {},
|
allowVoiceToContact = {},
|
||||||
sendMessage = {},
|
sendMessage = {},
|
||||||
onMessageChange = { _ -> },
|
onMessageChange = { _ -> },
|
||||||
@ -627,6 +649,8 @@ fun PreviewSendMsgViewInProgress() {
|
|||||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||||
needToAllowVoiceToContact = false,
|
needToAllowVoiceToContact = false,
|
||||||
allowedVoiceByPrefs = true,
|
allowedVoiceByPrefs = true,
|
||||||
|
userIsObserver = false,
|
||||||
|
userCanSend = true,
|
||||||
allowVoiceToContact = {},
|
allowVoiceToContact = {},
|
||||||
sendMessage = {},
|
sendMessage = {},
|
||||||
onMessageChange = { _ -> },
|
onMessageChange = { _ -> },
|
||||||
|
@ -166,7 +166,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
val values = GroupMemberRole.values().filter { it <= groupInfo.membership.memberRole }.map { it to it.text }
|
val values = GroupMemberRole.values().filter { it <= groupInfo.membership.memberRole && it != GroupMemberRole.Observer }.map { it to it.text }
|
||||||
ExposedDropDownSettingRow(
|
ExposedDropDownSettingRow(
|
||||||
generalGetString(R.string.new_member_role),
|
generalGetString(R.string.new_member_role),
|
||||||
values,
|
values,
|
||||||
|
@ -34,7 +34,7 @@ import chat.simplex.app.views.helpers.*
|
|||||||
import chat.simplex.app.views.usersettings.*
|
import chat.simplex.app.views.usersettings.*
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdated: (String?) -> Unit, close: () -> Unit) {
|
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) {
|
||||||
BackHandler(onBack = close)
|
BackHandler(onBack = close)
|
||||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||||
@ -95,9 +95,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdat
|
|||||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||||
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
|
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
|
||||||
manageGroupLink = {
|
manageGroupLink = {
|
||||||
withApi {
|
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
|
||||||
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, onGroupLinkUpdated) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -300,6 +298,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
Modifier.weight(1f).padding(end = DEFAULT_PADDING),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package chat.simplex.app.views.chat.group
|
package chat.simplex.app.views.chat.group
|
||||||
|
|
||||||
|
import SectionItemView
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@ -15,22 +18,26 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.app.R
|
import chat.simplex.app.R
|
||||||
import chat.simplex.app.model.ChatModel
|
import chat.simplex.app.model.*
|
||||||
import chat.simplex.app.model.GroupInfo
|
|
||||||
import chat.simplex.app.ui.theme.*
|
import chat.simplex.app.ui.theme.*
|
||||||
import chat.simplex.app.views.helpers.*
|
import chat.simplex.app.views.helpers.*
|
||||||
import chat.simplex.app.views.newchat.QRCode
|
import chat.simplex.app.views.newchat.QRCode
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, onGroupLinkUpdated: (String?) -> Unit) {
|
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit) {
|
||||||
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
|
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
|
||||||
|
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
|
||||||
var creatingLink by rememberSaveable { mutableStateOf(false) }
|
var creatingLink by rememberSaveable { mutableStateOf(false) }
|
||||||
val cxt = LocalContext.current
|
val cxt = LocalContext.current
|
||||||
fun createLink() {
|
fun createLink() {
|
||||||
creatingLink = true
|
creatingLink = true
|
||||||
withApi {
|
withApi {
|
||||||
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
|
val link = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
|
||||||
onGroupLinkUpdated(groupLink)
|
if (link != null) {
|
||||||
|
groupLink = link.first
|
||||||
|
groupLinkMemberRole.value = link.second
|
||||||
|
onGroupLinkUpdated(groupLink to groupLinkMemberRole.value)
|
||||||
|
}
|
||||||
creatingLink = false
|
creatingLink = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,9 +48,24 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
|
|||||||
}
|
}
|
||||||
GroupLinkLayout(
|
GroupLinkLayout(
|
||||||
groupLink = groupLink,
|
groupLink = groupLink,
|
||||||
|
groupInfo,
|
||||||
|
groupLinkMemberRole,
|
||||||
creatingLink,
|
creatingLink,
|
||||||
createLink = ::createLink,
|
createLink = ::createLink,
|
||||||
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
|
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
|
||||||
|
updateLink = {
|
||||||
|
val role = groupLinkMemberRole.value
|
||||||
|
if (role != null) {
|
||||||
|
withBGApi {
|
||||||
|
val link = chatModel.controller.apiGroupLinkMemberRole(groupInfo.groupId, role)
|
||||||
|
if (link != null) {
|
||||||
|
groupLink = link.first
|
||||||
|
groupLinkMemberRole.value = link.second
|
||||||
|
onGroupLinkUpdated(groupLink to groupLinkMemberRole.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
deleteLink = {
|
deleteLink = {
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title = generalGetString(R.string.delete_link_question),
|
title = generalGetString(R.string.delete_link_question),
|
||||||
@ -54,7 +76,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
|
|||||||
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
|
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
|
||||||
if (r) {
|
if (r) {
|
||||||
groupLink = null
|
groupLink = null
|
||||||
onGroupLinkUpdated(null)
|
onGroupLinkUpdated(null to null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,13 +91,18 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
|
|||||||
@Composable
|
@Composable
|
||||||
fun GroupLinkLayout(
|
fun GroupLinkLayout(
|
||||||
groupLink: String?,
|
groupLink: String?,
|
||||||
|
groupInfo: GroupInfo,
|
||||||
|
groupLinkMemberRole: MutableState<GroupMemberRole?>,
|
||||||
creatingLink: Boolean,
|
creatingLink: Boolean,
|
||||||
createLink: () -> Unit,
|
createLink: () -> Unit,
|
||||||
share: () -> Unit,
|
share: () -> Unit,
|
||||||
|
updateLink: () -> Unit,
|
||||||
deleteLink: () -> Unit
|
deleteLink: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.padding(horizontal = DEFAULT_PADDING),
|
Modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(start = DEFAULT_PADDING, bottom = DEFAULT_BOTTOM_PADDING, end = DEFAULT_PADDING),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top
|
verticalArrangement = Arrangement.Top
|
||||||
) {
|
) {
|
||||||
@ -93,7 +120,17 @@ fun GroupLinkLayout(
|
|||||||
if (groupLink == null) {
|
if (groupLink == null) {
|
||||||
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, disabled = creatingLink, click = createLink)
|
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, disabled = creatingLink, click = createLink)
|
||||||
} else {
|
} else {
|
||||||
QRCode(groupLink, Modifier.weight(1f, fill = false).aspectRatio(1f))
|
// SectionItemView(padding = PaddingValues(bottom = DEFAULT_PADDING)) {
|
||||||
|
// RoleSelectionRow(groupInfo, groupLinkMemberRole)
|
||||||
|
// }
|
||||||
|
var initialLaunch by remember { mutableStateOf(true) }
|
||||||
|
LaunchedEffect(groupLinkMemberRole.value) {
|
||||||
|
if (!initialLaunch) {
|
||||||
|
updateLink()
|
||||||
|
}
|
||||||
|
initialLaunch = false
|
||||||
|
}
|
||||||
|
QRCode(groupLink, Modifier.aspectRatio(1f))
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@ -116,6 +153,25 @@ fun GroupLinkLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole?>, enabled: Boolean = true) {
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
val values = listOf(GroupMemberRole.Member, GroupMemberRole.Observer).map { it to it.text }
|
||||||
|
ExposedDropDownSettingRow(
|
||||||
|
generalGetString(R.string.initial_member_role),
|
||||||
|
values,
|
||||||
|
selectedRole,
|
||||||
|
icon = null,
|
||||||
|
enabled = rememberUpdatedState(enabled),
|
||||||
|
onSelected = { selectedRole.value = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProgressIndicator() {
|
fun ProgressIndicator() {
|
||||||
Box(
|
Box(
|
||||||
|
@ -154,7 +154,7 @@ fun DatabaseLayout(
|
|||||||
val operationsDisabled = !stopped || progressIndicator
|
val operationsDisabled = !stopped || progressIndicator
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 48.dp),
|
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = DEFAULT_BOTTOM_PADDING),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
) {
|
) {
|
||||||
AppBarTitle(stringResource(R.string.your_chat_database))
|
AppBarTitle(stringResource(R.string.your_chat_database))
|
||||||
|
@ -85,13 +85,14 @@ fun SectionItemView(
|
|||||||
click: (() -> Unit)? = null,
|
click: (() -> Unit)? = null,
|
||||||
minHeight: Dp = 46.dp,
|
minHeight: Dp = 46.dp,
|
||||||
disabled: Boolean = false,
|
disabled: Boolean = false,
|
||||||
|
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING),
|
||||||
content: (@Composable RowScope.() -> Unit)
|
content: (@Composable RowScope.() -> Unit)
|
||||||
) {
|
) {
|
||||||
val modifier = Modifier
|
val modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.sizeIn(minHeight = minHeight)
|
.sizeIn(minHeight = minHeight)
|
||||||
Row(
|
Row(
|
||||||
if (click == null || disabled) modifier.padding(horizontal = DEFAULT_PADDING) else modifier.clickable(onClick = click).padding(horizontal = DEFAULT_PADDING),
|
if (click == null || disabled) modifier.padding(padding) else modifier.clickable(onClick = click).padding(padding),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
|
@ -222,6 +222,9 @@
|
|||||||
<string name="images_limit_desc">Only 10 images can be sent at the same time</string>
|
<string name="images_limit_desc">Only 10 images can be sent at the same time</string>
|
||||||
<string name="image_decoding_exception_title">Decoding error</string>
|
<string name="image_decoding_exception_title">Decoding error</string>
|
||||||
<string name="image_decoding_exception_desc">The image cannot be decoded. Please, try a different image or contact developers.</string>
|
<string name="image_decoding_exception_desc">The image cannot be decoded. Please, try a different image or contact developers.</string>
|
||||||
|
<string name="you_are_observer">you are observer</string>
|
||||||
|
<string name="observer_cant_send_message_title">You can\'t send messages!</string>
|
||||||
|
<string name="observer_cant_send_message_desc">Please contact group admin.</string>
|
||||||
|
|
||||||
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
|
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
|
||||||
<string name="image_descr">Image</string>
|
<string name="image_descr">Image</string>
|
||||||
@ -868,6 +871,7 @@
|
|||||||
<string name="snd_conn_event_switch_queue_phase_changing">changing address…</string>
|
<string name="snd_conn_event_switch_queue_phase_changing">changing address…</string>
|
||||||
|
|
||||||
<!-- GroupMemberRole -->
|
<!-- GroupMemberRole -->
|
||||||
|
<string name="group_member_role_observer">observer</string>
|
||||||
<string name="group_member_role_member">member</string>
|
<string name="group_member_role_member">member</string>
|
||||||
<string name="group_member_role_admin">admin</string>
|
<string name="group_member_role_admin">admin</string>
|
||||||
<string name="group_member_role_owner">owner</string>
|
<string name="group_member_role_owner">owner</string>
|
||||||
@ -890,6 +894,7 @@
|
|||||||
<!-- AddGroupMembersView.kt -->
|
<!-- AddGroupMembersView.kt -->
|
||||||
<string name="no_contacts_to_add">No contacts to add</string>
|
<string name="no_contacts_to_add">No contacts to add</string>
|
||||||
<string name="new_member_role">New member role</string>
|
<string name="new_member_role">New member role</string>
|
||||||
|
<string name="initial_member_role">Initial role</string>
|
||||||
<string name="icon_descr_expand_role">Expand role selection</string>
|
<string name="icon_descr_expand_role">Expand role selection</string>
|
||||||
<string name="invite_to_group_button">Invite to group</string>
|
<string name="invite_to_group_button">Invite to group</string>
|
||||||
<string name="skip_inviting_button">Skip inviting members</string>
|
<string name="skip_inviting_button">Skip inviting members</string>
|
||||||
@ -919,6 +924,7 @@
|
|||||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">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.</string>
|
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">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.</string>
|
||||||
<string name="all_group_members_will_remain_connected">All group members will remain connected.</string>
|
<string name="all_group_members_will_remain_connected">All group members will remain connected.</string>
|
||||||
<string name="error_creating_link_for_group">Error creating group link</string>
|
<string name="error_creating_link_for_group">Error creating group link</string>
|
||||||
|
<string name="error_updating_link_for_group">Error updating group link</string>
|
||||||
<string name="error_deleting_link_for_group">Error deleting group link</string>
|
<string name="error_deleting_link_for_group">Error deleting group link</string>
|
||||||
<string name="only_group_owners_can_change_prefs">Only group owners can change group preferences.</string>
|
<string name="only_group_owners_can_change_prefs">Only group owners can change group preferences.</string>
|
||||||
|
|
||||||
|
@ -545,6 +545,25 @@ final class Chat: ObservableObject, Identifiable {
|
|||||||
self.chatStats = chatStats
|
self.chatStats = chatStats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userCanSend: Bool {
|
||||||
|
switch chatInfo {
|
||||||
|
case .direct: return true
|
||||||
|
case let .group(groupInfo):
|
||||||
|
let m = groupInfo.membership
|
||||||
|
return m.memberActive && m.memberRole >= .member
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var userIsObserver: Bool {
|
||||||
|
switch chatInfo {
|
||||||
|
case let .group(groupInfo):
|
||||||
|
let m = groupInfo.membership
|
||||||
|
return m.memberActive && m.memberRole == .observer
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var id: ChatId { get { chatInfo.id } }
|
var id: ChatId { get { chatInfo.id } }
|
||||||
|
|
||||||
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
|
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
|
||||||
|
@ -344,7 +344,7 @@ func setUserSMPServers(smpServers: [ServerCfg]) async throws {
|
|||||||
|
|
||||||
func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> {
|
func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> {
|
||||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("testSMPServer: no current user") }
|
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("testSMPServer: no current user") }
|
||||||
let r = await chatSendCmd(.testSMPServer(userId: userId, smpServer: smpServer))
|
let r = await chatSendCmd(.apiTestSMPServer(userId: userId, smpServer: smpServer))
|
||||||
if case let .smpTestResult(_, testFailure) = r {
|
if case let .smpTestResult(_, testFailure) = r {
|
||||||
if let t = testFailure {
|
if let t = testFailure {
|
||||||
return .failure(t)
|
return .failure(t)
|
||||||
@ -868,9 +868,15 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiCreateGroupLink(_ groupId: Int64) async throws -> String {
|
func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) {
|
||||||
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId))
|
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole))
|
||||||
if case let .groupLinkCreated(_, _, connReq) = r { return connReq }
|
if case let .groupLinkCreated(_, _, connReq, memberRole) = r { return (connReq, memberRole) }
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) {
|
||||||
|
let r = await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole))
|
||||||
|
if case let .groupLink(_, _, connReq, memberRole) = r { return (connReq, memberRole) }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -880,11 +886,11 @@ func apiDeleteGroupLink(_ groupId: Int64) async throws {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiGetGroupLink(_ groupId: Int64) throws -> String? {
|
func apiGetGroupLink(_ groupId: Int64) throws -> (String, GroupMemberRole)? {
|
||||||
let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId))
|
let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId))
|
||||||
switch r {
|
switch r {
|
||||||
case let .groupLink(_, _, connReq):
|
case let .groupLink(_, _, connReq, memberRole):
|
||||||
return connReq
|
return (connReq, memberRole)
|
||||||
case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)):
|
case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)):
|
||||||
return nil
|
return nil
|
||||||
default: throw r
|
default: throw r
|
||||||
@ -1180,6 +1186,10 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
if active(user) {
|
if active(user) {
|
||||||
m.updateGroup(toGroup)
|
m.updateGroup(toGroup)
|
||||||
}
|
}
|
||||||
|
case let .memberRole(user, groupInfo, _, _, _, _):
|
||||||
|
if active(user) {
|
||||||
|
m.updateGroup(groupInfo)
|
||||||
|
}
|
||||||
case let .rcvFileStart(user, aChatItem):
|
case let .rcvFileStart(user, aChatItem):
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
chatItemSimpleUpdate(user, aChatItem)
|
||||||
case let .rcvFileComplete(user, aChatItem):
|
case let .rcvFileComplete(user, aChatItem):
|
||||||
|
@ -258,10 +258,11 @@ struct ComposeView: View {
|
|||||||
Image(systemName: "paperclip")
|
Image(systemName: "paperclip")
|
||||||
.resizable()
|
.resizable()
|
||||||
}
|
}
|
||||||
.disabled(composeState.attachmentDisabled)
|
.disabled(composeState.attachmentDisabled || !chat.userCanSend)
|
||||||
.frame(width: 25, height: 25)
|
.frame(width: 25, height: 25)
|
||||||
.padding(.bottom, 12)
|
.padding(.bottom, 12)
|
||||||
.padding(.leading, 12)
|
.padding(.leading, 12)
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
SendMessageView(
|
SendMessageView(
|
||||||
composeState: $composeState,
|
composeState: $composeState,
|
||||||
sendMessage: {
|
sendMessage: {
|
||||||
@ -288,6 +289,21 @@ struct ComposeView: View {
|
|||||||
)
|
)
|
||||||
.padding(.trailing, 12)
|
.padding(.trailing, 12)
|
||||||
.background(.background)
|
.background(.background)
|
||||||
|
.disabled(!chat.userCanSend)
|
||||||
|
|
||||||
|
if chat.userIsObserver {
|
||||||
|
Text("you are observer")
|
||||||
|
.italic()
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.onTapGesture {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title: "You can't send messages!",
|
||||||
|
message: "Please contact group admin."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: composeState.message) { _ in
|
.onChange(of: composeState.message) { _ in
|
||||||
@ -299,6 +315,13 @@ struct ComposeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: chat.userCanSend) { canSend in
|
||||||
|
if !canSend {
|
||||||
|
cancelCurrentVoiceRecording()
|
||||||
|
clearCurrentDraft()
|
||||||
|
clearState()
|
||||||
|
}
|
||||||
|
}
|
||||||
.confirmationDialog("Attach", isPresented: $showChooseSource, titleVisibility: .visible) {
|
.confirmationDialog("Attach", isPresented: $showChooseSource, titleVisibility: .visible) {
|
||||||
Button("Take picture") {
|
Button("Take picture") {
|
||||||
showTakePhoto = true
|
showTakePhoto = true
|
||||||
|
@ -140,7 +140,7 @@ struct AddGroupMembersView: View {
|
|||||||
private func rolePicker() -> some View {
|
private func rolePicker() -> some View {
|
||||||
Picker("New member role", selection: $selectedRole) {
|
Picker("New member role", selection: $selectedRole) {
|
||||||
ForEach(GroupMemberRole.allCases) { role in
|
ForEach(GroupMemberRole.allCases) { role in
|
||||||
if role <= groupInfo.membership.memberRole {
|
if role <= groupInfo.membership.memberRole && role != .observer {
|
||||||
Text(role.text)
|
Text(role.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ struct GroupChatInfoView: View {
|
|||||||
@ObservedObject private var alertManager = AlertManager.shared
|
@ObservedObject private var alertManager = AlertManager.shared
|
||||||
@State private var alert: GroupChatInfoViewAlert? = nil
|
@State private var alert: GroupChatInfoViewAlert? = nil
|
||||||
@State private var groupLink: String?
|
@State private var groupLink: String?
|
||||||
|
@State private var groupLinkMemberRole: GroupMemberRole = .member
|
||||||
@State private var showAddMembersSheet: Bool = false
|
@State private var showAddMembersSheet: Bool = false
|
||||||
@State private var connectionStats: ConnectionStats?
|
@State private var connectionStats: ConnectionStats?
|
||||||
@State private var connectionCode: String?
|
@State private var connectionCode: String?
|
||||||
@ -107,7 +108,9 @@ struct GroupChatInfoView: View {
|
|||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
do {
|
do {
|
||||||
groupLink = try apiGetGroupLink(groupInfo.groupId)
|
if let link = try apiGetGroupLink(groupInfo.groupId) {
|
||||||
|
(groupLink, groupLinkMemberRole) = link
|
||||||
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
|
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
|
||||||
}
|
}
|
||||||
@ -187,7 +190,7 @@ struct GroupChatInfoView: View {
|
|||||||
|
|
||||||
private func groupLinkButton() -> some View {
|
private func groupLinkButton() -> some View {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink)
|
GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink, groupLinkMemberRole: $groupLinkMemberRole)
|
||||||
.navigationBarTitle("Group link")
|
.navigationBarTitle("Group link")
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -12,6 +12,7 @@ import SimpleXChat
|
|||||||
struct GroupLinkView: View {
|
struct GroupLinkView: View {
|
||||||
var groupId: Int64
|
var groupId: Int64
|
||||||
@Binding var groupLink: String?
|
@Binding var groupLink: String?
|
||||||
|
@Binding var groupLinkMemberRole: GroupMemberRole
|
||||||
@State private var creatingLink = false
|
@State private var creatingLink = false
|
||||||
@State private var alert: GroupLinkAlert?
|
@State private var alert: GroupLinkAlert?
|
||||||
|
|
||||||
@ -33,6 +34,15 @@ struct GroupLinkView: View {
|
|||||||
Text("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.")
|
Text("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.")
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
if let groupLink = groupLink {
|
if let groupLink = groupLink {
|
||||||
|
// HStack {
|
||||||
|
// Text("Initial role")
|
||||||
|
// Picker("Initial role", selection: $groupLinkMemberRole) {
|
||||||
|
// ForEach([GroupMemberRole.member, GroupMemberRole.observer]) { role in
|
||||||
|
// Text(role.text)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .frame(maxWidth: .infinity, alignment: .leading)
|
||||||
QRCode(uri: groupLink)
|
QRCode(uri: groupLink)
|
||||||
HStack {
|
HStack {
|
||||||
Button {
|
Button {
|
||||||
@ -85,6 +95,16 @@ struct GroupLinkView: View {
|
|||||||
return Alert(title: Text(title), message: Text(error))
|
return Alert(title: Text(title), message: Text(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: groupLinkMemberRole) { _ in
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
_ = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole)
|
||||||
|
} catch let error {
|
||||||
|
let a = getErrorAlert(error, "Error updating group link")
|
||||||
|
alert = .error(title: a.title, error: a.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if groupLink == nil && !creatingLink {
|
if groupLink == nil && !creatingLink {
|
||||||
createGroupLink()
|
createGroupLink()
|
||||||
@ -100,7 +120,7 @@ struct GroupLinkView: View {
|
|||||||
let link = try await apiCreateGroupLink(groupId)
|
let link = try await apiCreateGroupLink(groupId)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
creatingLink = false
|
creatingLink = false
|
||||||
groupLink = link
|
(groupLink, groupLinkMemberRole) = link
|
||||||
}
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))")
|
logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))")
|
||||||
@ -120,8 +140,8 @@ struct GroupLinkView_Previews: PreviewProvider {
|
|||||||
@State var noGroupLink: String? = nil
|
@State var noGroupLink: String? = nil
|
||||||
|
|
||||||
return Group {
|
return Group {
|
||||||
GroupLinkView(groupId: 1, groupLink: $groupLink)
|
GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member))
|
||||||
GroupLinkView(groupId: 1, groupLink: $noGroupLink)
|
GroupLinkView(groupId: 1, groupLink: $noGroupLink, groupLinkMemberRole: Binding.constant(.member))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,12 +46,13 @@ public enum ChatCommand {
|
|||||||
case apiLeaveGroup(groupId: Int64)
|
case apiLeaveGroup(groupId: Int64)
|
||||||
case apiListMembers(groupId: Int64)
|
case apiListMembers(groupId: Int64)
|
||||||
case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile)
|
case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile)
|
||||||
case apiCreateGroupLink(groupId: Int64)
|
case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole)
|
||||||
|
case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole)
|
||||||
case apiDeleteGroupLink(groupId: Int64)
|
case apiDeleteGroupLink(groupId: Int64)
|
||||||
case apiGetGroupLink(groupId: Int64)
|
case apiGetGroupLink(groupId: Int64)
|
||||||
case apiGetUserSMPServers(userId: Int64)
|
case apiGetUserSMPServers(userId: Int64)
|
||||||
case apiSetUserSMPServers(userId: Int64, smpServers: [ServerCfg])
|
case apiSetUserSMPServers(userId: Int64, smpServers: [ServerCfg])
|
||||||
case testSMPServer(userId: Int64, smpServer: String)
|
case apiTestSMPServer(userId: Int64, smpServer: String)
|
||||||
case apiSetChatItemTTL(userId: Int64, seconds: Int64?)
|
case apiSetChatItemTTL(userId: Int64, seconds: Int64?)
|
||||||
case apiGetChatItemTTL(userId: Int64)
|
case apiGetChatItemTTL(userId: Int64)
|
||||||
case apiSetNetworkConfig(networkConfig: NetCfg)
|
case apiSetNetworkConfig(networkConfig: NetCfg)
|
||||||
@ -134,12 +135,13 @@ public enum ChatCommand {
|
|||||||
case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)"
|
case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)"
|
||||||
case let .apiListMembers(groupId): return "/_members #\(groupId)"
|
case let .apiListMembers(groupId): return "/_members #\(groupId)"
|
||||||
case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))"
|
case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))"
|
||||||
case let .apiCreateGroupLink(groupId): return "/_create link #\(groupId)"
|
case let .apiCreateGroupLink(groupId, memberRole): return "/_create link #\(groupId) \(memberRole)"
|
||||||
|
case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)"
|
||||||
case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)"
|
case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)"
|
||||||
case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)"
|
case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)"
|
||||||
case let .apiGetUserSMPServers(userId): return "/_smp \(userId)"
|
case let .apiGetUserSMPServers(userId): return "/_smp \(userId)"
|
||||||
case let .apiSetUserSMPServers(userId, smpServers): return "/_smp \(userId) \(smpServersStr(smpServers: smpServers))"
|
case let .apiSetUserSMPServers(userId, smpServers): return "/_smp \(userId) \(smpServersStr(smpServers: smpServers))"
|
||||||
case let .testSMPServer(userId, smpServer): return "/smp test \(userId) \(smpServer)"
|
case let .apiTestSMPServer(userId, smpServer): return "/_smp test \(userId) \(smpServer)"
|
||||||
case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))"
|
case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))"
|
||||||
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
|
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
|
||||||
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
|
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
|
||||||
@ -228,11 +230,12 @@ public enum ChatCommand {
|
|||||||
case .apiListMembers: return "apiListMembers"
|
case .apiListMembers: return "apiListMembers"
|
||||||
case .apiUpdateGroupProfile: return "apiUpdateGroupProfile"
|
case .apiUpdateGroupProfile: return "apiUpdateGroupProfile"
|
||||||
case .apiCreateGroupLink: return "apiCreateGroupLink"
|
case .apiCreateGroupLink: return "apiCreateGroupLink"
|
||||||
|
case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole"
|
||||||
case .apiDeleteGroupLink: return "apiDeleteGroupLink"
|
case .apiDeleteGroupLink: return "apiDeleteGroupLink"
|
||||||
case .apiGetGroupLink: return "apiGetGroupLink"
|
case .apiGetGroupLink: return "apiGetGroupLink"
|
||||||
case .apiGetUserSMPServers: return "apiGetUserSMPServers"
|
case .apiGetUserSMPServers: return "apiGetUserSMPServers"
|
||||||
case .apiSetUserSMPServers: return "apiSetUserSMPServers"
|
case .apiSetUserSMPServers: return "apiSetUserSMPServers"
|
||||||
case .testSMPServer: return "testSMPServer"
|
case .apiTestSMPServer: return "testSMPServer"
|
||||||
case .apiSetChatItemTTL: return "apiSetChatItemTTL"
|
case .apiSetChatItemTTL: return "apiSetChatItemTTL"
|
||||||
case .apiGetChatItemTTL: return "apiGetChatItemTTL"
|
case .apiGetChatItemTTL: return "apiGetChatItemTTL"
|
||||||
case .apiSetNetworkConfig: return "apiSetNetworkConfig"
|
case .apiSetNetworkConfig: return "apiSetNetworkConfig"
|
||||||
@ -391,8 +394,8 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case connectedToGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember)
|
case connectedToGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||||
case groupRemoved(user: User, groupInfo: GroupInfo) // unused
|
case groupRemoved(user: User, groupInfo: GroupInfo) // unused
|
||||||
case groupUpdated(user: User, toGroup: GroupInfo)
|
case groupUpdated(user: User, toGroup: GroupInfo)
|
||||||
case groupLinkCreated(user: User, groupInfo: GroupInfo, connReqContact: String)
|
case groupLinkCreated(user: User, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole)
|
||||||
case groupLink(user: User, groupInfo: GroupInfo, connReqContact: String)
|
case groupLink(user: User, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole)
|
||||||
case groupLinkDeleted(user: User, groupInfo: GroupInfo)
|
case groupLinkDeleted(user: User, groupInfo: GroupInfo)
|
||||||
// receiving file events
|
// receiving file events
|
||||||
case rcvFileAccepted(user: User, chatItem: AChatItem)
|
case rcvFileAccepted(user: User, chatItem: AChatItem)
|
||||||
@ -606,8 +609,8 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .connectedToGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
case let .connectedToGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||||
case let .groupRemoved(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
case let .groupRemoved(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||||
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
|
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
|
||||||
case let .groupLinkCreated(u, groupInfo, connReqContact): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)")
|
case let .groupLinkCreated(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)")
|
||||||
case let .groupLink(u, groupInfo, connReqContact): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)")
|
case let .groupLink(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)")
|
||||||
case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||||
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
|
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||||
case .rcvFileAcceptedSndCancelled: return noDetails
|
case .rcvFileAcceptedSndCancelled: return noDetails
|
||||||
|
@ -1517,7 +1517,7 @@ public struct GroupMember: Identifiable, Decodable {
|
|||||||
public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? {
|
public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? {
|
||||||
if !canBeRemoved(groupInfo: groupInfo) { return nil }
|
if !canBeRemoved(groupInfo: groupInfo) { return nil }
|
||||||
let userRole = groupInfo.membership.memberRole
|
let userRole = groupInfo.membership.memberRole
|
||||||
return GroupMemberRole.allCases.filter { $0 <= userRole }
|
return GroupMemberRole.allCases.filter { $0 <= userRole && $0 != .observer }
|
||||||
}
|
}
|
||||||
|
|
||||||
public var memberIncognito: Bool {
|
public var memberIncognito: Bool {
|
||||||
@ -1546,6 +1546,7 @@ public struct GroupMemberRef: Decodable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
|
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
|
||||||
|
case observer = "observer"
|
||||||
case member = "member"
|
case member = "member"
|
||||||
case admin = "admin"
|
case admin = "admin"
|
||||||
case owner = "owner"
|
case owner = "owner"
|
||||||
@ -1554,6 +1555,7 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec
|
|||||||
|
|
||||||
public var text: String {
|
public var text: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .observer: return NSLocalizedString("observer", comment: "member role")
|
||||||
case .member: return NSLocalizedString("member", comment: "member role")
|
case .member: return NSLocalizedString("member", comment: "member role")
|
||||||
case .admin: return NSLocalizedString("admin", comment: "member role")
|
case .admin: return NSLocalizedString("admin", comment: "member role")
|
||||||
case .owner: return NSLocalizedString("owner", comment: "member role")
|
case .owner: return NSLocalizedString("owner", comment: "member role")
|
||||||
@ -1562,9 +1564,10 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec
|
|||||||
|
|
||||||
private var comparisonValue: Int {
|
private var comparisonValue: Int {
|
||||||
switch self {
|
switch self {
|
||||||
case .member: return 0
|
case .observer: return 0
|
||||||
case .admin: return 1
|
case .member: return 1
|
||||||
case .owner: return 2
|
case .admin: return 2
|
||||||
|
case .owner: return 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 4.5.3.1
|
version: 4.5.4.1
|
||||||
#synopsis:
|
#synopsis:
|
||||||
#description:
|
#description:
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
|
@ -5,7 +5,7 @@ cabal-version: 1.12
|
|||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
|
|
||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 4.5.3.1
|
version: 4.5.4.1
|
||||||
category: Web, System, Services, Cryptography
|
category: Web, System, Services, Cryptography
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
author: simplex.chat
|
author: simplex.chat
|
||||||
@ -84,6 +84,7 @@ library
|
|||||||
Simplex.Chat.Migrations.M20230118_recreate_smp_servers
|
Simplex.Chat.Migrations.M20230118_recreate_smp_servers
|
||||||
Simplex.Chat.Migrations.M20230129_drop_chat_items_group_idx
|
Simplex.Chat.Migrations.M20230129_drop_chat_items_group_idx
|
||||||
Simplex.Chat.Migrations.M20230206_item_deleted_by_group_member_id
|
Simplex.Chat.Migrations.M20230206_item_deleted_by_group_member_id
|
||||||
|
Simplex.Chat.Migrations.M20230303_group_link_role
|
||||||
Simplex.Chat.Mobile
|
Simplex.Chat.Mobile
|
||||||
Simplex.Chat.Mobile.WebRTC
|
Simplex.Chat.Mobile.WebRTC
|
||||||
Simplex.Chat.Options
|
Simplex.Chat.Options
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
module Simplex.Chat where
|
module Simplex.Chat where
|
||||||
|
|
||||||
import Control.Applicative (optional, (<|>))
|
import Control.Applicative (optional, (<|>))
|
||||||
import Control.Concurrent.STM (retry, stateTVar)
|
import Control.Concurrent.STM (retry)
|
||||||
import Control.Logger.Simple
|
import Control.Logger.Simple
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
import Control.Monad.IO.Unlift
|
import Control.Monad.IO.Unlift
|
||||||
@ -824,8 +824,10 @@ processChatCommand = \case
|
|||||||
ok user
|
ok user
|
||||||
SetUserSMPServers smpServersConfig -> withUser $ \User {userId} ->
|
SetUserSMPServers smpServersConfig -> withUser $ \User {userId} ->
|
||||||
processChatCommand $ APISetUserSMPServers userId smpServersConfig
|
processChatCommand $ APISetUserSMPServers userId smpServersConfig
|
||||||
TestSMPServer userId smpServer -> withUserId userId $ \user ->
|
APITestSMPServer userId smpServer -> withUserId userId $ \user ->
|
||||||
CRSmpTestResult user <$> withAgent (\a -> testSMPServerConnection a (aUserId user) smpServer)
|
CRSmpTestResult user <$> withAgent (\a -> testSMPServerConnection a (aUserId user) smpServer)
|
||||||
|
TestSMPServer smpServer -> withUser $ \User {userId} ->
|
||||||
|
processChatCommand $ APITestSMPServer userId smpServer
|
||||||
APISetChatItemTTL userId newTTL_ -> withUser' $ \user -> do
|
APISetChatItemTTL userId newTTL_ -> withUser' $ \user -> do
|
||||||
checkSameUser userId user
|
checkSameUser userId user
|
||||||
checkStoreNotChanged $
|
checkStoreNotChanged $
|
||||||
@ -1189,25 +1191,36 @@ processChatCommand = \case
|
|||||||
CRGroupProfile user <$> withStore (\db -> getGroupInfoByName db user gName)
|
CRGroupProfile user <$> withStore (\db -> getGroupInfoByName db user gName)
|
||||||
UpdateGroupDescription gName description ->
|
UpdateGroupDescription gName description ->
|
||||||
updateGroupProfileByName gName $ \p -> p {description}
|
updateGroupProfileByName gName $ \p -> p {description}
|
||||||
APICreateGroupLink groupId -> withUser $ \user -> withChatLock "createGroupLink" $ do
|
APICreateGroupLink groupId mRole -> withUser $ \user -> withChatLock "createGroupLink" $ do
|
||||||
gInfo <- withStore $ \db -> getGroupInfo db user groupId
|
gInfo <- withStore $ \db -> getGroupInfo db user groupId
|
||||||
assertUserGroupRole gInfo GRAdmin
|
assertUserGroupRole gInfo GRAdmin
|
||||||
|
when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole
|
||||||
groupLinkId <- GroupLinkId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16))
|
groupLinkId <- GroupLinkId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16))
|
||||||
let crClientData = encodeJSON $ CRDataGroup groupLinkId
|
let crClientData = encodeJSON $ CRDataGroup groupLinkId
|
||||||
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact $ Just crClientData
|
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact $ Just crClientData
|
||||||
withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId
|
withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole
|
||||||
pure $ CRGroupLinkCreated user gInfo cReq
|
pure $ CRGroupLinkCreated user gInfo cReq mRole
|
||||||
|
APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withChatLock "groupLinkMemberRole " $ do
|
||||||
|
gInfo <- withStore $ \db -> getGroupInfo db user groupId
|
||||||
|
(groupLinkId, groupLink, mRole) <- withStore $ \db -> getGroupLink db user gInfo
|
||||||
|
assertUserGroupRole gInfo GRAdmin
|
||||||
|
when (mRole' > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole'
|
||||||
|
when (mRole' /= mRole) $ withStore' $ \db -> setGroupLinkMemberRole db user groupLinkId mRole'
|
||||||
|
pure $ CRGroupLink user gInfo groupLink mRole'
|
||||||
APIDeleteGroupLink groupId -> withUser $ \user -> withChatLock "deleteGroupLink" $ do
|
APIDeleteGroupLink groupId -> withUser $ \user -> withChatLock "deleteGroupLink" $ do
|
||||||
gInfo <- withStore $ \db -> getGroupInfo db user groupId
|
gInfo <- withStore $ \db -> getGroupInfo db user groupId
|
||||||
deleteGroupLink' user gInfo
|
deleteGroupLink' user gInfo
|
||||||
pure $ CRGroupLinkDeleted user gInfo
|
pure $ CRGroupLinkDeleted user gInfo
|
||||||
APIGetGroupLink groupId -> withUser $ \user -> do
|
APIGetGroupLink groupId -> withUser $ \user -> do
|
||||||
gInfo <- withStore $ \db -> getGroupInfo db user groupId
|
gInfo <- withStore $ \db -> getGroupInfo db user groupId
|
||||||
groupLink <- withStore $ \db -> getGroupLink db user gInfo
|
(_, groupLink, mRole) <- withStore $ \db -> getGroupLink db user gInfo
|
||||||
pure $ CRGroupLink user gInfo groupLink
|
pure $ CRGroupLink user gInfo groupLink mRole
|
||||||
CreateGroupLink gName -> withUser $ \user -> do
|
CreateGroupLink gName mRole -> withUser $ \user -> do
|
||||||
groupId <- withStore $ \db -> getGroupIdByName db user gName
|
groupId <- withStore $ \db -> getGroupIdByName db user gName
|
||||||
processChatCommand $ APICreateGroupLink groupId
|
processChatCommand $ APICreateGroupLink groupId mRole
|
||||||
|
GroupLinkMemberRole gName mRole -> withUser $ \user -> do
|
||||||
|
groupId <- withStore $ \db -> getGroupIdByName db user gName
|
||||||
|
processChatCommand $ APIGroupLinkMemberRole groupId mRole
|
||||||
DeleteGroupLink gName -> withUser $ \user -> do
|
DeleteGroupLink gName -> withUser $ \user -> do
|
||||||
groupId <- withStore $ \db -> getGroupIdByName db user gName
|
groupId <- withStore $ \db -> getGroupIdByName db user gName
|
||||||
processChatCommand $ APIDeleteGroupLink groupId
|
processChatCommand $ APIDeleteGroupLink groupId
|
||||||
@ -2211,7 +2224,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||||||
forM_ groupLinkId $ \_ -> probeMatchingContacts ct $ contactConnIncognito ct
|
forM_ groupLinkId $ \_ -> probeMatchingContacts ct $ contactConnIncognito ct
|
||||||
forM_ viaUserContactLink $ \userContactLinkId ->
|
forM_ viaUserContactLink $ \userContactLinkId ->
|
||||||
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
|
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
|
||||||
Just (UserContactLink {autoAccept = Just AutoAccept {autoReply = mc_}}, groupId_) -> do
|
Just (UserContactLink {autoAccept = Just AutoAccept {autoReply = mc_}}, groupId_, gLinkMemRole) -> do
|
||||||
forM_ mc_ $ \mc -> do
|
forM_ mc_ $ \mc -> do
|
||||||
(msg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc Nothing))
|
(msg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc Nothing))
|
||||||
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc)
|
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc)
|
||||||
@ -2219,7 +2232,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||||||
forM_ groupId_ $ \groupId -> do
|
forM_ groupId_ $ \groupId -> do
|
||||||
gVar <- asks idsDrg
|
gVar <- asks idsDrg
|
||||||
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation
|
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation
|
||||||
withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct GRMember groupConnIds
|
withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) ->
|
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) ->
|
||||||
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
|
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
|
||||||
@ -2576,7 +2589,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||||||
CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact
|
CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact
|
||||||
CORRequest cReq@UserContactRequest {localDisplayName} -> do
|
CORRequest cReq@UserContactRequest {localDisplayName} -> do
|
||||||
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
|
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
|
||||||
Just (UserContactLink {autoAccept}, groupId_) ->
|
Just (UserContactLink {autoAccept}, groupId_, _) ->
|
||||||
case autoAccept of
|
case autoAccept of
|
||||||
Just AutoAccept {acceptIncognito} -> case groupId_ of
|
Just AutoAccept {acceptIncognito} -> case groupId_ of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
@ -3211,9 +3224,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||||||
messageError $ eventName <> ": wrong call state " <> T.pack (show $ callStateTag callState)
|
messageError $ eventName <> ": wrong call state " <> T.pack (show $ callStateTag callState)
|
||||||
|
|
||||||
mergeContacts :: Contact -> Contact -> m ()
|
mergeContacts :: Contact -> Contact -> m ()
|
||||||
mergeContacts to from = do
|
mergeContacts c1 c2 = do
|
||||||
withStore' $ \db -> mergeContactRecords db userId to from
|
withStore' $ \db -> mergeContactRecords db userId c1 c2
|
||||||
toView $ CRContactsMerged user to from
|
toView $ CRContactsMerged user c1 c2
|
||||||
|
|
||||||
saveConnInfo :: Connection -> ConnInfo -> m ()
|
saveConnInfo :: Connection -> ConnInfo -> m ()
|
||||||
saveConnInfo activeConn connInfo = do
|
saveConnInfo activeConn connInfo = do
|
||||||
@ -4000,7 +4013,8 @@ chatCommandP =
|
|||||||
"/smp_servers " *> (SetUserSMPServers . SMPServersConfig . map toServerCfg <$> smpServersP),
|
"/smp_servers " *> (SetUserSMPServers . SMPServersConfig . map toServerCfg <$> smpServersP),
|
||||||
"/smp_servers" $> GetUserSMPServers,
|
"/smp_servers" $> GetUserSMPServers,
|
||||||
"/smp default" $> SetUserSMPServers (SMPServersConfig []),
|
"/smp default" $> SetUserSMPServers (SMPServersConfig []),
|
||||||
"/smp test " *> (TestSMPServer <$> A.decimal <* A.space <*> strP),
|
"/_smp test " *> (APITestSMPServer <$> A.decimal <* A.space <*> strP),
|
||||||
|
"/smp test " *> (TestSMPServer <$> strP),
|
||||||
"/_smp " *> (APISetUserSMPServers <$> A.decimal <* A.space <*> jsonP),
|
"/_smp " *> (APISetUserSMPServers <$> A.decimal <* A.space <*> jsonP),
|
||||||
"/smp " *> (SetUserSMPServers . SMPServersConfig . map toServerCfg <$> smpServersP),
|
"/smp " *> (SetUserSMPServers . SMPServersConfig . map toServerCfg <$> smpServersP),
|
||||||
"/_smp " *> (APIGetUserSMPServers <$> A.decimal),
|
"/_smp " *> (APIGetUserSMPServers <$> A.decimal),
|
||||||
@ -4035,13 +4049,14 @@ chatCommandP =
|
|||||||
"/enable #" *> (EnableGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName),
|
"/enable #" *> (EnableGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||||
("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles,
|
("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles,
|
||||||
("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups,
|
("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups,
|
||||||
|
("/help contacts" <|> "/help contact" <|> "/hc") $> ChatHelp HSContacts,
|
||||||
("/help address" <|> "/ha") $> ChatHelp HSMyAddress,
|
("/help address" <|> "/ha") $> ChatHelp HSMyAddress,
|
||||||
("/help messages" <|> "/hm") $> ChatHelp HSMessages,
|
("/help messages" <|> "/hm") $> ChatHelp HSMessages,
|
||||||
("/help settings" <|> "/hs") $> ChatHelp HSSettings,
|
("/help settings" <|> "/hs") $> ChatHelp HSSettings,
|
||||||
("/help" <|> "/h") $> ChatHelp HSMain,
|
("/help" <|> "/h") $> ChatHelp HSMain,
|
||||||
("/group " <|> "/g ") *> char_ '#' *> (NewGroup <$> groupProfile),
|
("/group " <|> "/g ") *> char_ '#' *> (NewGroup <$> groupProfile),
|
||||||
"/_group " *> (APINewGroup <$> A.decimal <* A.space <*> jsonP),
|
"/_group " *> (APINewGroup <$> A.decimal <* A.space <*> jsonP),
|
||||||
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
|
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRAdmin)),
|
||||||
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName),
|
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName),
|
||||||
("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
|
("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
|
||||||
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName),
|
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||||
@ -4056,10 +4071,12 @@ chatCommandP =
|
|||||||
("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayName <* A.space <*> groupProfile),
|
("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayName <* A.space <*> groupProfile),
|
||||||
("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayName),
|
("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayName),
|
||||||
"/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayName <*> optional (A.space *> msgTextP)),
|
"/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayName <*> optional (A.space *> msgTextP)),
|
||||||
"/_create link #" *> (APICreateGroupLink <$> A.decimal),
|
"/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember)),
|
||||||
|
"/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole),
|
||||||
"/_delete link #" *> (APIDeleteGroupLink <$> A.decimal),
|
"/_delete link #" *> (APIDeleteGroupLink <$> A.decimal),
|
||||||
"/_get link #" *> (APIGetGroupLink <$> A.decimal),
|
"/_get link #" *> (APIGetGroupLink <$> A.decimal),
|
||||||
"/create link #" *> (CreateGroupLink <$> displayName),
|
"/create link #" *> (CreateGroupLink <$> displayName <*> (memberRole <|> pure GRMember)),
|
||||||
|
"/set link role #" *> (GroupLinkMemberRole <$> displayName <*> memberRole),
|
||||||
"/delete link #" *> (DeleteGroupLink <$> displayName),
|
"/delete link #" *> (DeleteGroupLink <$> displayName),
|
||||||
"/show link #" *> (ShowGroupLink <$> displayName),
|
"/show link #" *> (ShowGroupLink <$> displayName),
|
||||||
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <*> pure Nothing <*> quotedMsg <*> msgTextP),
|
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <*> pure Nothing <*> quotedMsg <*> msgTextP),
|
||||||
@ -4169,8 +4186,7 @@ chatCommandP =
|
|||||||
[ " owner" $> GROwner,
|
[ " owner" $> GROwner,
|
||||||
" admin" $> GRAdmin,
|
" admin" $> GRAdmin,
|
||||||
" member" $> GRMember,
|
" member" $> GRMember,
|
||||||
-- " observer" $> GRObserver,
|
" observer" $> GRObserver
|
||||||
pure GRAdmin
|
|
||||||
]
|
]
|
||||||
chatNameP = ChatName <$> chatTypeP <*> displayName
|
chatNameP = ChatName <$> chatTypeP <*> displayName
|
||||||
chatNameP' = ChatName <$> (chatTypeP <|> pure CTDirect) <*> displayName
|
chatNameP' = ChatName <$> (chatTypeP <|> pure CTDirect) <*> displayName
|
||||||
|
@ -170,7 +170,7 @@ data ChatController = ChatController
|
|||||||
logFilePath :: Maybe FilePath
|
logFilePath :: Maybe FilePath
|
||||||
}
|
}
|
||||||
|
|
||||||
data HelpSection = HSMain | HSFiles | HSGroups | HSMyAddress | HSMarkdown | HSMessages | HSSettings
|
data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSMarkdown | HSMessages | HSSettings
|
||||||
deriving (Show, Generic)
|
deriving (Show, Generic)
|
||||||
|
|
||||||
instance ToJSON HelpSection where
|
instance ToJSON HelpSection where
|
||||||
@ -237,14 +237,16 @@ data ChatCommand
|
|||||||
| APILeaveGroup GroupId
|
| APILeaveGroup GroupId
|
||||||
| APIListMembers GroupId
|
| APIListMembers GroupId
|
||||||
| APIUpdateGroupProfile GroupId GroupProfile
|
| APIUpdateGroupProfile GroupId GroupProfile
|
||||||
| APICreateGroupLink GroupId
|
| APICreateGroupLink GroupId GroupMemberRole
|
||||||
|
| APIGroupLinkMemberRole GroupId GroupMemberRole
|
||||||
| APIDeleteGroupLink GroupId
|
| APIDeleteGroupLink GroupId
|
||||||
| APIGetGroupLink GroupId
|
| APIGetGroupLink GroupId
|
||||||
| APIGetUserSMPServers UserId
|
| APIGetUserSMPServers UserId
|
||||||
| GetUserSMPServers
|
| GetUserSMPServers
|
||||||
| APISetUserSMPServers UserId SMPServersConfig
|
| APISetUserSMPServers UserId SMPServersConfig
|
||||||
| SetUserSMPServers SMPServersConfig
|
| SetUserSMPServers SMPServersConfig
|
||||||
| TestSMPServer UserId SMPServerWithAuth
|
| APITestSMPServer UserId SMPServerWithAuth
|
||||||
|
| TestSMPServer SMPServerWithAuth
|
||||||
| APISetChatItemTTL UserId (Maybe Int64)
|
| APISetChatItemTTL UserId (Maybe Int64)
|
||||||
| SetChatItemTTL (Maybe Int64)
|
| SetChatItemTTL (Maybe Int64)
|
||||||
| APIGetChatItemTTL UserId
|
| APIGetChatItemTTL UserId
|
||||||
@ -316,7 +318,8 @@ data ChatCommand
|
|||||||
| UpdateGroupNames GroupName GroupProfile
|
| UpdateGroupNames GroupName GroupProfile
|
||||||
| ShowGroupProfile GroupName
|
| ShowGroupProfile GroupName
|
||||||
| UpdateGroupDescription GroupName (Maybe Text)
|
| UpdateGroupDescription GroupName (Maybe Text)
|
||||||
| CreateGroupLink GroupName
|
| CreateGroupLink GroupName GroupMemberRole
|
||||||
|
| GroupLinkMemberRole GroupName GroupMemberRole
|
||||||
| DeleteGroupLink GroupName
|
| DeleteGroupLink GroupName
|
||||||
| ShowGroupLink GroupName
|
| ShowGroupLink GroupName
|
||||||
| SendGroupMessageQuote {groupName :: GroupName, contactName_ :: Maybe ContactName, quotedMsg :: Text, message :: Text}
|
| SendGroupMessageQuote {groupName :: GroupName, contactName_ :: Maybe ContactName, quotedMsg :: Text, message :: Text}
|
||||||
@ -454,8 +457,8 @@ data ChatResponse
|
|||||||
| CRGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
| CRGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||||
| CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember}
|
| CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember}
|
||||||
| CRGroupProfile {user :: User, groupInfo :: GroupInfo}
|
| CRGroupProfile {user :: User, groupInfo :: GroupInfo}
|
||||||
| CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
|
| CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact, memberRole :: GroupMemberRole}
|
||||||
| CRGroupLink {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
|
| CRGroupLink {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact, memberRole :: GroupMemberRole}
|
||||||
| CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo}
|
| CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo}
|
||||||
| CRAcceptingGroupJoinRequest {user :: User, groupInfo :: GroupInfo, contact :: Contact}
|
| CRAcceptingGroupJoinRequest {user :: User, groupInfo :: GroupInfo, contact :: Contact}
|
||||||
| CRMemberSubError {user :: User, groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError}
|
| CRMemberSubError {user :: User, groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError}
|
||||||
@ -684,6 +687,7 @@ data ChatErrorType
|
|||||||
| CEContactDisabled {contact :: Contact}
|
| CEContactDisabled {contact :: Contact}
|
||||||
| CEConnectionDisabled {connection :: Connection}
|
| CEConnectionDisabled {connection :: Connection}
|
||||||
| CEGroupUserRole {groupInfo :: GroupInfo, requiredRole :: GroupMemberRole}
|
| CEGroupUserRole {groupInfo :: GroupInfo, requiredRole :: GroupMemberRole}
|
||||||
|
| CEGroupMemberInitialRole {groupInfo :: GroupInfo, initialRole :: GroupMemberRole}
|
||||||
| CEContactIncognitoCantInvite
|
| CEContactIncognitoCantInvite
|
||||||
| CEGroupIncognitoCantInvite
|
| CEGroupIncognitoCantInvite
|
||||||
| CEGroupContactRole {contactName :: ContactName}
|
| CEGroupContactRole {contactName :: ContactName}
|
||||||
|
@ -6,6 +6,7 @@ module Simplex.Chat.Help
|
|||||||
chatHelpInfo,
|
chatHelpInfo,
|
||||||
filesHelpInfo,
|
filesHelpInfo,
|
||||||
groupsHelpInfo,
|
groupsHelpInfo,
|
||||||
|
contactsHelpInfo,
|
||||||
myAddressHelpInfo,
|
myAddressHelpInfo,
|
||||||
messagesHelpInfo,
|
messagesHelpInfo,
|
||||||
markdownInfo,
|
markdownInfo,
|
||||||
@ -84,7 +85,7 @@ chatHelpInfo =
|
|||||||
green "Create your address: " <> highlight "/address",
|
green "Create your address: " <> highlight "/address",
|
||||||
"",
|
"",
|
||||||
green "Other commands:",
|
green "Other commands:",
|
||||||
indent <> highlight "/help <topic> " <> " - help on: " <> listHighlight ["messages", "files", "groups", "address", "settings"],
|
indent <> highlight "/help <topic> " <> " - help on: " <> listHighlight ["groups", "contacts", "messages", "files", "address", "settings"],
|
||||||
indent <> highlight "/profile " <> " - show / update user profile",
|
indent <> highlight "/profile " <> " - show / update user profile",
|
||||||
indent <> highlight "/delete <contact>" <> " - delete contact and all messages with them",
|
indent <> highlight "/delete <contact>" <> " - delete contact and all messages with them",
|
||||||
indent <> highlight "/chats " <> " - most recent chats",
|
indent <> highlight "/chats " <> " - most recent chats",
|
||||||
@ -103,10 +104,12 @@ filesHelpInfo =
|
|||||||
indent <> highlight "/file @<contact> <file_path> " <> " - send file to contact",
|
indent <> highlight "/file @<contact> <file_path> " <> " - send file to contact",
|
||||||
indent <> highlight "/file #<group> <file_path> " <> " - send file to group",
|
indent <> highlight "/file #<group> <file_path> " <> " - send file to group",
|
||||||
indent <> highlight "/image <name> [<file_path>] " <> " - send file as image to @contact or #group",
|
indent <> highlight "/image <name> [<file_path>] " <> " - send file as image to @contact or #group",
|
||||||
indent <> highlight "/freceive <file_id> [<file_path>]" <> " - accept to receive file",
|
indent <> highlight "/freceive <file_id> [<file_path>] " <> " - accept to receive file",
|
||||||
indent <> highlight "/fforward <name> [<file_id>] " <> " - forward received file to @contact or #group",
|
indent <> highlight "/fforward <name> [<file_id>] " <> " - forward received file to @contact or #group",
|
||||||
indent <> highlight "/fcancel <file_id> " <> " - cancel sending / receiving file",
|
indent <> highlight "/fcancel <file_id> " <> " - cancel sending / receiving file",
|
||||||
indent <> highlight "/fstatus <file_id> " <> " - show file transfer status",
|
indent <> highlight "/fstatus <file_id> " <> " - show file transfer status",
|
||||||
|
indent <> highlight "/imgf @<contact> <file_id> " <> " - forward received image to contact",
|
||||||
|
indent <> highlight "/imgf #<group> <file_id> " <> " - forward received image to group",
|
||||||
"",
|
"",
|
||||||
"The commands may be abbreviated: " <> listHighlight ["/f", "/img", "/fr", "/ff", "/fc", "/fs"]
|
"The commands may be abbreviated: " <> listHighlight ["/f", "/img", "/fr", "/ff", "/fc", "/fs"]
|
||||||
]
|
]
|
||||||
@ -115,31 +118,89 @@ groupsHelpInfo :: [StyledString]
|
|||||||
groupsHelpInfo =
|
groupsHelpInfo =
|
||||||
map
|
map
|
||||||
styleMarkdown
|
styleMarkdown
|
||||||
[ green "Group management commands:",
|
[ green "Group commands:",
|
||||||
indent <> highlight "/group <group> [<full_name>] " <> " - create group",
|
indent <> highlight "/group <group> [<full_name>] " <> " - create group",
|
||||||
indent <> highlight "/add <group> <contact> [<role>]" <> " - add contact to group, roles: " <> highlight "owner" <> ", " <> highlight "admin" <> " (default), " <> highlight "member",
|
indent <> highlight "/add <group> <contact> [<role>] " <> " - add contact to group, roles: " <> highlight "owner" <> ", " <> highlight "admin" <> " (default), " <> highlight "member",
|
||||||
indent <> highlight "/join <group> " <> " - accept group invitation",
|
indent <> highlight "/join <group> " <> " - accept group invitation",
|
||||||
|
indent <> highlight "/members <group> " <> " - list group members",
|
||||||
indent <> highlight "/remove <group> <member> " <> " - remove member from group",
|
indent <> highlight "/remove <group> <member> " <> " - remove member from group",
|
||||||
indent <> highlight "/leave <group> " <> " - leave group",
|
indent <> highlight "/leave <group> " <> " - leave group",
|
||||||
indent <> highlight "/delete <group> " <> " - delete group",
|
indent <> highlight "/clear #<group> " <> " - clear all messages in the group locally",
|
||||||
indent <> highlight "/members <group> " <> " - list group members",
|
indent <> highlight "/delete #<group> " <> " - delete group and all messages",
|
||||||
indent <> highlight "/gp <group> " <> " - view group profile",
|
indent <> highlight "/gp <group> " <> " - view group profile",
|
||||||
indent <> highlight "/gp <group> <new_name> [<full_name>] " <> " - update group profile",
|
indent <> highlight "/gp <group> <name> [<full_name>] " <> " - update group profile names",
|
||||||
indent <> highlight "/group_descr <group> [<descr>] " <> " - update/remove group description",
|
indent <> highlight "/group_descr <group> [<descr>] " <> " - update/remove group description",
|
||||||
indent <> highlight "/groups " <> " - list groups",
|
indent <> highlight "/groups " <> " - list groups",
|
||||||
indent <> highlight "#<group> <message> " <> " - send message to group",
|
indent <> highlight "#<group> <message> " <> " - send message to group",
|
||||||
indent <> highlight "/create link #<group> " <> " - create public group link",
|
"",
|
||||||
|
green "Public group links:",
|
||||||
|
indent <> highlight "/create link #<group> [role] " <> " - create public group link (with optional role, default: member)",
|
||||||
|
indent <> highlight "/set link role #<group> role " <> " - change role assigned to the users joining via the link (member/observer)",
|
||||||
|
indent <> highlight "/show link #<group> " <> " - show public group link and initial member role",
|
||||||
|
indent <> highlight "/delete link #<group> " <> " - delete link to join the group (does NOT delete any members)",
|
||||||
|
"",
|
||||||
|
green "Mute group messages:",
|
||||||
|
indent <> highlight "/mute #<group> " <> " - do not show contact's messages",
|
||||||
|
indent <> highlight "/unmute #<group> " <> " - show contact's messages",
|
||||||
|
"",
|
||||||
|
green "Group member connection and security:",
|
||||||
|
indent <> highlight "/code #<group> <member> " <> " - show member's security code",
|
||||||
|
indent <> highlight "/verify #<group> <member> <code> " <> " - verify member's security code",
|
||||||
|
indent <> highlight "/verify #<group> <member> " <> " - clear security code verification",
|
||||||
|
indent <> highlight "/info #<group> <member> " <> " - info about member connection",
|
||||||
|
indent <> highlight "/switch #<group> <member> " <> " - switch receiving messages to another SMP relay",
|
||||||
|
"",
|
||||||
|
green "Group chat preferences:",
|
||||||
|
indent <> highlight "/set voice #<group> on/off " <> " - enable/disable voice messages",
|
||||||
|
indent <> highlight "/set delete #<group> on/off " <> " - enable/disable full message deletion",
|
||||||
|
indent <> highlight "/set direct #<group> on/off " <> " - enable/disable direct messages to other members",
|
||||||
|
indent <> highlight "/set disappear #<group> on <time> " <> " - enable disappearing messages with <time>:",
|
||||||
|
indent <> highlight " " <> " 30s, 5min, 1h, 8h, day, week, month",
|
||||||
|
indent <> highlight "/set disappear #<group> off " <> " - disable disappearing messages",
|
||||||
"",
|
"",
|
||||||
"The commands may be abbreviated: " <> listHighlight ["/g", "/a", "/j", "/rm", "/l", "/d", "/ms", "/gs"]
|
"The commands may be abbreviated: " <> listHighlight ["/g", "/a", "/j", "/rm", "/l", "/d", "/ms", "/gs"]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
contactsHelpInfo :: [StyledString]
|
||||||
|
contactsHelpInfo =
|
||||||
|
map
|
||||||
|
styleMarkdown
|
||||||
|
[ green "Contact commands:",
|
||||||
|
indent <> highlight "/contacts " <> " - list contacts",
|
||||||
|
indent <> highlight "/clear @<name> " <> " - clear all messages with the contact locally",
|
||||||
|
indent <> highlight "/delete @<name> " <> " - delete contact and all messages",
|
||||||
|
"",
|
||||||
|
green "Mute contact messages:",
|
||||||
|
indent <> highlight "/mute @<name> " <> " - do not show contact's messages",
|
||||||
|
indent <> highlight "/unmute @<name> " <> " - show contact's messages",
|
||||||
|
"",
|
||||||
|
green "Contact connection and security:",
|
||||||
|
indent <> highlight "/code @<name> " <> " - show contact's security code",
|
||||||
|
indent <> highlight "/verify @<name> <code> " <> " - verify contact's security code",
|
||||||
|
indent <> highlight "/verify @<name> " <> " - clear security code verification",
|
||||||
|
indent <> highlight "/info @<name> " <> " - info about contact connection",
|
||||||
|
indent <> highlight "/switch @<name> " <> " - switch receiving messages to another SMP relay",
|
||||||
|
"",
|
||||||
|
green "Contact chat preferences:",
|
||||||
|
indent <> highlight "/set voice @<name> yes/no/always " <> " - allow/prohibit voice messages with the contact",
|
||||||
|
indent <> highlight "/set voice @<name> " <> " - reset voice messages to user's default",
|
||||||
|
indent <> highlight "/set delete @<name> yes/no/always " <> " - allow/prohibit full message deletion",
|
||||||
|
indent <> highlight "/set delete @<name> " <> " - reset full deletion to user's default",
|
||||||
|
indent <> highlight "/set disappear @<name> yes <time> " <> " - enable disappearing messages with <time>:",
|
||||||
|
indent <> highlight " " <> " 30s, 5min, 1h, 8h, day, week, month",
|
||||||
|
indent <> highlight "/set disappear @<name> yes " <> " - enable disappearing messages with offered time",
|
||||||
|
indent <> highlight "/set disappear @<name> no " <> " - disable disappearing messages",
|
||||||
|
"",
|
||||||
|
"The commands may be abbreviated: " <> listHighlight ["/d", "/i"]
|
||||||
|
]
|
||||||
|
|
||||||
myAddressHelpInfo :: [StyledString]
|
myAddressHelpInfo :: [StyledString]
|
||||||
myAddressHelpInfo =
|
myAddressHelpInfo =
|
||||||
map
|
map
|
||||||
styleMarkdown
|
styleMarkdown
|
||||||
[ green "Your contact address commands:",
|
[ green "Your contact address commands:",
|
||||||
indent <> highlight "/address " <> " - create your address",
|
indent <> highlight "/address " <> " - create your address",
|
||||||
indent <> highlight "/delete_address" <> " - delete your address (accepted contacts will remain connected)",
|
indent <> highlight "/delete_address " <> " - delete your address (accepted contacts will remain connected)",
|
||||||
indent <> highlight "/show_address " <> " - show your address",
|
indent <> highlight "/show_address " <> " - show your address",
|
||||||
indent <> highlight "/accept <name> " <> " - accept contact request",
|
indent <> highlight "/accept <name> " <> " - accept contact request",
|
||||||
indent <> highlight "/reject <name> " <> " - reject contact request",
|
indent <> highlight "/reject <name> " <> " - reject contact request",
|
||||||
@ -158,7 +219,7 @@ messagesHelpInfo =
|
|||||||
indent <> highlight "/chats all " <> " - all conversations",
|
indent <> highlight "/chats all " <> " - all conversations",
|
||||||
"",
|
"",
|
||||||
green "Show recent messages",
|
green "Show recent messages",
|
||||||
indent <> highlight "/tail @alice [N]" <> " - the last N messages with alice (10 by default)",
|
indent <> highlight "/tail @alice [N] " <> " - the last N messages with alice (10 by default)",
|
||||||
indent <> highlight "/tail #team [N] " <> " - the last N messages in the group team",
|
indent <> highlight "/tail #team [N] " <> " - the last N messages in the group team",
|
||||||
indent <> highlight "/tail [N] " <> " - the last N messages in all chats",
|
indent <> highlight "/tail [N] " <> " - the last N messages in all chats",
|
||||||
"",
|
"",
|
||||||
@ -205,10 +266,13 @@ settingsInfo =
|
|||||||
map
|
map
|
||||||
styleMarkdown
|
styleMarkdown
|
||||||
[ green "Chat settings:",
|
[ green "Chat settings:",
|
||||||
|
indent <> highlight "/incognito on/off " <> " - enable/disable incognito mode",
|
||||||
indent <> highlight "/network " <> " - show / set network access options",
|
indent <> highlight "/network " <> " - show / set network access options",
|
||||||
indent <> highlight "/smp " <> " - show / set custom SMP servers",
|
indent <> highlight "/smp " <> " - show / set custom SMP servers",
|
||||||
indent <> highlight "/info <contact> " <> " - information about contact connection",
|
indent <> highlight "/info <contact> " <> " - information about contact connection",
|
||||||
indent <> highlight "/info #<group> <member> " <> " - information about member connection",
|
indent <> highlight "/info #<group> <member> " <> " - information about member connection",
|
||||||
indent <> highlight "/(un)mute <contact> " <> " - (un)mute contact, the last messages can be printed with /tail command",
|
indent <> highlight "/(un)mute <contact> " <> " - (un)mute contact, the last messages can be printed with /tail command",
|
||||||
indent <> highlight "/(un)mute #<group> " <> " - (un)mute group"
|
indent <> highlight "/(un)mute #<group> " <> " - (un)mute group",
|
||||||
|
indent <> highlight "/get stats " <> " - get usage statistics",
|
||||||
|
indent <> highlight "/reset stats " <> " - reset usage statistics"
|
||||||
]
|
]
|
||||||
|
12
src/Simplex/Chat/Migrations/M20230303_group_link_role.hs
Normal file
12
src/Simplex/Chat/Migrations/M20230303_group_link_role.hs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
module Simplex.Chat.Migrations.M20230303_group_link_role where
|
||||||
|
|
||||||
|
import Database.SQLite.Simple (Query)
|
||||||
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
|
|
||||||
|
m20230303_group_link_role :: Query
|
||||||
|
m20230303_group_link_role =
|
||||||
|
[sql|
|
||||||
|
ALTER TABLE user_contact_links ADD COLUMN group_link_member_role TEXT NULL; -- member or observer
|
||||||
|
|]
|
@ -282,6 +282,7 @@ CREATE TABLE user_contact_links(
|
|||||||
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
|
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
|
||||||
auto_accept_incognito INTEGER DEFAULT 0 CHECK(auto_accept_incognito NOT NULL),
|
auto_accept_incognito INTEGER DEFAULT 0 CHECK(auto_accept_incognito NOT NULL),
|
||||||
group_link_id BLOB,
|
group_link_id BLOB,
|
||||||
|
group_link_member_role TEXT NULL,
|
||||||
UNIQUE(user_id, local_display_name)
|
UNIQUE(user_id, local_display_name)
|
||||||
);
|
);
|
||||||
CREATE TABLE contact_requests(
|
CREATE TABLE contact_requests(
|
||||||
|
@ -75,6 +75,7 @@ module Simplex.Chat.Store
|
|||||||
deleteGroupLink,
|
deleteGroupLink,
|
||||||
getGroupLink,
|
getGroupLink,
|
||||||
getGroupLinkId,
|
getGroupLinkId,
|
||||||
|
setGroupLinkMemberRole,
|
||||||
createOrUpdateContactRequest,
|
createOrUpdateContactRequest,
|
||||||
getContactRequest',
|
getContactRequest',
|
||||||
getContactRequest,
|
getContactRequest,
|
||||||
@ -258,7 +259,6 @@ module Simplex.Chat.Store
|
|||||||
where
|
where
|
||||||
|
|
||||||
import Control.Applicative ((<|>))
|
import Control.Applicative ((<|>))
|
||||||
import Control.Concurrent.STM (stateTVar)
|
|
||||||
import Control.Exception (Exception)
|
import Control.Exception (Exception)
|
||||||
import qualified Control.Exception as E
|
import qualified Control.Exception as E
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
@ -341,6 +341,7 @@ import Simplex.Chat.Migrations.M20230117_fkey_indexes
|
|||||||
import Simplex.Chat.Migrations.M20230118_recreate_smp_servers
|
import Simplex.Chat.Migrations.M20230118_recreate_smp_servers
|
||||||
import Simplex.Chat.Migrations.M20230129_drop_chat_items_group_idx
|
import Simplex.Chat.Migrations.M20230129_drop_chat_items_group_idx
|
||||||
import Simplex.Chat.Migrations.M20230206_item_deleted_by_group_member_id
|
import Simplex.Chat.Migrations.M20230206_item_deleted_by_group_member_id
|
||||||
|
import Simplex.Chat.Migrations.M20230303_group_link_role
|
||||||
import Simplex.Chat.Protocol
|
import Simplex.Chat.Protocol
|
||||||
import Simplex.Chat.Types
|
import Simplex.Chat.Types
|
||||||
import Simplex.Chat.Util (week)
|
import Simplex.Chat.Util (week)
|
||||||
@ -406,7 +407,8 @@ schemaMigrations =
|
|||||||
("20230117_fkey_indexes", m20230117_fkey_indexes),
|
("20230117_fkey_indexes", m20230117_fkey_indexes),
|
||||||
("20230118_recreate_smp_servers", m20230118_recreate_smp_servers),
|
("20230118_recreate_smp_servers", m20230118_recreate_smp_servers),
|
||||||
("20230129_drop_chat_items_group_idx", m20230129_drop_chat_items_group_idx),
|
("20230129_drop_chat_items_group_idx", m20230129_drop_chat_items_group_idx),
|
||||||
("20230206_item_deleted_by_group_member_id", m20230206_item_deleted_by_group_member_id)
|
("20230206_item_deleted_by_group_member_id", m20230206_item_deleted_by_group_member_id),
|
||||||
|
("20230303_group_link_role", m20230303_group_link_role)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | The list of migrations in ascending order by date
|
-- | The list of migrations in ascending order by date
|
||||||
@ -1086,13 +1088,13 @@ getUserAddress db User {userId} =
|
|||||||
|]
|
|]
|
||||||
(Only userId)
|
(Only userId)
|
||||||
|
|
||||||
getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> IO (Maybe (UserContactLink, Maybe GroupId))
|
getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> IO (Maybe (UserContactLink, Maybe GroupId, GroupMemberRole))
|
||||||
getUserContactLinkById db userId userContactLinkId =
|
getUserContactLinkById db userId userContactLinkId =
|
||||||
maybeFirstRow (\(ucl :. Only groupId_) -> (toUserContactLink ucl, groupId_)) $
|
maybeFirstRow (\(ucl :. (groupId_, mRole_)) -> (toUserContactLink ucl, groupId_, fromMaybe GRMember mRole_)) $
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content, group_id
|
SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role
|
||||||
FROM user_contact_links
|
FROM user_contact_links
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND user_contact_link_id = ?
|
AND user_contact_link_id = ?
|
||||||
@ -1117,14 +1119,14 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do
|
|||||||
Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply)
|
Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply)
|
||||||
_ -> (False, False, Nothing)
|
_ -> (False, False, Nothing)
|
||||||
|
|
||||||
createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> ExceptT StoreError IO ()
|
createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> GroupMemberRole -> ExceptT StoreError IO ()
|
||||||
createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq groupLinkId =
|
createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq groupLinkId memberRole =
|
||||||
checkConstraint (SEDuplicateGroupLink groupInfo) . liftIO $ do
|
checkConstraint (SEDuplicateGroupLink groupInfo) . liftIO $ do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
"INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
|
"INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
|
||||||
(userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, True, currentTs, currentTs)
|
(userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs)
|
||||||
userContactLinkId <- insertedRowId db
|
userContactLinkId <- insertedRowId db
|
||||||
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing Nothing 0 currentTs
|
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing Nothing 0 currentTs
|
||||||
|
|
||||||
@ -1182,16 +1184,22 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do
|
|||||||
(userId, groupId)
|
(userId, groupId)
|
||||||
DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND group_id = ?" (userId, groupId)
|
DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND group_id = ?" (userId, groupId)
|
||||||
|
|
||||||
getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO ConnReqContact
|
getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO (Int64, ConnReqContact, GroupMemberRole)
|
||||||
getGroupLink db User {userId} gInfo@GroupInfo {groupId} =
|
getGroupLink db User {userId} gInfo@GroupInfo {groupId} =
|
||||||
ExceptT . firstRow fromOnly (SEGroupLinkNotFound gInfo) $
|
ExceptT . firstRow groupLink (SEGroupLinkNotFound gInfo) $
|
||||||
DB.query db "SELECT conn_req_contact FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
|
DB.query db "SELECT user_contact_link_id, conn_req_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
|
||||||
|
where
|
||||||
|
groupLink (linkId, cReq, mRole_) = (linkId, cReq, fromMaybe GRMember mRole_)
|
||||||
|
|
||||||
getGroupLinkId :: DB.Connection -> User -> GroupInfo -> IO (Maybe GroupLinkId)
|
getGroupLinkId :: DB.Connection -> User -> GroupInfo -> IO (Maybe GroupLinkId)
|
||||||
getGroupLinkId db User {userId} GroupInfo {groupId} =
|
getGroupLinkId db User {userId} GroupInfo {groupId} =
|
||||||
fmap join . maybeFirstRow fromOnly $
|
fmap join . maybeFirstRow fromOnly $
|
||||||
DB.query db "SELECT group_link_id FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
|
DB.query db "SELECT group_link_id FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
|
||||||
|
|
||||||
|
setGroupLinkMemberRole :: DB.Connection -> User -> Int64 -> GroupMemberRole -> IO ()
|
||||||
|
setGroupLinkMemberRole db User {userId} userContactLinkId memberRole =
|
||||||
|
DB.execute db "UPDATE user_contact_links SET group_link_member_role = ? WHERE user_id = ? AND user_contact_link_id = ?" (memberRole, userId, userContactLinkId)
|
||||||
|
|
||||||
createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest
|
createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest
|
||||||
createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profile {displayName, fullName, image, preferences} xContactId_ =
|
createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profile {displayName, fullName, image, preferences} xContactId_ =
|
||||||
liftIO (maybeM getContact' xContactId_) >>= \case
|
liftIO (maybeM getContact' xContactId_) >>= \case
|
||||||
@ -1583,8 +1591,17 @@ matchSentProbe db user@User {userId} _from@Contact {contactId} (Probe probe) = d
|
|||||||
cId : _ -> eitherToMaybe <$> runExceptT (getContact db user cId)
|
cId : _ -> eitherToMaybe <$> runExceptT (getContact db user cId)
|
||||||
|
|
||||||
mergeContactRecords :: DB.Connection -> UserId -> Contact -> Contact -> IO ()
|
mergeContactRecords :: DB.Connection -> UserId -> Contact -> Contact -> IO ()
|
||||||
mergeContactRecords db userId Contact {contactId = toContactId} Contact {contactId = fromContactId, localDisplayName} = do
|
mergeContactRecords db userId ct1 ct2 = do
|
||||||
|
let (toCt, fromCt) = toFromContacts ct1 ct2
|
||||||
|
Contact {contactId = toContactId} = toCt
|
||||||
|
Contact {contactId = fromContactId, localDisplayName} = fromCt
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
|
-- TODO next query fixes incorrect unused contacts deletion; consider more thorough fix
|
||||||
|
when (contactDirect toCt && not (contactUsed toCt)) $
|
||||||
|
DB.execute
|
||||||
|
db
|
||||||
|
"UPDATE contacts SET contact_used = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?"
|
||||||
|
(currentTs, userId, toContactId)
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
"UPDATE connections SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
|
"UPDATE connections SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
|
||||||
@ -1620,6 +1637,17 @@ mergeContactRecords db userId Contact {contactId = toContactId} Contact {contact
|
|||||||
deleteContactProfile_ db userId fromContactId
|
deleteContactProfile_ db userId fromContactId
|
||||||
DB.execute db "DELETE FROM contacts WHERE contact_id = ? AND user_id = ?" (fromContactId, userId)
|
DB.execute db "DELETE FROM contacts WHERE contact_id = ? AND user_id = ?" (fromContactId, userId)
|
||||||
DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId)
|
DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId)
|
||||||
|
where
|
||||||
|
toFromContacts :: Contact -> Contact -> (Contact, Contact)
|
||||||
|
toFromContacts c1 c2
|
||||||
|
| d1 && not d2 = (c1, c2)
|
||||||
|
| d2 && not d1 = (c2, c1)
|
||||||
|
| ctCreatedAt c1 <= ctCreatedAt c2 = (c1, c2)
|
||||||
|
| otherwise = (c2, c1)
|
||||||
|
where
|
||||||
|
d1 = directOrUsed c1
|
||||||
|
d2 = directOrUsed c2
|
||||||
|
ctCreatedAt Contact {createdAt} = createdAt
|
||||||
|
|
||||||
getConnectionEntity :: DB.Connection -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity
|
getConnectionEntity :: DB.Connection -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity
|
||||||
getConnectionEntity db user@User {userId, userContactId} agentConnId = do
|
getConnectionEntity db user@User {userId, userContactId} agentConnId = do
|
||||||
|
@ -160,9 +160,12 @@ contactConnId = aConnId . contactConn
|
|||||||
contactConnIncognito :: Contact -> Bool
|
contactConnIncognito :: Contact -> Bool
|
||||||
contactConnIncognito = connIncognito . contactConn
|
contactConnIncognito = connIncognito . contactConn
|
||||||
|
|
||||||
|
contactDirect :: Contact -> Bool
|
||||||
|
contactDirect Contact {activeConn = Connection {connLevel, viaGroupLink}} = connLevel == 0 && not viaGroupLink
|
||||||
|
|
||||||
directOrUsed :: Contact -> Bool
|
directOrUsed :: Contact -> Bool
|
||||||
directOrUsed Contact {contactUsed, activeConn = Connection {connLevel, viaGroupLink}} =
|
directOrUsed ct@Contact {contactUsed} =
|
||||||
(connLevel == 0 && not viaGroupLink) || contactUsed
|
contactDirect ct || contactUsed
|
||||||
|
|
||||||
anyDirectOrUsed :: Contact -> Bool
|
anyDirectOrUsed :: Contact -> Bool
|
||||||
anyDirectOrUsed Contact {contactUsed, activeConn = Connection {connLevel}} = connLevel == 0 || contactUsed
|
anyDirectOrUsed Contact {contactUsed, activeConn = Connection {connLevel}} = connLevel == 0 || contactUsed
|
||||||
|
@ -94,6 +94,7 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
|
|||||||
HSMain -> chatHelpInfo
|
HSMain -> chatHelpInfo
|
||||||
HSFiles -> filesHelpInfo
|
HSFiles -> filesHelpInfo
|
||||||
HSGroups -> groupsHelpInfo
|
HSGroups -> groupsHelpInfo
|
||||||
|
HSContacts -> contactsHelpInfo
|
||||||
HSMyAddress -> myAddressHelpInfo
|
HSMyAddress -> myAddressHelpInfo
|
||||||
HSMessages -> messagesHelpInfo
|
HSMessages -> messagesHelpInfo
|
||||||
HSMarkdown -> markdownInfo
|
HSMarkdown -> markdownInfo
|
||||||
@ -185,8 +186,8 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
|
|||||||
CRGroupDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> groupName' g) <> " to delete the local copy of the group"]
|
CRGroupDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> groupName' g) <> " to delete the local copy of the group"]
|
||||||
CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m
|
CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m
|
||||||
CRGroupProfile u g -> ttyUser u $ viewGroupProfile g
|
CRGroupProfile u g -> ttyUser u $ viewGroupProfile g
|
||||||
CRGroupLinkCreated u g cReq -> ttyUser u $ groupLink_ "Group link is created!" g cReq
|
CRGroupLinkCreated u g cReq mRole -> ttyUser u $ groupLink_ "Group link is created!" g cReq mRole
|
||||||
CRGroupLink u g cReq -> ttyUser u $ groupLink_ "Group link:" g cReq
|
CRGroupLink u g cReq mRole -> ttyUser u $ groupLink_ "Group link:" g cReq mRole
|
||||||
CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g
|
CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g
|
||||||
CRAcceptingGroupJoinRequest _ g c -> [ttyFullContact c <> ": accepting request to join group " <> ttyGroup' g <> "..."]
|
CRAcceptingGroupJoinRequest _ g c -> [ttyFullContact c <> ": accepting request to join group " <> ttyGroup' g <> "..."]
|
||||||
CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e]
|
CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e]
|
||||||
@ -540,13 +541,13 @@ autoAcceptStatus_ = \case
|
|||||||
maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply
|
maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply
|
||||||
_ -> ["auto_accept off"]
|
_ -> ["auto_accept off"]
|
||||||
|
|
||||||
groupLink_ :: StyledString -> GroupInfo -> ConnReqContact -> [StyledString]
|
groupLink_ :: StyledString -> GroupInfo -> ConnReqContact -> GroupMemberRole -> [StyledString]
|
||||||
groupLink_ intro g cReq =
|
groupLink_ intro g cReq mRole =
|
||||||
[ intro,
|
[ intro,
|
||||||
"",
|
"",
|
||||||
(plain . strEncode) cReq,
|
(plain . strEncode) cReq,
|
||||||
"",
|
"",
|
||||||
"Anybody can connect to you and join group with: " <> highlight' "/c <group_link_above>",
|
"Anybody can connect to you and join group as " <> showRole mRole <> " with: " <> highlight' "/c <group_link_above>",
|
||||||
"to show it again: " <> highlight ("/show link #" <> groupName' g),
|
"to show it again: " <> highlight ("/show link #" <> groupName' g),
|
||||||
"to delete it: " <> highlight ("/delete link #" <> groupName' g) <> " (joined members will remain connected to you)"
|
"to delete it: " <> highlight ("/delete link #" <> groupName' g) <> " (joined members will remain connected to you)"
|
||||||
]
|
]
|
||||||
@ -1224,6 +1225,7 @@ viewChatError logLevel = \case
|
|||||||
(: []) . (ttyGroup' g <>) $ case role of
|
(: []) . (ttyGroup' g <>) $ case role of
|
||||||
GRAuthor -> ": you don't have permission to send messages"
|
GRAuthor -> ": you don't have permission to send messages"
|
||||||
_ -> ": you have insufficient permissions for this action, the required role is " <> plain (strEncode role)
|
_ -> ": you have insufficient permissions for this action, the required role is " <> plain (strEncode role)
|
||||||
|
CEGroupMemberInitialRole g role -> [ttyGroup' g <> ": initial role for group member cannot be " <> plain (strEncode role) <> ", use member or observer"]
|
||||||
CEContactIncognitoCantInvite -> ["you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"]
|
CEContactIncognitoCantInvite -> ["you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"]
|
||||||
CEGroupIncognitoCantInvite -> ["you've connected to this group using an incognito profile - prohibited to invite contacts"]
|
CEGroupIncognitoCantInvite -> ["you've connected to this group using an incognito profile - prohibited to invite contacts"]
|
||||||
CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"]
|
CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"]
|
||||||
|
@ -405,14 +405,14 @@ testTestSMPServerConnection :: HasCallStack => FilePath -> IO ()
|
|||||||
testTestSMPServerConnection =
|
testTestSMPServerConnection =
|
||||||
testChat2 aliceProfile bobProfile $
|
testChat2 aliceProfile bobProfile $
|
||||||
\alice _ -> do
|
\alice _ -> do
|
||||||
alice ##> "/smp test 1 smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001"
|
alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001"
|
||||||
alice <## "SMP server test passed"
|
alice <## "SMP server test passed"
|
||||||
-- to test with password:
|
-- to test with password:
|
||||||
-- alice <## "SMP server test failed at CreateQueue, error: SMP AUTH"
|
-- alice <## "SMP server test failed at CreateQueue, error: SMP AUTH"
|
||||||
-- alice <## "Server requires authorization to create queues, check password"
|
-- alice <## "Server requires authorization to create queues, check password"
|
||||||
alice ##> "/smp test 1 smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"
|
alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"
|
||||||
alice <## "SMP server test passed"
|
alice <## "SMP server test passed"
|
||||||
alice ##> "/smp test 1 smp://LcJU@localhost:7001"
|
alice ##> "/smp test smp://LcJU@localhost:7001"
|
||||||
alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:7001 NETWORK"
|
alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:7001 NETWORK"
|
||||||
alice <## "Possibly, certificate fingerprint in server address is incorrect"
|
alice <## "Possibly, certificate fingerprint in server address is incorrect"
|
||||||
|
|
||||||
|
@ -46,6 +46,8 @@ chatGroupTests = do
|
|||||||
it "create group link, join via group link - incognito membership" testGroupLinkIncognitoMembership
|
it "create group link, join via group link - incognito membership" testGroupLinkIncognitoMembership
|
||||||
it "unused host contact is deleted after all groups with it are deleted" testGroupLinkUnusedHostContactDeleted
|
it "unused host contact is deleted after all groups with it are deleted" testGroupLinkUnusedHostContactDeleted
|
||||||
it "leaving groups with unused host contacts deletes incognito profiles" testGroupLinkIncognitoUnusedHostContactsDeleted
|
it "leaving groups with unused host contacts deletes incognito profiles" testGroupLinkIncognitoUnusedHostContactsDeleted
|
||||||
|
it "group link member role" testGroupLinkMemberRole
|
||||||
|
it "leaving and deleting the group joined via link should NOT delete previously existing direct contacts" testGroupLinkLeaveDelete
|
||||||
|
|
||||||
testGroup :: HasCallStack => SpecWith FilePath
|
testGroup :: HasCallStack => SpecWith FilePath
|
||||||
testGroup = versionTestMatrix3 runTestGroup
|
testGroup = versionTestMatrix3 runTestGroup
|
||||||
@ -127,28 +129,27 @@ testGroupShared alice bob cath checkMessages = do
|
|||||||
alice <## "bob (Bob)"
|
alice <## "bob (Bob)"
|
||||||
alice <## "cath (Catherine)"
|
alice <## "cath (Catherine)"
|
||||||
-- test observer role
|
-- test observer role
|
||||||
-- to be enabled once the role is enabled in parser
|
alice ##> "/mr team bob observer"
|
||||||
-- alice ##> "/mr team bob observer"
|
concurrentlyN_
|
||||||
-- concurrentlyN_
|
[ alice <## "#team: you changed the role of bob from admin to observer",
|
||||||
-- [ alice <## "#team: you changed the role of bob from admin to observer",
|
bob <## "#team: alice changed your role from admin to observer",
|
||||||
-- bob <## "#team: alice changed your role from admin to observer",
|
cath <## "#team: alice changed the role of bob from admin to observer"
|
||||||
-- cath <## "#team: alice changed the role of bob from admin to observer"
|
]
|
||||||
-- ]
|
bob ##> "#team hello"
|
||||||
-- bob ##> "#team hello"
|
bob <## "#team: you don't have permission to send messages"
|
||||||
-- bob <## "#team: you don't have permission to send messages to this group"
|
bob ##> "/rm team cath"
|
||||||
-- bob ##> "/rm team cath"
|
bob <## "#team: you have insufficient permissions for this action, the required role is admin"
|
||||||
-- bob <## "#team: you have insufficient permissions for this action, the required role is admin"
|
cath #> "#team hello"
|
||||||
-- cath #> "#team hello"
|
concurrentlyN_
|
||||||
-- concurrentlyN_
|
[ alice <# "#team cath> hello",
|
||||||
-- [ alice <# "#team cath> hello",
|
bob <# "#team cath> hello"
|
||||||
-- bob <# "#team cath> hello"
|
]
|
||||||
-- ]
|
alice ##> "/mr team bob admin"
|
||||||
-- alice ##> "/mr team bob admin"
|
concurrentlyN_
|
||||||
-- concurrentlyN_
|
[ alice <## "#team: you changed the role of bob from observer to admin",
|
||||||
-- [ alice <## "#team: you changed the role of bob from observer to admin",
|
bob <## "#team: alice changed your role from observer to admin",
|
||||||
-- bob <## "#team: alice changed your role from observer to admin",
|
cath <## "#team: alice changed the role of bob from observer to admin"
|
||||||
-- cath <## "#team: alice changed the role of bob from observer to admin"
|
]
|
||||||
-- ]
|
|
||||||
-- remove member
|
-- remove member
|
||||||
bob ##> "/rm team cath"
|
bob ##> "/rm team cath"
|
||||||
concurrentlyN_
|
concurrentlyN_
|
||||||
@ -1423,14 +1424,14 @@ testGroupLink =
|
|||||||
alice ##> "/show link #team"
|
alice ##> "/show link #team"
|
||||||
alice <## "no group link, to create: /create link #team"
|
alice <## "no group link, to create: /create link #team"
|
||||||
alice ##> "/create link #team"
|
alice ##> "/create link #team"
|
||||||
_ <- getGroupLink alice "team" True
|
_ <- getGroupLink alice "team" GRMember True
|
||||||
alice ##> "/delete link #team"
|
alice ##> "/delete link #team"
|
||||||
alice <## "Group link is deleted - joined members will remain connected."
|
alice <## "Group link is deleted - joined members will remain connected."
|
||||||
alice <## "To create a new group link use /create link #team"
|
alice <## "To create a new group link use /create link #team"
|
||||||
alice ##> "/create link #team"
|
alice ##> "/create link #team"
|
||||||
gLink <- getGroupLink alice "team" True
|
gLink <- getGroupLink alice "team" GRMember True
|
||||||
alice ##> "/show link #team"
|
alice ##> "/show link #team"
|
||||||
_ <- getGroupLink alice "team" False
|
_ <- getGroupLink alice "team" GRMember False
|
||||||
alice ##> "/create link #team"
|
alice ##> "/create link #team"
|
||||||
alice <## "you already have link for this group, to show: /show link #team"
|
alice <## "you already have link for this group, to show: /show link #team"
|
||||||
bob ##> ("/c " <> gLink)
|
bob ##> ("/c " <> gLink)
|
||||||
@ -1522,7 +1523,7 @@ testGroupLinkDeleteGroupRejoin =
|
|||||||
alice <## "group #team is created"
|
alice <## "group #team is created"
|
||||||
alice <## "to add members use /a team <name> or /create link #team"
|
alice <## "to add members use /a team <name> or /create link #team"
|
||||||
alice ##> "/create link #team"
|
alice ##> "/create link #team"
|
||||||
gLink <- getGroupLink alice "team" True
|
gLink <- getGroupLink alice "team" GRMember True
|
||||||
bob ##> ("/c " <> gLink)
|
bob ##> ("/c " <> gLink)
|
||||||
bob <## "connection request sent!"
|
bob <## "connection request sent!"
|
||||||
alice <## "bob (Bob): accepting request to join group #team..."
|
alice <## "bob (Bob): accepting request to join group #team..."
|
||||||
@ -1578,7 +1579,7 @@ testGroupLinkContactUsed =
|
|||||||
alice <## "group #team is created"
|
alice <## "group #team is created"
|
||||||
alice <## "to add members use /a team <name> or /create link #team"
|
alice <## "to add members use /a team <name> or /create link #team"
|
||||||
alice ##> "/create link #team"
|
alice ##> "/create link #team"
|
||||||
gLink <- getGroupLink alice "team" True
|
gLink <- getGroupLink alice "team" GRMember True
|
||||||
bob ##> ("/c " <> gLink)
|
bob ##> ("/c " <> gLink)
|
||||||
bob <## "connection request sent!"
|
bob <## "connection request sent!"
|
||||||
alice <## "bob (Bob): accepting request to join group #team..."
|
alice <## "bob (Bob): accepting request to join group #team..."
|
||||||
@ -1638,7 +1639,7 @@ testGroupLinkIncognitoMembership =
|
|||||||
(bob <## ("#team: you joined the group incognito as " <> bobIncognito))
|
(bob <## ("#team: you joined the group incognito as " <> bobIncognito))
|
||||||
-- bob creates group link, cath joins
|
-- bob creates group link, cath joins
|
||||||
bob ##> "/create link #team"
|
bob ##> "/create link #team"
|
||||||
gLink <- getGroupLink bob "team" True
|
gLink <- getGroupLink bob "team" GRMember True
|
||||||
cath ##> ("/c " <> gLink)
|
cath ##> ("/c " <> gLink)
|
||||||
cath <## "connection request sent!"
|
cath <## "connection request sent!"
|
||||||
bob <## "cath (Catherine): accepting request to join group #team..."
|
bob <## "cath (Catherine): accepting request to join group #team..."
|
||||||
@ -1729,7 +1730,7 @@ testGroupLinkUnusedHostContactDeleted =
|
|||||||
alice <## "group #team is created"
|
alice <## "group #team is created"
|
||||||
alice <## "to add members use /a team <name> or /create link #team"
|
alice <## "to add members use /a team <name> or /create link #team"
|
||||||
alice ##> "/create link #team"
|
alice ##> "/create link #team"
|
||||||
gLinkTeam <- getGroupLink alice "team" True
|
gLinkTeam <- getGroupLink alice "team" GRMember True
|
||||||
bob ##> ("/c " <> gLinkTeam)
|
bob ##> ("/c " <> gLinkTeam)
|
||||||
bob <## "connection request sent!"
|
bob <## "connection request sent!"
|
||||||
alice <## "bob (Bob): accepting request to join group #team..."
|
alice <## "bob (Bob): accepting request to join group #team..."
|
||||||
@ -1747,7 +1748,7 @@ testGroupLinkUnusedHostContactDeleted =
|
|||||||
alice <## "group #club is created"
|
alice <## "group #club is created"
|
||||||
alice <## "to add members use /a club <name> or /create link #club"
|
alice <## "to add members use /a club <name> or /create link #club"
|
||||||
alice ##> "/create link #club"
|
alice ##> "/create link #club"
|
||||||
gLinkClub <- getGroupLink alice "club" True
|
gLinkClub <- getGroupLink alice "club" GRMember True
|
||||||
bob ##> ("/c " <> gLinkClub)
|
bob ##> ("/c " <> gLinkClub)
|
||||||
bob <## "connection request sent!"
|
bob <## "connection request sent!"
|
||||||
alice <## "bob_1 (Bob): accepting request to join group #club..."
|
alice <## "bob_1 (Bob): accepting request to join group #club..."
|
||||||
@ -1822,7 +1823,7 @@ testGroupLinkIncognitoUnusedHostContactsDeleted =
|
|||||||
alice <## ("group #" <> group <> " is created")
|
alice <## ("group #" <> group <> " is created")
|
||||||
alice <## ("to add members use /a " <> group <> " <name> or /create link #" <> group)
|
alice <## ("to add members use /a " <> group <> " <name> or /create link #" <> group)
|
||||||
alice ##> ("/create link #" <> group)
|
alice ##> ("/create link #" <> group)
|
||||||
gLinkTeam <- getGroupLink alice group True
|
gLinkTeam <- getGroupLink alice group GRMember True
|
||||||
bob ##> ("/c " <> gLinkTeam)
|
bob ##> ("/c " <> gLinkTeam)
|
||||||
bobIncognito <- getTermLine bob
|
bobIncognito <- getTermLine bob
|
||||||
bob <## "connection request sent incognito!"
|
bob <## "connection request sent incognito!"
|
||||||
@ -1850,3 +1851,138 @@ testGroupLinkIncognitoUnusedHostContactsDeleted =
|
|||||||
]
|
]
|
||||||
bob ##> ("/d #" <> group)
|
bob ##> ("/d #" <> group)
|
||||||
bob <## ("#" <> group <> ": you deleted the group")
|
bob <## ("#" <> group <> ": you deleted the group")
|
||||||
|
|
||||||
|
testGroupLinkMemberRole :: HasCallStack => FilePath -> IO ()
|
||||||
|
testGroupLinkMemberRole =
|
||||||
|
testChat3 aliceProfile bobProfile cathProfile $
|
||||||
|
\alice bob cath -> do
|
||||||
|
alice ##> "/g team"
|
||||||
|
alice <## "group #team is created"
|
||||||
|
alice <## "to add members use /a team <name> or /create link #team"
|
||||||
|
alice ##> "/create link #team admin"
|
||||||
|
alice <## "#team: initial role for group member cannot be admin, use member or observer"
|
||||||
|
alice ##> "/create link #team observer"
|
||||||
|
gLink <- getGroupLink alice "team" GRObserver True
|
||||||
|
bob ##> ("/c " <> gLink)
|
||||||
|
bob <## "connection request sent!"
|
||||||
|
alice <## "bob (Bob): accepting request to join group #team..."
|
||||||
|
concurrentlyN_
|
||||||
|
[ do
|
||||||
|
alice <## "bob (Bob): contact is connected"
|
||||||
|
alice <## "bob invited to group #team via your group link"
|
||||||
|
alice <## "#team: bob joined the group",
|
||||||
|
do
|
||||||
|
bob <## "alice (Alice): contact is connected"
|
||||||
|
bob <## "#team: you joined the group"
|
||||||
|
]
|
||||||
|
alice ##> "/set link role #team admin"
|
||||||
|
alice <## "#team: initial role for group member cannot be admin, use member or observer"
|
||||||
|
alice ##> "/set link role #team member"
|
||||||
|
_ <- getGroupLink alice "team" GRMember False
|
||||||
|
cath ##> ("/c " <> gLink)
|
||||||
|
cath <## "connection request sent!"
|
||||||
|
alice <## "cath (Catherine): accepting request to join group #team..."
|
||||||
|
-- if contact existed it is merged
|
||||||
|
concurrentlyN_
|
||||||
|
[ alice
|
||||||
|
<### [ "cath (Catherine): contact is connected",
|
||||||
|
EndsWith "invited to group #team via your group link",
|
||||||
|
EndsWith "joined the group"
|
||||||
|
],
|
||||||
|
cath
|
||||||
|
<### [ "alice (Alice): contact is connected",
|
||||||
|
"#team: you joined the group",
|
||||||
|
"#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 hello"
|
||||||
|
concurrently_
|
||||||
|
(bob <# "#team alice> hello")
|
||||||
|
(cath <# "#team alice> hello")
|
||||||
|
cath #> "#team hello too"
|
||||||
|
concurrently_
|
||||||
|
(alice <# "#team cath> hello too")
|
||||||
|
(bob <# "#team cath> hello too")
|
||||||
|
bob ##> "#team hey"
|
||||||
|
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"
|
||||||
|
concurrently_
|
||||||
|
(bob <## "#team: alice changed your role from observer to member")
|
||||||
|
(cath <## "#team: alice changed the role of bob from observer to member")
|
||||||
|
bob #> "#team hey now"
|
||||||
|
concurrently_
|
||||||
|
(alice <# "#team bob> hey now")
|
||||||
|
(cath <# "#team bob> hey now")
|
||||||
|
|
||||||
|
testGroupLinkLeaveDelete :: HasCallStack => FilePath -> IO ()
|
||||||
|
testGroupLinkLeaveDelete =
|
||||||
|
testChat3 aliceProfile bobProfile cathProfile $
|
||||||
|
\alice bob cath -> do
|
||||||
|
connectUsers alice bob
|
||||||
|
connectUsers cath bob
|
||||||
|
alice ##> "/g team"
|
||||||
|
alice <## "group #team is created"
|
||||||
|
alice <## "to add members use /a team <name> or /create link #team"
|
||||||
|
alice ##> "/create link #team"
|
||||||
|
gLink <- getGroupLink alice "team" GRMember True
|
||||||
|
bob ##> ("/c " <> gLink)
|
||||||
|
bob <## "connection request sent!"
|
||||||
|
alice <## "bob_1 (Bob): accepting request to join group #team..."
|
||||||
|
concurrentlyN_
|
||||||
|
[ alice
|
||||||
|
<### [ "bob_1 (Bob): contact is connected",
|
||||||
|
"contact bob_1 is merged into bob",
|
||||||
|
"use @bob <message> to send messages",
|
||||||
|
EndsWith "invited to group #team via your group link",
|
||||||
|
EndsWith "joined the group"
|
||||||
|
],
|
||||||
|
bob
|
||||||
|
<### [ "alice_1 (Alice): contact is connected",
|
||||||
|
"contact alice_1 is merged into alice",
|
||||||
|
"use @alice <message> to send messages",
|
||||||
|
"#team: you joined the group"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
cath ##> ("/c " <> gLink)
|
||||||
|
cath <## "connection request sent!"
|
||||||
|
alice <## "cath (Catherine): accepting request to join group #team..."
|
||||||
|
concurrentlyN_
|
||||||
|
[ alice
|
||||||
|
<### [ "cath (Catherine): contact is connected",
|
||||||
|
"cath invited to group #team via your group link",
|
||||||
|
"#team: cath joined the group"
|
||||||
|
],
|
||||||
|
cath
|
||||||
|
<### [ "alice (Alice): contact is connected",
|
||||||
|
"#team: you joined the group",
|
||||||
|
"#team: member bob_1 (Bob) is connected",
|
||||||
|
"contact bob_1 is merged into bob",
|
||||||
|
"use @bob <message> to send messages"
|
||||||
|
],
|
||||||
|
bob
|
||||||
|
<### [ "#team: alice added cath_1 (Catherine) to the group (connecting...)",
|
||||||
|
"#team: new member cath_1 is connected",
|
||||||
|
"contact cath_1 is merged into cath",
|
||||||
|
"use @cath <message> to send messages"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
bob ##> "/l team"
|
||||||
|
concurrentlyN_
|
||||||
|
[ do
|
||||||
|
bob <## "#team: you left the group"
|
||||||
|
bob <## "use /d #team to delete the group",
|
||||||
|
alice <## "#team: bob left the group",
|
||||||
|
cath <## "#team: bob left the group"
|
||||||
|
]
|
||||||
|
bob ##> "/contacts"
|
||||||
|
bob <## "alice (Alice)"
|
||||||
|
bob <## "cath (Catherine)"
|
||||||
|
bob ##> "/d #team"
|
||||||
|
bob <## "#team: you deleted the group"
|
||||||
|
bob ##> "/contacts"
|
||||||
|
bob <## "alice (Alice)"
|
||||||
|
bob <## "cath (Catherine)"
|
||||||
|
@ -323,13 +323,13 @@ getContactLink cc created = do
|
|||||||
cc <## "to delete it: /da (accepted contacts will remain connected)"
|
cc <## "to delete it: /da (accepted contacts will remain connected)"
|
||||||
pure link
|
pure link
|
||||||
|
|
||||||
getGroupLink :: HasCallStack => TestCC -> String -> Bool -> IO String
|
getGroupLink :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO String
|
||||||
getGroupLink cc gName created = do
|
getGroupLink cc gName mRole created = do
|
||||||
cc <## if created then "Group link is created!" else "Group link:"
|
cc <## if created then "Group link is created!" else "Group link:"
|
||||||
cc <## ""
|
cc <## ""
|
||||||
link <- getTermLine cc
|
link <- getTermLine cc
|
||||||
cc <## ""
|
cc <## ""
|
||||||
cc <## "Anybody can connect to you and join group with: /c <group_link_above>"
|
cc <## ("Anybody can connect to you and join group as " <> B.unpack (strEncode mRole) <> " with: /c <group_link_above>")
|
||||||
cc <## ("to show it again: /show link #" <> gName)
|
cc <## ("to show it again: /show link #" <> gName)
|
||||||
cc <## ("to delete it: /delete link #" <> gName <> " (joined members will remain connected to you)")
|
cc <## ("to delete it: /delete link #" <> gName <> " (joined members will remain connected to you)")
|
||||||
pure link
|
pure link
|
||||||
|
Loading…
Reference in New Issue
Block a user