Merge branch 'master' into xftp

This commit is contained in:
Evgeny Poberezkin 2023-03-18 08:38:27 +00:00
commit 858f0f2650
17 changed files with 158 additions and 34 deletions

View File

@ -496,6 +496,14 @@ data class Chat (
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
@Serializable
@ -942,7 +950,7 @@ data class GroupMember (
fun canChangeRoleTo(groupInfo: GroupInfo): List<GroupMemberRole>? =
if (!canBeRemoved(groupInfo)) null
else groupInfo.membership.memberRole.let { userRole ->
GroupMemberRole.values().filter { it <= userRole }
GroupMemberRole.values().filter { it <= userRole && it != GroupMemberRole.Observer }
}
val memberIncognito = memberProfile.profileId != memberContactProfileId

View File

@ -83,6 +83,7 @@ fun TerminalLayout(
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
sendMessage = sendCommand,

View File

@ -648,6 +648,7 @@ fun ComposeView(
}
val userCanSend = rememberUpdatedState(chat.userCanSend)
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
Column {
contextItemView()
@ -744,6 +745,7 @@ fun ComposeView(
needToAllowVoiceToContact,
allowedVoiceByPrefs,
allowVoiceToContact = ::allowVoiceToContact,
userIsObserver = userIsObserver.value,
userCanSend = userCanSend.value,
sendMessage = {
sendMessage()

View File

@ -60,6 +60,7 @@ fun SendMsgView(
liveMessageAlertShown: SharedPreference<Boolean>,
needToAllowVoiceToContact: Boolean,
allowedVoiceByPrefs: Boolean,
userIsObserver: Boolean,
userCanSend: Boolean,
allowVoiceToContact: () -> Unit,
sendMessage: () -> Unit,
@ -75,7 +76,7 @@ fun SendMsgView(
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
NativeKeyboard(composeState, textStyle, showDeleteTextButton, userCanSend, onMessageChange)
NativeKeyboard(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange)
// Disable clicks on text field
if (cs.preview is ComposePreview.VoicePreview || !userCanSend) {
Box(Modifier
@ -182,7 +183,7 @@ private fun NativeKeyboard(
composeState: MutableState<ComposeState>,
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userCanSend: Boolean,
userIsObserver: Boolean,
onMessageChange: (String) -> Unit
) {
val cs = composeState.value
@ -262,16 +263,23 @@ private fun NativeKeyboard(
}
showDeleteTextButton.value = it.lineCount >= 4
}
if (composeState.value.preview is ComposePreview.VoicePreview || !userCanSend) {
Text(
if (composeState.value.preview is ComposePreview.VoicePreview) generalGetString(R.string.voice_message_send_text) else generalGetString(R.string.you_are_observer),
Modifier.padding(padding),
color = HighOrLowlight,
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
)
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(
generalGetString(textId),
Modifier.padding(padding),
color = HighOrLowlight,
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
)
}
@Composable
private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>) {
IconButton(
@ -581,6 +589,7 @@ fun PreviewSendMsgView() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
sendMessage = {},
@ -610,6 +619,7 @@ fun PreviewSendMsgViewEditing() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
sendMessage = {},
@ -639,6 +649,7 @@ fun PreviewSendMsgViewInProgress() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
sendMessage = {},

View File

@ -166,7 +166,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
verticalAlignment = Alignment.CenterVertically,
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(
generalGetString(R.string.new_member_role),
values,

View File

@ -120,9 +120,9 @@ fun GroupLinkLayout(
if (groupLink == null) {
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, disabled = creatingLink, click = createLink)
} else {
SectionItemView(padding = PaddingValues(bottom = DEFAULT_PADDING)) {
RoleSelectionRow(groupInfo, groupLinkMemberRole)
}
// SectionItemView(padding = PaddingValues(bottom = DEFAULT_PADDING)) {
// RoleSelectionRow(groupInfo, groupLinkMemberRole)
// }
var initialLaunch by remember { mutableStateOf(true) }
LaunchedEffect(groupLinkMemberRole.value) {
if (!initialLaunch) {

View File

@ -555,6 +555,15 @@ final class Chat: ObservableObject, Identifiable {
}
}
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 viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }

View File

@ -291,7 +291,7 @@ struct ComposeView: View {
.background(.background)
.disabled(!chat.userCanSend)
if (!chat.userCanSend) {
if chat.userIsObserver {
Text("you are observer")
.italic()
.foregroundColor(.secondary)

View File

@ -140,7 +140,7 @@ struct AddGroupMembersView: View {
private func rolePicker() -> some View {
Picker("New member role", selection: $selectedRole) {
ForEach(GroupMemberRole.allCases) { role in
if role <= groupInfo.membership.memberRole {
if role <= groupInfo.membership.memberRole && role != .observer {
Text(role.text)
}
}

View File

@ -34,15 +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.")
.padding(.bottom)
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)
// 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)
HStack {
Button {

View File

@ -1517,7 +1517,7 @@ public struct GroupMember: Identifiable, Decodable {
public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? {
if !canBeRemoved(groupInfo: groupInfo) { return nil }
let userRole = groupInfo.membership.memberRole
return GroupMemberRole.allCases.filter { $0 <= userRole }
return GroupMemberRole.allCases.filter { $0 <= userRole && $0 != .observer }
}
public var memberIncognito: Bool {

View File

@ -1,5 +1,5 @@
name: simplex-chat
version: 4.5.3.1
version: 4.5.4.2
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme

View File

@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 4.5.3.1
version: 4.5.4.2
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat

View File

@ -3439,9 +3439,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
messageError $ eventName <> ": wrong call state " <> T.pack (show $ callStateTag callState)
mergeContacts :: Contact -> Contact -> m ()
mergeContacts to from = do
withStore' $ \db -> mergeContactRecords db userId to from
toView $ CRContactsMerged user to from
mergeContacts c1 c2 = do
withStore' $ \db -> mergeContactRecords db userId c1 c2
toView $ CRContactsMerged user c1 c2
saveConnInfo :: Connection -> ConnInfo -> m ()
saveConnInfo activeConn connInfo = do

View File

@ -1622,8 +1622,17 @@ matchSentProbe db user@User {userId} _from@Contact {contactId} (Probe probe) = d
cId : _ -> eitherToMaybe <$> runExceptT (getContact db user cId)
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
-- 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
"UPDATE connections SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
@ -1659,6 +1668,17 @@ mergeContactRecords db userId Contact {contactId = toContactId} Contact {contact
deleteContactProfile_ db userId fromContactId
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)
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 user@User {userId, userContactId} agentConnId = do

View File

@ -159,9 +159,12 @@ contactConnId = aConnId . contactConn
contactConnIncognito :: Contact -> Bool
contactConnIncognito = connIncognito . contactConn
contactDirect :: Contact -> Bool
contactDirect Contact {activeConn = Connection {connLevel, viaGroupLink}} = connLevel == 0 && not viaGroupLink
directOrUsed :: Contact -> Bool
directOrUsed Contact {contactUsed, activeConn = Connection {connLevel, viaGroupLink}} =
(connLevel == 0 && not viaGroupLink) || contactUsed
directOrUsed ct@Contact {contactUsed} =
contactDirect ct || contactUsed
anyDirectOrUsed :: Contact -> Bool
anyDirectOrUsed Contact {contactUsed, activeConn = Connection {connLevel}} = connLevel == 0 || contactUsed

View File

@ -47,6 +47,7 @@ chatGroupTests = do
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 "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 = versionTestMatrix3 runTestGroup
@ -1916,3 +1917,72 @@ testGroupLinkMemberRole =
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)"