Merge branch 'master' into master-ghc8107

This commit is contained in:
spaced4ndy 2023-10-25 10:47:35 +04:00
commit 9ded1c9821
21 changed files with 147 additions and 45 deletions

View File

@ -720,8 +720,9 @@ func apiUpdateProfile(profile: Profile) async throws -> (Profile, [Contact])? {
let userId = try currentUserId("apiUpdateProfile") let userId = try currentUserId("apiUpdateProfile")
let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile)) let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile))
switch r { switch r {
case .userProfileNoChange: return nil case .userProfileNoChange: return (profile, [])
case let .userProfileUpdated(_, _, toProfile, updateSummary): return (toProfile, updateSummary.changedContacts) case let .userProfileUpdated(_, _, toProfile, updateSummary): return (toProfile, updateSummary.changedContacts)
case .chatCmdError(_, .errorStore(.duplicateName)): return nil;
default: throw r default: throw r
} }
} }

View File

@ -242,7 +242,7 @@ struct ChatInfoView: View {
} }
.actionSheet(isPresented: $showDeleteContactActionSheet) { .actionSheet(isPresented: $showDeleteContactActionSheet) {
if contact.ready && contact.active { if contact.ready && contact.active {
ActionSheet( return ActionSheet(
title: Text("Delete contact?\nThis cannot be undone!"), title: Text("Delete contact?\nThis cannot be undone!"),
buttons: [ buttons: [
.destructive(Text("Delete and notify contact")) { deleteContact(notify: true) }, .destructive(Text("Delete and notify contact")) { deleteContact(notify: true) },
@ -251,7 +251,7 @@ struct ChatInfoView: View {
] ]
) )
} else { } else {
ActionSheet( return ActionSheet(
title: Text("Delete contact?\nThis cannot be undone!"), title: Text("Delete contact?\nThis cannot be undone!"),
buttons: [ buttons: [
.destructive(Text("Delete")) { deleteContact() }, .destructive(Text("Delete")) { deleteContact() },

View File

@ -78,7 +78,7 @@ struct ChatListNavLink: View {
.frame(height: rowHeights[dynamicTypeSize]) .frame(height: rowHeights[dynamicTypeSize])
.actionSheet(isPresented: $showDeleteContactActionSheet) { .actionSheet(isPresented: $showDeleteContactActionSheet) {
if contact.ready && contact.active { if contact.ready && contact.active {
ActionSheet( return ActionSheet(
title: Text("Delete contact?\nThis cannot be undone!"), title: Text("Delete contact?\nThis cannot be undone!"),
buttons: [ buttons: [
.destructive(Text("Delete and notify contact")) { Task { await deleteChat(chat, notify: true) } }, .destructive(Text("Delete and notify contact")) { Task { await deleteChat(chat, notify: true) } },
@ -87,7 +87,7 @@ struct ChatListNavLink: View {
] ]
) )
} else { } else {
ActionSheet( return ActionSheet(
title: Text("Delete contact?\nThis cannot be undone!"), title: Text("Delete contact?\nThis cannot be undone!"),
buttons: [ buttons: [
.destructive(Text("Delete")) { Task { await deleteChat(chat) } }, .destructive(Text("Delete")) { Task { await deleteChat(chat) } },

View File

@ -174,11 +174,13 @@ struct UserProfile: View {
chatModel.updateCurrentUser(newProfile) chatModel.updateCurrentUser(newProfile)
profile = newProfile profile = newProfile
} }
editProfile = false
} else {
alert = .duplicateUserError
} }
} catch { } catch {
logger.error("UserProfile apiUpdateProfile error: \(responseError(error))") logger.error("UserProfile apiUpdateProfile error: \(responseError(error))")
} }
editProfile = false
} }
} }
} }

View File

@ -83,9 +83,9 @@ public enum AppState: String {
public var canSuspend: Bool { public var canSuspend: Bool {
switch self { switch self {
case .active: true case .active: return true
case .bgRefresh: true case .bgRefresh: return true
default: false default: return false
} }
} }
} }

View File

@ -143,6 +143,7 @@ fun processExternalIntent(intent: Intent?) {
val text = intent.getStringExtra(Intent.EXTRA_TEXT) val text = intent.getStringExtra(Intent.EXTRA_TEXT)
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) { if (uri != null) {
if (uri.scheme != "content") return showNonContentUriAlert()
// Shared file that contains plain text, like `*.log` file // Shared file that contains plain text, like `*.log` file
chatModel.sharedContent.value = SharedContent.File(text ?: "", uri.toURI()) chatModel.sharedContent.value = SharedContent.File(text ?: "", uri.toURI())
} else if (text != null) { } else if (text != null) {
@ -153,12 +154,14 @@ fun processExternalIntent(intent: Intent?) {
isMediaIntent(intent) -> { isMediaIntent(intent) -> {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) { if (uri != null) {
if (uri.scheme != "content") return showNonContentUriAlert()
chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri.toURI())) chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri.toURI()))
} // All other mime types } // All other mime types
} }
else -> { else -> {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) { if (uri != null) {
if (uri.scheme != "content") return showNonContentUriAlert()
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri.toURI()) chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri.toURI())
} }
} }
@ -173,6 +176,7 @@ fun processExternalIntent(intent: Intent?) {
isMediaIntent(intent) -> { isMediaIntent(intent) -> {
val uris = intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri> val uris = intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>
if (uris != null) { if (uris != null) {
if (uris.any { it.scheme != "content" }) return showNonContentUriAlert()
chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris.map { it.toURI() }) chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris.map { it.toURI() })
} // All other mime types } // All other mime types
} }
@ -185,6 +189,13 @@ fun processExternalIntent(intent: Intent?) {
fun isMediaIntent(intent: Intent): Boolean = fun isMediaIntent(intent: Intent): Boolean =
intent.type?.startsWith("image/") == true || intent.type?.startsWith("video/") == true intent.type?.startsWith("image/") == true || intent.type?.startsWith("video/") == true
private fun showNonContentUriAlert() {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.non_content_uri_alert_title),
text = generalGetString(MR.strings.non_content_uri_alert_text)
)
}
//fun testJson() { //fun testJson() {
// val str: String = """ // val str: String = """
// """.trimIndent() // """.trimIndent()

View File

@ -943,6 +943,9 @@ object ChatController {
val r = sendCmd(CC.ApiUpdateProfile(userId, profile)) val r = sendCmd(CC.ApiUpdateProfile(userId, profile))
if (r is CR.UserProfileNoChange) return profile to emptyList() if (r is CR.UserProfileNoChange) return profile to emptyList()
if (r is CR.UserProfileUpdated) return r.toProfile to r.updateSummary.changedContacts if (r is CR.UserProfileUpdated) return r.toProfile to r.updateSummary.changedContacts
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc))
}
Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}") Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}")
return null return null
} }

View File

@ -324,8 +324,26 @@ fun ComposeView(
} }
fun deleteUnusedFiles() { fun deleteUnusedFiles() {
chatModel.filesToDelete.forEach { it.delete() } val shared = chatModel.sharedContent.value
chatModel.filesToDelete.clear() if (shared == null) {
chatModel.filesToDelete.forEach { it.delete() }
chatModel.filesToDelete.clear()
} else {
val sharedPaths = when (shared) {
is SharedContent.Media -> shared.uris.map { it.toString() }
is SharedContent.File -> listOf(shared.uri.toString())
is SharedContent.Text -> emptyList()
}
// When sharing a file and pasting it in SimpleX itself, the file shouldn't be deleted before sending or before leaving the chat after sharing
chatModel.filesToDelete.removeAll { file ->
if (sharedPaths.any { it.endsWith(file.name) }) {
false
} else {
file.delete()
true
}
}
}
} }
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? { suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {

View File

@ -42,8 +42,8 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
val (newProfile, _) = updated val (newProfile, _) = updated
chatModel.updateCurrentUser(newProfile) chatModel.updateCurrentUser(newProfile)
profile = newProfile profile = newProfile
close()
} }
close()
} }
} }
) )

View File

@ -16,6 +16,8 @@
<!-- MainActivity.kt --> <!-- MainActivity.kt -->
<string name="opening_database">Opening database…</string> <string name="opening_database">Opening database…</string>
<string name="non_content_uri_alert_title">Invalid file path</string>
<string name="non_content_uri_alert_text">You shared an invalid file path. Report the issue to the app developers.</string>
<!-- Server info - ChatModel.kt --> <!-- Server info - ChatModel.kt -->
<string name="server_connected">connected</string> <string name="server_connected">connected</string>

View File

@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: 6e2bb080263b9d899e2bd369559dbd4f6978b4cc tag: bba7ad349459dc212782517bce68a60299bbb827
source-repository-package source-repository-package
type: git type: git

View File

@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."6e2bb080263b9d899e2bd369559dbd4f6978b4cc" = "0rwamck6jvky16g1323h1b0355yw79v81557wxwjpzni97k418s9"; "https://github.com/simplex-chat/simplexmq.git"."bba7ad349459dc212782517bce68a60299bbb827" = "0qsiv83zvz2q4g9rhjys57yaj14hvjl0wp71dmckvqayyz7mfqqa";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."804fa283f067bd3fd89b8c5f8d25b3047813a517" = "1j67wp7rfybfx3ryx08z6gqmzj85j51hmzhgx47ihgmgr47sl895"; "https://github.com/kazu-yamamoto/http2.git"."804fa283f067bd3fd89b8c5f8d25b3047813a517" = "1j67wp7rfybfx3ryx08z6gqmzj85j51hmzhgx47ihgmgr47sl895";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";

View File

@ -1502,13 +1502,15 @@ processChatCommand = \case
chatRef <- getChatRef user chatName chatRef <- getChatRef user chatName
chatItemId <- getChatItemIdByText user chatRef msg chatItemId <- getChatItemIdByText user chatRef msg
processChatCommand $ APIChatItemReaction chatRef chatItemId add reaction processChatCommand $ APIChatItemReaction chatRef chatItemId add reaction
APINewGroup userId gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do APINewGroup userId incognito gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do
checkValidName displayName checkValidName displayName
gVar <- asks idsDrg gVar <- asks idsDrg
groupInfo <- withStore $ \db -> createNewGroup db gVar user gProfile -- [incognito] generate incognito profile for group membership
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
groupInfo <- withStore $ \db -> createNewGroup db gVar user gProfile incognitoProfile
pure $ CRGroupCreated user groupInfo pure $ CRGroupCreated user groupInfo
NewGroup gProfile -> withUser $ \User {userId} -> NewGroup incognito gProfile -> withUser $ \User {userId} ->
processChatCommand $ APINewGroup userId gProfile processChatCommand $ APINewGroup userId incognito gProfile
APIAddMember groupId contactId memRole -> withUser $ \user -> withChatLock "addMember" $ do APIAddMember groupId contactId memRole -> withUser $ \user -> withChatLock "addMember" $ do
-- TODO for large groups: no need to load all members to determine if contact is a member -- TODO for large groups: no need to load all members to determine if contact is a member
(group, contact) <- withStore $ \db -> (,) <$> getGroup db user groupId <*> getContact db user contactId (group, contact) <- withStore $ \db -> (,) <$> getGroup db user groupId <*> getContact db user contactId
@ -5707,8 +5709,8 @@ chatCommandP =
("/help settings" <|> "/hs") $> ChatHelp HSSettings, ("/help settings" <|> "/hs") $> ChatHelp HSSettings,
("/help db" <|> "/hd") $> ChatHelp HSDatabase, ("/help db" <|> "/hd") $> ChatHelp HSDatabase,
("/help" <|> "/h") $> ChatHelp HSMain, ("/help" <|> "/h") $> ChatHelp HSMain,
("/group " <|> "/g ") *> char_ '#' *> (NewGroup <$> groupProfile), ("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile),
"/_group " *> (APINewGroup <$> A.decimal <* A.space <*> jsonP), "/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP),
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRMember)), ("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRMember)),
("/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),

View File

@ -363,8 +363,8 @@ data ChatCommand
| EditMessage {chatName :: ChatName, editedMsg :: Text, message :: Text} | EditMessage {chatName :: ChatName, editedMsg :: Text, message :: Text}
| UpdateLiveMessage {chatName :: ChatName, chatItemId :: ChatItemId, liveMessage :: Bool, message :: Text} | UpdateLiveMessage {chatName :: ChatName, chatItemId :: ChatItemId, liveMessage :: Bool, message :: Text}
| ReactToMessage {add :: Bool, reaction :: MsgReaction, chatName :: ChatName, reactToMessage :: Text} | ReactToMessage {add :: Bool, reaction :: MsgReaction, chatName :: ChatName, reactToMessage :: Text}
| APINewGroup UserId GroupProfile | APINewGroup UserId IncognitoEnabled GroupProfile
| NewGroup GroupProfile | NewGroup IncognitoEnabled GroupProfile
| AddMember GroupName ContactName GroupMemberRole | AddMember GroupName ContactName GroupMemberRole
| JoinGroup GroupName | JoinGroup GroupName
| MemberRole GroupName ContactName GroupMemberRole | MemberRole GroupName ContactName GroupMemberRole

View File

@ -188,17 +188,6 @@ createIncognitoProfile db User {userId} p = do
createdAt <- getCurrentTime createdAt <- getCurrentTime
createIncognitoProfile_ db userId createdAt p createIncognitoProfile_ db userId createdAt p
createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Profile -> IO Int64
createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, image} = do
DB.execute
db
[sql|
INSERT INTO contact_profiles (display_name, full_name, image, user_id, incognito, created_at, updated_at)
VALUES (?,?,?,?,?,?,?)
|]
(displayName, fullName, image, userId, Just True, createdAt, createdAt)
insertedRowId db
createDirectContact :: DB.Connection -> User -> Connection -> Profile -> ExceptT StoreError IO Contact createDirectContact :: DB.Connection -> User -> Connection -> Profile -> ExceptT StoreError IO Contact
createDirectContact db user@User {userId} activeConn@Connection {connId, localAlias} p@Profile {preferences} = do createDirectContact db user@User {userId} activeConn@Connection {connId, localAlias} p@Profile {preferences} = do
createdAt <- liftIO getCurrentTime createdAt <- liftIO getCurrentTime

View File

@ -278,11 +278,12 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection connRow}) in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection connRow})
-- | creates completely new group with a single member - the current user -- | creates completely new group with a single member - the current user
createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> ExceptT StoreError IO GroupInfo createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo
createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do createNewGroup db gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
fullGroupPreferences = mergeGroupPreferences groupPreferences fullGroupPreferences = mergeGroupPreferences groupPreferences
currentTs <- getCurrentTime currentTs <- getCurrentTime
customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile
withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
groupId <- liftIO $ do groupId <- liftIO $ do
DB.execute DB.execute
@ -296,7 +297,7 @@ createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do
(ldn, userId, profileId, True, currentTs, currentTs, currentTs) (ldn, userId, profileId, True, currentTs, currentTs, currentTs)
insertedRowId db insertedRowId db
memberId <- liftIO $ encodedRandomBytes gVar 12 memberId <- liftIO $ encodedRandomBytes gVar 12
membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser customUserProfileId currentTs
let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False} let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False}
pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId = Nothing, chatSettings, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs} pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId = Nothing, chatSettings, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs}

View File

@ -182,6 +182,17 @@ createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange
where where
ent ct = if connType == ct then entityId else Nothing ent ct = if connType == ct then entityId else Nothing
createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Profile -> IO Int64
createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, image} = do
DB.execute
db
[sql|
INSERT INTO contact_profiles (display_name, full_name, image, user_id, incognito, created_at, updated_at)
VALUES (?,?,?,?,?,?,?)
|]
(displayName, fullName, image, userId, Just True, createdAt, createdAt)
insertedRowId db
setPeerChatVRange :: DB.Connection -> Int64 -> VersionRange -> IO () setPeerChatVRange :: DB.Connection -> Int64 -> VersionRange -> IO ()
setPeerChatVRange db connId (VersionRange minVer maxVer) = setPeerChatVRange db connId (VersionRange minVer maxVer) =
DB.execute DB.execute

View File

@ -131,7 +131,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
CRUserContactLink u UserContactLink {connReqContact, autoAccept} -> ttyUser u $ connReqContact_ "Your chat address:" connReqContact <> autoAcceptStatus_ autoAccept CRUserContactLink u UserContactLink {connReqContact, autoAccept} -> ttyUser u $ connReqContact_ "Your chat address:" connReqContact <> autoAcceptStatus_ autoAccept
CRUserContactLinkUpdated u UserContactLink {autoAccept} -> ttyUser u $ autoAcceptStatus_ autoAccept CRUserContactLinkUpdated u UserContactLink {autoAccept} -> ttyUser u $ autoAcceptStatus_ autoAccept
CRContactRequestRejected u UserContactRequest {localDisplayName = c} -> ttyUser u [ttyContact c <> ": contact request rejected"] CRContactRequestRejected u UserContactRequest {localDisplayName = c} -> ttyUser u [ttyContact c <> ": contact request rejected"]
CRGroupCreated u g -> ttyUser u $ viewGroupCreated g CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView
CRGroupMembers u g -> ttyUser u $ viewGroupMembers g CRGroupMembers u g -> ttyUser u $ viewGroupMembers g
CRGroupsList u gs -> ttyUser u $ viewGroupsList gs CRGroupsList u gs -> ttyUser u $ viewGroupsList gs
CRSentGroupInvitation u g c _ -> CRSentGroupInvitation u g c _ ->
@ -789,11 +789,22 @@ viewReceivedContactRequest c Profile {fullName} =
"to reject: " <> highlight ("/rc " <> viewName c) <> " (the sender will NOT be notified)" "to reject: " <> highlight ("/rc " <> viewName c) <> " (the sender will NOT be notified)"
] ]
viewGroupCreated :: GroupInfo -> [StyledString] viewGroupCreated :: GroupInfo -> Bool -> [StyledString]
viewGroupCreated g = viewGroupCreated g testView =
[ "group " <> ttyFullGroup g <> " is created", case incognitoMembershipProfile g of
"to add members use " <> highlight ("/a " <> viewGroupName g <> " <name>") <> " or " <> highlight ("/create link #" <> viewGroupName g) Just localProfile
] | testView -> incognitoProfile' profile : message
| otherwise -> message
where
profile = fromLocalProfile localProfile
message =
[ "group " <> ttyFullGroup g <> " is created, your incognito profile for this group is " <> incognitoProfile' profile,
"to add members use " <> highlight ("/create link #" <> viewGroupName g)
]
Nothing ->
[ "group " <> ttyFullGroup g <> " is created",
"to add members use " <> highlight ("/a " <> viewGroupName g <> " <name>") <> " or " <> highlight ("/create link #" <> viewGroupName g)
]
viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString] viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString]
viewCannotResendInvitation g c = viewCannotResendInvitation g c =
@ -1668,7 +1679,7 @@ viewChatError logLevel = \case
_ -> ": 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"] 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 are using an incognito profile for this group - 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"]
CEGroupNotJoined g -> ["you did not join this group, use " <> highlight ("/join #" <> viewGroupName g)] CEGroupNotJoined g -> ["you did not join this group, use " <> highlight ("/join #" <> viewGroupName g)]
CEGroupMemberNotActive -> ["your group connection is not active yet, try later"] CEGroupMemberNotActive -> ["your group connection is not active yet, try later"]

View File

@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq # - ../simplexmq
- github: simplex-chat/simplexmq - github: simplex-chat/simplexmq
commit: 6e2bb080263b9d899e2bd369559dbd4f6978b4cc commit: bba7ad349459dc212782517bce68a60299bbb827
- github: kazu-yamamoto/http2 - github: kazu-yamamoto/http2
commit: 804fa283f067bd3fd89b8c5f8d25b3047813a517 commit: 804fa283f067bd3fd89b8c5f8d25b3047813a517
# - ../direct-sqlcipher # - ../direct-sqlcipher

View File

@ -23,6 +23,7 @@ chatGroupTests = do
describe "chat groups" $ do describe "chat groups" $ do
it "add contacts, create group and send/receive messages" testGroup it "add contacts, create group and send/receive messages" testGroup
it "add contacts, create group and send/receive messages, check messages" testGroupCheckMessages it "add contacts, create group and send/receive messages, check messages" testGroupCheckMessages
it "create group with incognito membership" testNewGroupIncognito
it "create and join group with 4 members" testGroup2 it "create and join group with 4 members" testGroup2
it "create and delete group" testGroupDelete it "create and delete group" testGroupDelete
it "create group with the same displayName" testGroupSameName it "create group with the same displayName" testGroupSameName
@ -277,6 +278,56 @@ testGroupShared alice bob cath checkMessages = do
alice #$> ("/_unread chat #1 on", id, "ok") alice #$> ("/_unread chat #1 on", id, "ok")
alice #$> ("/_unread chat #1 off", id, "ok") alice #$> ("/_unread chat #1 off", id, "ok")
testNewGroupIncognito :: HasCallStack => FilePath -> IO ()
testNewGroupIncognito =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
-- alice creates group with incognito membership
alice ##> "/g i team"
aliceIncognito <- getTermLine alice
alice <## ("group #team is created, your incognito profile for this group is " <> aliceIncognito)
alice <## "to add members use /create link #team"
-- alice invites bob
alice ##> "/a team bob"
alice <## "you are using an incognito profile for this group - prohibited to invite contacts"
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..."
_ <- getTermLine alice
concurrentlyN_
[ do
alice <## ("bob_1 (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
alice <## "use /i bob_1 to print out this incognito profile again"
alice <## "bob_1 invited to group #team via your group link"
alice <## "#team: bob_1 joined the group",
do
bob <## (aliceIncognito <> ": contact is connected")
bob <## "#team: you joined the group"
]
alice <##> bob
alice ?#> "@bob_1 hi, I'm incognito"
bob <# (aliceIncognito <> "> hi, I'm incognito")
bob #> ("@" <> aliceIncognito <> " hey, I'm bob")
alice ?<# "bob_1> hey, I'm bob"
alice ?#> "#team hello"
bob <# ("#team " <> aliceIncognito <> "> hello")
bob #> "#team hi there"
alice ?<# "#team bob_1> hi there"
alice ##> "/gs"
alice <## "i #team (2 members)"
bob ##> "/gs"
bob <## "#team (2 members)"
testGroup2 :: HasCallStack => FilePath -> IO () testGroup2 :: HasCallStack => FilePath -> IO ()
testGroup2 = testGroup2 =
testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $ testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $

View File

@ -1085,7 +1085,7 @@ testJoinGroupIncognito =
] ]
-- cath cannot invite to the group because her membership is incognito -- cath cannot invite to the group because her membership is incognito
cath ##> "/a secret_club dan" cath ##> "/a secret_club dan"
cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts" cath <## "you are using an incognito profile for this group - prohibited to invite contacts"
-- alice invites dan -- alice invites dan
alice ##> "/a secret_club dan admin" alice ##> "/a secret_club dan admin"
concurrentlyN_ concurrentlyN_