mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 04:39:53 +00:00
core: profile names with spaces (#3105)
* core: profile names with spaces * test * more test * update name validation, export C API * refactor * validate name when creating profile in CLI * validate display name in all APIs when it is changed
This commit is contained in:
parent
da2a94578a
commit
38be27271f
10 changed files with 237 additions and 82 deletions
|
@ -456,6 +456,7 @@ test-suite simplex-chat-test
|
||||||
MobileTests
|
MobileTests
|
||||||
ProtocolTests
|
ProtocolTests
|
||||||
SchemaDump
|
SchemaDump
|
||||||
|
ValidNames
|
||||||
ViewTests
|
ViewTests
|
||||||
WebRTCTests
|
WebRTCTests
|
||||||
Broadcast.Bot
|
Broadcast.Bot
|
||||||
|
|
|
@ -32,7 +32,7 @@ import Data.Bifunctor (bimap, first)
|
||||||
import qualified Data.ByteString.Base64 as B64
|
import qualified Data.ByteString.Base64 as B64
|
||||||
import Data.ByteString.Char8 (ByteString)
|
import Data.ByteString.Char8 (ByteString)
|
||||||
import qualified Data.ByteString.Char8 as B
|
import qualified Data.ByteString.Char8 as B
|
||||||
import Data.Char (isSpace, toLower)
|
import Data.Char
|
||||||
import Data.Constraint (Dict (..))
|
import Data.Constraint (Dict (..))
|
||||||
import Data.Either (fromRight, rights)
|
import Data.Either (fromRight, rights)
|
||||||
import Data.Fixed (div')
|
import Data.Fixed (div')
|
||||||
|
@ -359,6 +359,7 @@ processChatCommand :: forall m. ChatMonad m => ChatCommand -> m ChatResponse
|
||||||
processChatCommand = \case
|
processChatCommand = \case
|
||||||
ShowActiveUser -> withUser' $ pure . CRActiveUser
|
ShowActiveUser -> withUser' $ pure . CRActiveUser
|
||||||
CreateActiveUser NewUser {profile, sameServers, pastTimestamp} -> do
|
CreateActiveUser NewUser {profile, sameServers, pastTimestamp} -> do
|
||||||
|
forM_ profile $ \Profile {displayName} -> checkValidName displayName
|
||||||
p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile
|
p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile
|
||||||
u <- asks currentUser
|
u <- asks currentUser
|
||||||
(smp, smpServers) <- chooseServers SPSMP
|
(smp, smpServers) <- chooseServers SPSMP
|
||||||
|
@ -1457,7 +1458,8 @@ 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 -> withUserId userId $ \user -> do
|
APINewGroup userId gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do
|
||||||
|
checkValidName displayName
|
||||||
gVar <- asks idsDrg
|
gVar <- asks idsDrg
|
||||||
groupInfo <- withStore $ \db -> createNewGroup db gVar user gProfile
|
groupInfo <- withStore $ \db -> createNewGroup db gVar user gProfile
|
||||||
pure $ CRGroupCreated user groupInfo
|
pure $ CRGroupCreated user groupInfo
|
||||||
|
@ -1962,9 +1964,10 @@ processChatCommand = \case
|
||||||
updateProfile :: User -> Profile -> m ChatResponse
|
updateProfile :: User -> Profile -> m ChatResponse
|
||||||
updateProfile user p' = updateProfile_ user p' $ withStore $ \db -> updateUserProfile db user p'
|
updateProfile user p' = updateProfile_ user p' $ withStore $ \db -> updateUserProfile db user p'
|
||||||
updateProfile_ :: User -> Profile -> m User -> m ChatResponse
|
updateProfile_ :: User -> Profile -> m User -> m ChatResponse
|
||||||
updateProfile_ user@User {profile = p} p' updateUser
|
updateProfile_ user@User {profile = p@LocalProfile {displayName = n}} p'@Profile {displayName = n'} updateUser
|
||||||
| p' == fromLocalProfile p = pure $ CRUserProfileNoChange user
|
| p' == fromLocalProfile p = pure $ CRUserProfileNoChange user
|
||||||
| otherwise = do
|
| otherwise = do
|
||||||
|
when (n /= n') $ checkValidName n'
|
||||||
-- read contacts before user update to correctly merge preferences
|
-- read contacts before user update to correctly merge preferences
|
||||||
-- [incognito] filter out contacts with whom user has incognito connections
|
-- [incognito] filter out contacts with whom user has incognito connections
|
||||||
contacts <-
|
contacts <-
|
||||||
|
@ -2006,8 +2009,9 @@ processChatCommand = \case
|
||||||
when (directOrUsed ct') $ createSndFeatureItems user ct ct'
|
when (directOrUsed ct') $ createSndFeatureItems user ct ct'
|
||||||
pure $ CRContactPrefsUpdated user ct ct'
|
pure $ CRContactPrefsUpdated user ct ct'
|
||||||
runUpdateGroupProfile :: User -> Group -> GroupProfile -> m ChatResponse
|
runUpdateGroupProfile :: User -> Group -> GroupProfile -> m ChatResponse
|
||||||
runUpdateGroupProfile user (Group g@GroupInfo {groupProfile = p} ms) p' = do
|
runUpdateGroupProfile user (Group g@GroupInfo {groupProfile = p@GroupProfile {displayName = n}} ms) p'@GroupProfile {displayName = n'} = do
|
||||||
assertUserGroupRole g GROwner
|
assertUserGroupRole g GROwner
|
||||||
|
when (n /= n') $ checkValidName n'
|
||||||
g' <- withStore $ \db -> updateGroupProfile db user g p'
|
g' <- withStore $ \db -> updateGroupProfile db user g p'
|
||||||
(msg, _) <- sendGroupMessage user g' ms (XGrpInfo p')
|
(msg, _) <- sendGroupMessage user g' ms (XGrpInfo p')
|
||||||
let cd = CDGroupSnd g'
|
let cd = CDGroupSnd g'
|
||||||
|
@ -2016,6 +2020,10 @@ processChatCommand = \case
|
||||||
toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat g') ci)
|
toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat g') ci)
|
||||||
createGroupFeatureChangedItems user cd CISndGroupFeature g g'
|
createGroupFeatureChangedItems user cd CISndGroupFeature g g'
|
||||||
pure $ CRGroupUpdated user g g' Nothing
|
pure $ CRGroupUpdated user g g' Nothing
|
||||||
|
checkValidName :: GroupName -> m ()
|
||||||
|
checkValidName displayName = do
|
||||||
|
let validName = T.pack $ mkValidName $ T.unpack displayName
|
||||||
|
when (displayName /= validName) $ throwChatError CEInvalidDisplayName {displayName, validName}
|
||||||
assertUserGroupRole :: GroupInfo -> GroupMemberRole -> m ()
|
assertUserGroupRole :: GroupInfo -> GroupMemberRole -> m ()
|
||||||
assertUserGroupRole g@GroupInfo {membership} requiredRole = do
|
assertUserGroupRole g@GroupInfo {membership} requiredRole = do
|
||||||
when (membership.memberRole < requiredRole) $ throwChatError $ CEGroupUserRole g requiredRole
|
when (membership.memberRole < requiredRole) $ throwChatError $ CEGroupUserRole g requiredRole
|
||||||
|
@ -5245,8 +5253,7 @@ getCreateActiveUser st testView = do
|
||||||
where
|
where
|
||||||
loop = do
|
loop = do
|
||||||
displayName <- getContactName
|
displayName <- getContactName
|
||||||
fullName <- T.pack <$> getWithPrompt "full name (optional)"
|
withTransaction st (\db -> runExceptT $ createUserRecord db (AgentUserId 1) Profile {displayName, fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} True) >>= \case
|
||||||
withTransaction st (\db -> runExceptT $ createUserRecord db (AgentUserId 1) Profile {displayName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing} True) >>= \case
|
|
||||||
Left SEDuplicateName -> do
|
Left SEDuplicateName -> do
|
||||||
putStrLn "chosen display name is already used by another profile on this device, choose another one"
|
putStrLn "chosen display name is already used by another profile on this device, choose another one"
|
||||||
loop
|
loop
|
||||||
|
@ -5276,10 +5283,13 @@ getCreateActiveUser st testView = do
|
||||||
T.unpack $ localDisplayName <> if T.null fullName || localDisplayName == fullName then "" else " (" <> fullName <> ")"
|
T.unpack $ localDisplayName <> if T.null fullName || localDisplayName == fullName then "" else " (" <> fullName <> ")"
|
||||||
getContactName :: IO ContactName
|
getContactName :: IO ContactName
|
||||||
getContactName = do
|
getContactName = do
|
||||||
displayName <- getWithPrompt "display name (no spaces)"
|
displayName <- getWithPrompt "display name"
|
||||||
if null displayName || isJust (find (== ' ') displayName)
|
let validName = mkValidName displayName
|
||||||
then putStrLn "display name has space(s), choose another one" >> getContactName
|
if
|
||||||
else pure $ T.pack displayName
|
| null displayName -> putStrLn "display name can't be empty" >> getContactName
|
||||||
|
| null validName -> putStrLn "display name is invalid, please choose another" >> getContactName
|
||||||
|
| displayName /= validName -> putStrLn ("display name is invalid, you could use this one: " <> validName) >> getContactName
|
||||||
|
| otherwise -> pure $ T.pack displayName
|
||||||
getWithPrompt :: String -> IO String
|
getWithPrompt :: String -> IO String
|
||||||
getWithPrompt s = putStr (s <> ": ") >> hFlush stdout >> getLine
|
getWithPrompt s = putStr (s <> ": ") >> hFlush stdout >> getLine
|
||||||
|
|
||||||
|
@ -5610,7 +5620,13 @@ chatCommandP =
|
||||||
mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString
|
mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString
|
||||||
msgContentP = "text " *> mcTextP <|> "json " *> jsonP
|
msgContentP = "text " *> mcTextP <|> "json " *> jsonP
|
||||||
ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal
|
ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal
|
||||||
displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' '))
|
displayName = safeDecodeUtf8 <$> (quoted "'\"" <|> takeNameTill isSpace)
|
||||||
|
where
|
||||||
|
takeNameTill p =
|
||||||
|
A.peekChar' >>= \c ->
|
||||||
|
if refChar c then A.takeTill p else fail "invalid first character in display name"
|
||||||
|
quoted cs = A.choice [A.char c *> takeNameTill (== c) <* A.char c | c <- cs]
|
||||||
|
refChar c = c > ' ' && c /= '#' && c /= '@'
|
||||||
sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A.space <*> pure msgDir <*> quotedMsg <*> msgTextP
|
sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A.space <*> pure msgDir <*> quotedMsg <*> msgTextP
|
||||||
quotedMsg = safeDecodeUtf8 <$> (A.char '(' *> A.takeTill (== ')') <* A.char ')') <* optional A.space
|
quotedMsg = safeDecodeUtf8 <$> (A.char '(' *> A.takeTill (== ')') <* A.char ')') <* optional A.space
|
||||||
reactionP = MREmoji <$> (mrEmojiChar <$?> (toEmoji <$> A.anyChar))
|
reactionP = MREmoji <$> (mrEmojiChar <$?> (toEmoji <$> A.anyChar))
|
||||||
|
@ -5623,7 +5639,6 @@ chatCommandP =
|
||||||
'*' -> head "❤️"
|
'*' -> head "❤️"
|
||||||
'^' -> '🚀'
|
'^' -> '🚀'
|
||||||
c -> c
|
c -> c
|
||||||
refChar c = c > ' ' && c /= '#' && c /= '@'
|
|
||||||
liveMessageP = " live=" *> onOffP <|> pure False
|
liveMessageP = " live=" *> onOffP <|> pure False
|
||||||
sendMessageTTLP = " ttl=" *> ((Just <$> A.decimal) <|> ("default" $> Nothing)) <|> pure Nothing
|
sendMessageTTLP = " ttl=" *> ((Just <$> A.decimal) <|> ("default" $> Nothing)) <|> pure Nothing
|
||||||
receiptSettings = do
|
receiptSettings = do
|
||||||
|
@ -5718,3 +5733,16 @@ timeItToView s action = do
|
||||||
let diff = diffToMilliseconds $ diffUTCTime t2 t1
|
let diff = diffToMilliseconds $ diffUTCTime t2 t1
|
||||||
toView $ CRTimedAction s diff
|
toView $ CRTimedAction s diff
|
||||||
pure a
|
pure a
|
||||||
|
|
||||||
|
mkValidName :: String -> String
|
||||||
|
mkValidName = reverse . dropWhile isSpace . fst . foldl' addChar ("", '\NUL')
|
||||||
|
where
|
||||||
|
addChar (r, prev) c = if notProhibited && validChar then (c' : r, c') else (r, prev)
|
||||||
|
where
|
||||||
|
c' = if isSpace c then ' ' else c
|
||||||
|
validChar
|
||||||
|
| prev == '\NUL' || isSpace prev = validFirstChar
|
||||||
|
| isPunctuation prev = validFirstChar || isSpace c
|
||||||
|
| otherwise = validFirstChar || isSpace c || isMark c || isPunctuation c
|
||||||
|
validFirstChar = isLetter c || isNumber c || isSymbol c
|
||||||
|
notProhibited = c `notElem` ("@#'\"`" :: String)
|
||||||
|
|
|
@ -882,6 +882,7 @@ data ChatErrorType
|
||||||
| CEEmptyUserPassword {userId :: UserId}
|
| CEEmptyUserPassword {userId :: UserId}
|
||||||
| CEUserAlreadyHidden {userId :: UserId}
|
| CEUserAlreadyHidden {userId :: UserId}
|
||||||
| CEUserNotHidden {userId :: UserId}
|
| CEUserNotHidden {userId :: UserId}
|
||||||
|
| CEInvalidDisplayName {displayName :: Text, validName :: Text}
|
||||||
| CEChatNotStarted
|
| CEChatNotStarted
|
||||||
| CEChatNotStopped
|
| CEChatNotStopped
|
||||||
| CEChatStoreChanged
|
| CEChatStoreChanged
|
||||||
|
|
|
@ -65,6 +65,8 @@ foreign export ccall "chat_parse_server" cChatParseServer :: CString -> IO CJSON
|
||||||
|
|
||||||
foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CString -> IO CString
|
foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CString -> IO CString
|
||||||
|
|
||||||
|
foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString
|
||||||
|
|
||||||
foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
|
foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
|
||||||
|
|
||||||
foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
|
foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
|
||||||
|
@ -124,6 +126,10 @@ cChatPasswordHash cPwd cSalt = do
|
||||||
salt <- B.packCString cSalt
|
salt <- B.packCString cSalt
|
||||||
newCStringFromBS $ chatPasswordHash pwd salt
|
newCStringFromBS $ chatPasswordHash pwd salt
|
||||||
|
|
||||||
|
-- This function supports utf8 strings
|
||||||
|
cChatValidName :: CString -> IO CString
|
||||||
|
cChatValidName cName = newCString . mkValidName =<< peekCString cName
|
||||||
|
|
||||||
mobileChatOpts :: String -> String -> ChatOpts
|
mobileChatOpts :: String -> String -> ChatOpts
|
||||||
mobileChatOpts dbFilePrefix dbKey =
|
mobileChatOpts dbFilePrefix dbKey =
|
||||||
ChatOpts
|
ChatOpts
|
||||||
|
|
|
@ -15,7 +15,7 @@ import Data.Aeson (ToJSON)
|
||||||
import qualified Data.Aeson as J
|
import qualified Data.Aeson as J
|
||||||
import qualified Data.ByteString.Char8 as B
|
import qualified Data.ByteString.Char8 as B
|
||||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||||
import Data.Char (toUpper)
|
import Data.Char (isSpace, toUpper)
|
||||||
import Data.Function (on)
|
import Data.Function (on)
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
|
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
|
||||||
|
@ -224,7 +224,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
|
||||||
CRLeftMember u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " left the group"]
|
CRLeftMember u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " left the group"]
|
||||||
CRGroupEmpty u g -> ttyUser u [ttyFullGroup g <> ": group is empty"]
|
CRGroupEmpty u g -> ttyUser u [ttyFullGroup g <> ": group is empty"]
|
||||||
CRGroupRemoved u g -> ttyUser u [ttyFullGroup g <> ": you are no longer a member or group deleted"]
|
CRGroupRemoved u g -> ttyUser u [ttyFullGroup g <> ": you are no longer a member or group deleted"]
|
||||||
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 #" <> viewGroupName 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
|
||||||
CRGroupDescription u g -> ttyUser u $ viewGroupDescription g
|
CRGroupDescription u g -> ttyUser u $ viewGroupDescription g
|
||||||
|
@ -674,10 +674,7 @@ viewContactNotFound cName suspectedMember =
|
||||||
["no contact " <> ttyContact cName <> useMessageMember]
|
["no contact " <> ttyContact cName <> useMessageMember]
|
||||||
where
|
where
|
||||||
useMessageMember = case suspectedMember of
|
useMessageMember = case suspectedMember of
|
||||||
Just (g, m) -> do
|
Just (g, m) -> ", use " <> highlight ("@#" <> viewGroupName g <> " " <> viewMemberName m <> " <your message>")
|
||||||
let GroupInfo {localDisplayName = gName} = g
|
|
||||||
GroupMember {localDisplayName = mName} = m
|
|
||||||
", use " <> highlight' ("@#" <> T.unpack gName <> " " <> T.unpack mName <> " <your message>")
|
|
||||||
_ -> ""
|
_ -> ""
|
||||||
|
|
||||||
viewChatCleared :: AChatInfo -> [StyledString]
|
viewChatCleared :: AChatInfo -> [StyledString]
|
||||||
|
@ -732,14 +729,14 @@ groupLink_ intro g cReq mRole =
|
||||||
(plain . strEncode) cReq,
|
(plain . strEncode) cReq,
|
||||||
"",
|
"",
|
||||||
"Anybody can connect to you and join group as " <> showRole mRole <> " 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 #" <> viewGroupName g),
|
||||||
"to delete it: " <> highlight ("/delete link #" <> groupName' g) <> " (joined members will remain connected to you)"
|
"to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)"
|
||||||
]
|
]
|
||||||
|
|
||||||
viewGroupLinkDeleted :: GroupInfo -> [StyledString]
|
viewGroupLinkDeleted :: GroupInfo -> [StyledString]
|
||||||
viewGroupLinkDeleted g =
|
viewGroupLinkDeleted g =
|
||||||
[ "Group link is deleted - joined members will remain connected.",
|
[ "Group link is deleted - joined members will remain connected.",
|
||||||
"To create a new group link use " <> highlight ("/create link #" <> groupName' g)
|
"To create a new group link use " <> highlight ("/create link #" <> viewGroupName g)
|
||||||
]
|
]
|
||||||
|
|
||||||
viewSentInvitation :: Maybe Profile -> Bool -> [StyledString]
|
viewSentInvitation :: Maybe Profile -> Bool -> [StyledString]
|
||||||
|
@ -756,20 +753,20 @@ viewSentInvitation incognitoProfile testView =
|
||||||
viewReceivedContactRequest :: ContactName -> Profile -> [StyledString]
|
viewReceivedContactRequest :: ContactName -> Profile -> [StyledString]
|
||||||
viewReceivedContactRequest c Profile {fullName} =
|
viewReceivedContactRequest c Profile {fullName} =
|
||||||
[ ttyFullName c fullName <> " wants to connect to you!",
|
[ ttyFullName c fullName <> " wants to connect to you!",
|
||||||
"to accept: " <> highlight ("/ac " <> c),
|
"to accept: " <> highlight ("/ac " <> viewName c),
|
||||||
"to reject: " <> highlight ("/rc " <> c) <> " (the sender will NOT be notified)"
|
"to reject: " <> highlight ("/rc " <> viewName c) <> " (the sender will NOT be notified)"
|
||||||
]
|
]
|
||||||
|
|
||||||
viewGroupCreated :: GroupInfo -> [StyledString]
|
viewGroupCreated :: GroupInfo -> [StyledString]
|
||||||
viewGroupCreated g@GroupInfo {localDisplayName = n} =
|
viewGroupCreated g =
|
||||||
[ "group " <> ttyFullGroup g <> " is created",
|
[ "group " <> ttyFullGroup g <> " is created",
|
||||||
"to add members use " <> highlight ("/a " <> n <> " <name>") <> " or " <> highlight ("/create link #" <> n)
|
"to add members use " <> highlight ("/a " <> viewGroupName g <> " <name>") <> " or " <> highlight ("/create link #" <> viewGroupName g)
|
||||||
]
|
]
|
||||||
|
|
||||||
viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString]
|
viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString]
|
||||||
viewCannotResendInvitation GroupInfo {localDisplayName = gn} c =
|
viewCannotResendInvitation g c =
|
||||||
[ ttyContact c <> " is already invited to group " <> ttyGroup gn,
|
[ ttyContact c <> " is already invited to group " <> ttyGroup' g,
|
||||||
"to re-send invitation: " <> highlight ("/rm " <> gn <> " " <> c) <> ", " <> highlight ("/a " <> gn <> " " <> c)
|
"to re-send invitation: " <> highlight ("/rm " <> viewGroupName g <> " " <> c) <> ", " <> highlight ("/a " <> viewGroupName g <> " " <> viewName c)
|
||||||
]
|
]
|
||||||
|
|
||||||
viewDirectMessagesProhibited :: MsgDirection -> Contact -> [StyledString]
|
viewDirectMessagesProhibited :: MsgDirection -> Contact -> [StyledString]
|
||||||
|
@ -790,11 +787,11 @@ viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [Style
|
||||||
viewReceivedGroupInvitation g c role =
|
viewReceivedGroupInvitation g c role =
|
||||||
ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role) :
|
ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role) :
|
||||||
case incognitoMembershipProfile g of
|
case incognitoMembershipProfile g of
|
||||||
Just mp -> ["use " <> highlight ("/j " <> groupName' g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile mp)]
|
Just mp -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile mp)]
|
||||||
Nothing -> ["use " <> highlight ("/j " <> groupName' g) <> " to accept"]
|
Nothing -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to accept"]
|
||||||
|
|
||||||
groupPreserved :: GroupInfo -> [StyledString]
|
groupPreserved :: GroupInfo -> [StyledString]
|
||||||
groupPreserved g = ["use " <> highlight ("/d #" <> groupName' g) <> " to delete the group"]
|
groupPreserved g = ["use " <> highlight ("/d #" <> viewGroupName g) <> " to delete the group"]
|
||||||
|
|
||||||
connectedMember :: GroupMember -> StyledString
|
connectedMember :: GroupMember -> StyledString
|
||||||
connectedMember m = case memberCategory m of
|
connectedMember m = case memberCategory m of
|
||||||
|
@ -845,7 +842,7 @@ viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filt
|
||||||
_ -> ""
|
_ -> ""
|
||||||
|
|
||||||
viewContactConnected :: Contact -> Maybe Profile -> Bool -> [StyledString]
|
viewContactConnected :: Contact -> Maybe Profile -> Bool -> [StyledString]
|
||||||
viewContactConnected ct@Contact {localDisplayName} userIncognitoProfile testView =
|
viewContactConnected ct userIncognitoProfile testView =
|
||||||
case userIncognitoProfile of
|
case userIncognitoProfile of
|
||||||
Just profile ->
|
Just profile ->
|
||||||
if testView
|
if testView
|
||||||
|
@ -854,7 +851,7 @@ viewContactConnected ct@Contact {localDisplayName} userIncognitoProfile testView
|
||||||
where
|
where
|
||||||
message =
|
message =
|
||||||
[ ttyFullContact ct <> ": contact is connected, your incognito profile for this contact is " <> incognitoProfile' profile,
|
[ ttyFullContact ct <> ": contact is connected, your incognito profile for this contact is " <> incognitoProfile' profile,
|
||||||
"use " <> highlight ("/i " <> localDisplayName) <> " to print out this incognito profile again"
|
"use " <> highlight ("/i " <> viewContactName ct) <> " to print out this incognito profile again"
|
||||||
]
|
]
|
||||||
Nothing ->
|
Nothing ->
|
||||||
[ttyFullContact ct <> ": contact is connected"]
|
[ttyFullContact ct <> ": contact is connected"]
|
||||||
|
@ -865,10 +862,10 @@ viewGroupsList gs = map groupSS $ sortOn (ldn_ . fst) gs
|
||||||
where
|
where
|
||||||
ldn_ :: GroupInfo -> Text
|
ldn_ :: GroupInfo -> Text
|
||||||
ldn_ g = T.toLower g.localDisplayName
|
ldn_ g = T.toLower g.localDisplayName
|
||||||
groupSS (g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership, chatSettings}, GroupSummary {currentMembers}) =
|
groupSS (g@GroupInfo {membership, chatSettings}, GroupSummary {currentMembers}) =
|
||||||
case memberStatus membership of
|
case memberStatus membership of
|
||||||
GSMemInvited -> groupInvitation' g
|
GSMemInvited -> groupInvitation' g
|
||||||
s -> membershipIncognito g <> ttyGroup ldn <> optFullName ldn fullName <> viewMemberStatus s
|
s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s
|
||||||
where
|
where
|
||||||
viewMemberStatus = \case
|
viewMemberStatus = \case
|
||||||
GSMemRemoved -> delete "you are removed"
|
GSMemRemoved -> delete "you are removed"
|
||||||
|
@ -876,18 +873,18 @@ viewGroupsList gs = map groupSS $ sortOn (ldn_ . fst) gs
|
||||||
GSMemGroupDeleted -> delete "group deleted"
|
GSMemGroupDeleted -> delete "group deleted"
|
||||||
_
|
_
|
||||||
| enableNtfs chatSettings -> " (" <> memberCount <> ")"
|
| enableNtfs chatSettings -> " (" <> memberCount <> ")"
|
||||||
| otherwise -> " (" <> memberCount <> ", muted, you can " <> highlight ("/unmute #" <> ldn) <> ")"
|
| otherwise -> " (" <> memberCount <> ", muted, you can " <> highlight ("/unmute #" <> viewGroupName g) <> ")"
|
||||||
delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> ldn) <> ")"
|
delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> viewGroupName g) <> ")"
|
||||||
memberCount = sShow currentMembers <> " member" <> if currentMembers == 1 then "" else "s"
|
memberCount = sShow currentMembers <> " member" <> if currentMembers == 1 then "" else "s"
|
||||||
|
|
||||||
groupInvitation' :: GroupInfo -> StyledString
|
groupInvitation' :: GroupInfo -> StyledString
|
||||||
groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}} =
|
groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}} =
|
||||||
highlight ("#" <> ldn)
|
highlight ("#" <> viewName ldn)
|
||||||
<> optFullName ldn fullName
|
<> optFullName ldn fullName
|
||||||
<> " - you are invited ("
|
<> " - you are invited ("
|
||||||
<> highlight ("/j " <> ldn)
|
<> highlight ("/j " <> viewName ldn)
|
||||||
<> joinText
|
<> joinText
|
||||||
<> highlight ("/d #" <> ldn)
|
<> highlight ("/d #" <> viewName ldn)
|
||||||
<> " to delete invitation)"
|
<> " to delete invitation)"
|
||||||
where
|
where
|
||||||
joinText = case incognitoMembershipProfile g of
|
joinText = case incognitoMembershipProfile g of
|
||||||
|
@ -895,21 +892,21 @@ groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfil
|
||||||
Nothing -> " to join, "
|
Nothing -> " to join, "
|
||||||
|
|
||||||
viewContactsMerged :: Contact -> Contact -> [StyledString]
|
viewContactsMerged :: Contact -> Contact -> [StyledString]
|
||||||
viewContactsMerged _into@Contact {localDisplayName = c1} _merged@Contact {localDisplayName = c2} =
|
viewContactsMerged c1 c2 =
|
||||||
[ "contact " <> ttyContact c2 <> " is merged into " <> ttyContact c1,
|
[ "contact " <> ttyContact' c2 <> " is merged into " <> ttyContact' c1,
|
||||||
"use " <> ttyToContact c1 <> highlight' "<message>" <> " to send messages"
|
"use " <> ttyToContact' c1 <> highlight' "<message>" <> " to send messages"
|
||||||
]
|
]
|
||||||
|
|
||||||
viewUserProfile :: Profile -> [StyledString]
|
viewUserProfile :: Profile -> [StyledString]
|
||||||
viewUserProfile Profile {displayName, fullName} =
|
viewUserProfile Profile {displayName, fullName} =
|
||||||
[ "user profile: " <> ttyFullName displayName fullName,
|
[ "user profile: " <> ttyFullName displayName fullName,
|
||||||
"use " <> highlight' "/p <display name> [<full name>]" <> " to change it",
|
"use " <> highlight' "/p <display name>" <> " to change it",
|
||||||
"(the updated profile will be sent to all your contacts)"
|
"(the updated profile will be sent to all your contacts)"
|
||||||
]
|
]
|
||||||
|
|
||||||
viewUserPrivacy :: User -> User -> [StyledString]
|
viewUserPrivacy :: User -> User -> [StyledString]
|
||||||
viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', showNtfs, viewPwdHash} =
|
viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', showNtfs, viewPwdHash} =
|
||||||
[ (if userId == userId' then "current " else "") <> "user " <> plain n' <> ":",
|
[ plain $ (if userId == userId' then "current " else "") <> "user " <> viewName n' <> ":",
|
||||||
"messages are " <> if showNtfs then "shown" else "hidden (use /tail to view)",
|
"messages are " <> if showNtfs then "shown" else "hidden (use /tail to view)",
|
||||||
"profile is " <> if isJust viewPwdHash then "hidden" else "visible"
|
"profile is " <> if isJust viewPwdHash then "hidden" else "visible"
|
||||||
]
|
]
|
||||||
|
@ -1055,18 +1052,18 @@ viewGroupMemberSwitch g m (SwitchProgress qd phase _) = case qd of
|
||||||
QDSnd -> [ttyGroup' g <> ": " <> ttyMember m <> " " <> viewSwitchPhase phase <> " for you"]
|
QDSnd -> [ttyGroup' g <> ": " <> ttyMember m <> " " <> viewSwitchPhase phase <> " for you"]
|
||||||
|
|
||||||
viewContactRatchetSync :: Contact -> RatchetSyncProgress -> [StyledString]
|
viewContactRatchetSync :: Contact -> RatchetSyncProgress -> [StyledString]
|
||||||
viewContactRatchetSync ct@Contact {localDisplayName = c} RatchetSyncProgress {ratchetSyncStatus = rss} =
|
viewContactRatchetSync ct RatchetSyncProgress {ratchetSyncStatus = rss} =
|
||||||
[ttyContact' ct <> ": " <> (plain . ratchetSyncStatusToText) rss]
|
[ttyContact' ct <> ": " <> (plain . ratchetSyncStatusToText) rss]
|
||||||
<> help
|
<> help
|
||||||
where
|
where
|
||||||
help = ["use " <> highlight ("/sync " <> c) <> " to synchronize" | rss `elem` [RSAllowed, RSRequired]]
|
help = ["use " <> highlight ("/sync " <> viewContactName ct) <> " to synchronize" | rss `elem` [RSAllowed, RSRequired]]
|
||||||
|
|
||||||
viewGroupMemberRatchetSync :: GroupInfo -> GroupMember -> RatchetSyncProgress -> [StyledString]
|
viewGroupMemberRatchetSync :: GroupInfo -> GroupMember -> RatchetSyncProgress -> [StyledString]
|
||||||
viewGroupMemberRatchetSync g m@GroupMember {localDisplayName = n} RatchetSyncProgress {ratchetSyncStatus = rss} =
|
viewGroupMemberRatchetSync g m RatchetSyncProgress {ratchetSyncStatus = rss} =
|
||||||
[ttyGroup' g <> " " <> ttyMember m <> ": " <> (plain . ratchetSyncStatusToText) rss]
|
[ttyGroup' g <> " " <> ttyMember m <> ": " <> (plain . ratchetSyncStatusToText) rss]
|
||||||
<> help
|
<> help
|
||||||
where
|
where
|
||||||
help = ["use " <> highlight ("/sync #" <> groupName' g <> " " <> n) <> " to synchronize" | rss `elem` [RSAllowed, RSRequired]]
|
help = ["use " <> highlight ("/sync #" <> viewGroupName g <> " " <> viewMemberName m) <> " to synchronize" | rss `elem` [RSAllowed, RSRequired]]
|
||||||
|
|
||||||
viewContactVerificationReset :: Contact -> [StyledString]
|
viewContactVerificationReset :: Contact -> [StyledString]
|
||||||
viewContactVerificationReset ct =
|
viewContactVerificationReset ct =
|
||||||
|
@ -1077,10 +1074,10 @@ viewGroupMemberVerificationReset g m =
|
||||||
[ttyGroup' g <> " " <> ttyMember m <> ": security code changed"]
|
[ttyGroup' g <> " " <> ttyMember m <> ": security code changed"]
|
||||||
|
|
||||||
viewContactCode :: Contact -> Text -> Bool -> [StyledString]
|
viewContactCode :: Contact -> Text -> Bool -> [StyledString]
|
||||||
viewContactCode ct@Contact {localDisplayName = c} = viewSecurityCode (ttyContact' ct) ("/verify " <> c <> " <code from your contact>")
|
viewContactCode ct = viewSecurityCode (ttyContact' ct) ("/verify " <> viewContactName ct <> " <code from your contact>")
|
||||||
|
|
||||||
viewGroupMemberCode :: GroupInfo -> GroupMember -> Text -> Bool -> [StyledString]
|
viewGroupMemberCode :: GroupInfo -> GroupMember -> Text -> Bool -> [StyledString]
|
||||||
viewGroupMemberCode g m@GroupMember {localDisplayName = n} = viewSecurityCode (ttyGroup' g <> " " <> ttyMember m) ("/verify #" <> groupName' g <> " " <> n <> " <code from your contact>")
|
viewGroupMemberCode g m = viewSecurityCode (ttyGroup' g <> " " <> ttyMember m) ("/verify #" <> viewGroupName g <> " " <> viewMemberName m <> " <code from your contact>")
|
||||||
|
|
||||||
viewSecurityCode :: StyledString -> Text -> Text -> Bool -> [StyledString]
|
viewSecurityCode :: StyledString -> Text -> Text -> Bool -> [StyledString]
|
||||||
viewSecurityCode name cmd code testView
|
viewSecurityCode name cmd code testView
|
||||||
|
@ -1206,9 +1203,9 @@ bold' :: String -> StyledString
|
||||||
bold' = styled Bold
|
bold' = styled Bold
|
||||||
|
|
||||||
viewContactAliasUpdated :: Contact -> [StyledString]
|
viewContactAliasUpdated :: Contact -> [StyledString]
|
||||||
viewContactAliasUpdated Contact {localDisplayName = n, profile = LocalProfile {localAlias}}
|
viewContactAliasUpdated ct@Contact {profile = LocalProfile {localAlias}}
|
||||||
| localAlias == "" = ["contact " <> ttyContact n <> " alias removed"]
|
| localAlias == "" = ["contact " <> ttyContact' ct <> " alias removed"]
|
||||||
| otherwise = ["contact " <> ttyContact n <> " alias updated: " <> plain localAlias]
|
| otherwise = ["contact " <> ttyContact' ct <> " alias updated: " <> plain localAlias]
|
||||||
|
|
||||||
viewConnectionAliasUpdated :: PendingContactConnection -> [StyledString]
|
viewConnectionAliasUpdated :: PendingContactConnection -> [StyledString]
|
||||||
viewConnectionAliasUpdated PendingContactConnection {pccConnId, localAlias}
|
viewConnectionAliasUpdated PendingContactConnection {pccConnId, localAlias}
|
||||||
|
@ -1385,10 +1382,10 @@ savingFile' testView (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, f
|
||||||
savingFile' _ _ = ["saving file"] -- shouldn't happen
|
savingFile' _ _ = ["saving file"] -- shouldn't happen
|
||||||
|
|
||||||
receivingFile_' :: StyledString -> AChatItem -> [StyledString]
|
receivingFile_' :: StyledString -> AChatItem -> [StyledString]
|
||||||
receivingFile_' status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) =
|
receivingFile_' status (AChatItem _ _ (DirectChat c) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) =
|
||||||
[status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact c]
|
[status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact' c]
|
||||||
receivingFile_' status (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupRcv GroupMember {localDisplayName = m}}) =
|
receivingFile_' status (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupRcv m}) =
|
||||||
[status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact m]
|
[status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyMember m]
|
||||||
receivingFile_' status _ = [status <> " receiving file"] -- shouldn't happen
|
receivingFile_' status _ = [status <> " receiving file"] -- shouldn't happen
|
||||||
|
|
||||||
receivingFile_ :: StyledString -> RcvFileTransfer -> [StyledString]
|
receivingFile_ :: StyledString -> RcvFileTransfer -> [StyledString]
|
||||||
|
@ -1556,6 +1553,9 @@ viewChatError logLevel = \case
|
||||||
CEEmptyUserPassword _ -> ["user password is required"]
|
CEEmptyUserPassword _ -> ["user password is required"]
|
||||||
CEUserAlreadyHidden _ -> ["user is already hidden"]
|
CEUserAlreadyHidden _ -> ["user is already hidden"]
|
||||||
CEUserNotHidden _ -> ["user is not hidden"]
|
CEUserNotHidden _ -> ["user is not hidden"]
|
||||||
|
CEInvalidDisplayName {displayName, validName} -> map plain $
|
||||||
|
["invalid display name: " <> viewName displayName]
|
||||||
|
<> ["you could use this one: " <> viewName validName | not (T.null validName)]
|
||||||
CEChatNotStarted -> ["error: chat not started"]
|
CEChatNotStarted -> ["error: chat not started"]
|
||||||
CEChatNotStopped -> ["error: chat not stopped"]
|
CEChatNotStopped -> ["error: chat not stopped"]
|
||||||
CEChatStoreChanged -> ["error: chat store changed, please restart chat"]
|
CEChatStoreChanged -> ["error: chat store changed, please restart chat"]
|
||||||
|
@ -1568,8 +1568,8 @@ viewChatError logLevel = \case
|
||||||
]
|
]
|
||||||
CEContactNotFound cName m_ -> viewContactNotFound cName m_
|
CEContactNotFound cName m_ -> viewContactNotFound cName m_
|
||||||
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
|
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
|
||||||
|
CEContactDisabled ct -> [ttyContact' ct <> ": disabled, to enable: " <> highlight ("/enable " <> viewContactName ct) <> ", to delete: " <> highlight ("/d " <> viewContactName ct)]
|
||||||
CEContactNotActive c -> [ttyContact' c <> ": not active"]
|
CEContactNotActive c -> [ttyContact' c <> ": not active"]
|
||||||
CEContactDisabled Contact {localDisplayName = c} -> [ttyContact c <> ": disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)]
|
|
||||||
CEConnectionDisabled Connection {connId, connType} -> [plain $ "connection " <> textEncode connType <> " (" <> tshow connId <> ") is disabled" | logLevel <= CLLWarning]
|
CEConnectionDisabled Connection {connId, connType} -> [plain $ "connection " <> textEncode connType <> " (" <> tshow connId <> ") is disabled" | logLevel <= CLLWarning]
|
||||||
CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"]
|
CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"]
|
||||||
CEGroupDuplicateMemberId -> ["cannot add member - duplicate member ID"]
|
CEGroupDuplicateMemberId -> ["cannot add member - duplicate member ID"]
|
||||||
|
@ -1581,7 +1581,7 @@ viewChatError logLevel = \case
|
||||||
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"]
|
||||||
CEGroupNotJoined g -> ["you did not join this group, use " <> highlight ("/join #" <> groupName' 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"]
|
||||||
CEGroupMemberUserRemoved -> ["you are no longer a member of the group"]
|
CEGroupMemberUserRemoved -> ["you are no longer a member of the group"]
|
||||||
CEGroupMemberNotFound -> ["group doesn't have this member"]
|
CEGroupMemberNotFound -> ["group doesn't have this member"]
|
||||||
|
@ -1641,8 +1641,8 @@ viewChatError logLevel = \case
|
||||||
SEFileIdNotFoundBySharedMsgId _ -> [] -- recipient tried to accept cancelled file
|
SEFileIdNotFoundBySharedMsgId _ -> [] -- recipient tried to accept cancelled file
|
||||||
SEConnectionNotFound agentConnId -> ["event connection not found, agent ID: " <> sShow agentConnId | logLevel <= CLLWarning] -- mutes delete group error
|
SEConnectionNotFound agentConnId -> ["event connection not found, agent ID: " <> sShow agentConnId | logLevel <= CLLWarning] -- mutes delete group error
|
||||||
SEChatItemNotFoundByText text -> ["message not found by text: " <> plain text]
|
SEChatItemNotFoundByText text -> ["message not found by text: " <> plain text]
|
||||||
SEDuplicateGroupLink g -> ["you already have link for this group, to show: " <> highlight ("/show link #" <> groupName' g)]
|
SEDuplicateGroupLink g -> ["you already have link for this group, to show: " <> highlight ("/show link #" <> viewGroupName g)]
|
||||||
SEGroupLinkNotFound g -> ["no group link, to create: " <> highlight ("/create link #" <> groupName' g)]
|
SEGroupLinkNotFound g -> ["no group link, to create: " <> highlight ("/create link #" <> viewGroupName g)]
|
||||||
e -> ["chat db error: " <> sShow e]
|
e -> ["chat db error: " <> sShow e]
|
||||||
ChatErrorDatabase err -> case err of
|
ChatErrorDatabase err -> case err of
|
||||||
DBErrorEncrypted -> ["error: chat database is already encrypted"]
|
DBErrorEncrypted -> ["error: chat database is already encrypted"]
|
||||||
|
@ -1687,8 +1687,8 @@ viewChatError logLevel = \case
|
||||||
|
|
||||||
viewConnectionEntityDisabled :: ConnectionEntity -> [StyledString]
|
viewConnectionEntityDisabled :: ConnectionEntity -> [StyledString]
|
||||||
viewConnectionEntityDisabled entity = case entity of
|
viewConnectionEntityDisabled entity = case entity of
|
||||||
RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)]
|
RcvDirectMsgConnection _ (Just c) -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable " <> viewContactName c) <> ", to delete: " <> highlight ("/d " <> viewContactName c)]
|
||||||
RcvGroupMsgConnection _ GroupInfo {localDisplayName = g} GroupMember {localDisplayName = m} -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable #" <> g <> " " <> m)]
|
RcvGroupMsgConnection _ g m -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable #" <> viewGroupName g <> " " <> viewMemberName m)]
|
||||||
_ -> ["[" <> entityLabel <> "] connection is disabled"]
|
_ -> ["[" <> entityLabel <> "] connection is disabled"]
|
||||||
where
|
where
|
||||||
entityLabel = connEntityLabel entity
|
entityLabel = connEntityLabel entity
|
||||||
|
@ -1703,7 +1703,7 @@ connEntityLabel = \case
|
||||||
UserContactConnection _ UserContact {} -> "contact address"
|
UserContactConnection _ UserContact {} -> "contact address"
|
||||||
|
|
||||||
ttyContact :: ContactName -> StyledString
|
ttyContact :: ContactName -> StyledString
|
||||||
ttyContact = styled $ colored Green
|
ttyContact = styled (colored Green) . viewName
|
||||||
|
|
||||||
ttyContact' :: Contact -> StyledString
|
ttyContact' :: Contact -> StyledString
|
||||||
ttyContact' Contact {localDisplayName = c} = ttyContact c
|
ttyContact' Contact {localDisplayName = c} = ttyContact c
|
||||||
|
@ -1723,37 +1723,46 @@ ttyFullName :: ContactName -> Text -> StyledString
|
||||||
ttyFullName c fullName = ttyContact c <> optFullName c fullName
|
ttyFullName c fullName = ttyContact c <> optFullName c fullName
|
||||||
|
|
||||||
ttyToContact :: ContactName -> StyledString
|
ttyToContact :: ContactName -> StyledString
|
||||||
ttyToContact c = ttyTo $ "@" <> c <> " "
|
ttyToContact c = ttyTo $ "@" <> viewName c <> " "
|
||||||
|
|
||||||
ttyToContact' :: Contact -> StyledString
|
ttyToContact' :: Contact -> StyledString
|
||||||
ttyToContact' ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyToContact c
|
ttyToContact' ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyToContact c
|
||||||
|
|
||||||
ttyToContactEdited' :: Contact -> StyledString
|
ttyToContactEdited' :: Contact -> StyledString
|
||||||
ttyToContactEdited' ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyTo ("@" <> c <> " [edited] ")
|
ttyToContactEdited' ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyTo ("@" <> viewName c <> " [edited] ")
|
||||||
|
|
||||||
ttyQuotedContact :: Contact -> StyledString
|
ttyQuotedContact :: Contact -> StyledString
|
||||||
ttyQuotedContact Contact {localDisplayName = c} = ttyFrom $ c <> ">"
|
ttyQuotedContact Contact {localDisplayName = c} = ttyFrom $ viewName c <> ">"
|
||||||
|
|
||||||
ttyQuotedMember :: Maybe GroupMember -> StyledString
|
ttyQuotedMember :: Maybe GroupMember -> StyledString
|
||||||
ttyQuotedMember (Just GroupMember {localDisplayName = c}) = "> " <> ttyFrom c
|
ttyQuotedMember (Just GroupMember {localDisplayName = c}) = "> " <> ttyFrom (viewName c)
|
||||||
ttyQuotedMember _ = "> " <> ttyFrom "?"
|
ttyQuotedMember _ = "> " <> ttyFrom "?"
|
||||||
|
|
||||||
ttyFromContact :: Contact -> StyledString
|
ttyFromContact :: Contact -> StyledString
|
||||||
ttyFromContact ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyFrom (c <> "> ")
|
ttyFromContact ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyFrom (viewName c <> "> ")
|
||||||
|
|
||||||
ttyFromContactEdited :: Contact -> StyledString
|
ttyFromContactEdited :: Contact -> StyledString
|
||||||
ttyFromContactEdited ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyFrom (c <> "> [edited] ")
|
ttyFromContactEdited ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyFrom (viewName c <> "> [edited] ")
|
||||||
|
|
||||||
ttyFromContactDeleted :: Contact -> Maybe Text -> StyledString
|
ttyFromContactDeleted :: Contact -> Maybe Text -> StyledString
|
||||||
ttyFromContactDeleted ct@Contact {localDisplayName = c} deletedText_ =
|
ttyFromContactDeleted ct@Contact {localDisplayName = c} deletedText_ =
|
||||||
ctIncognito ct <> ttyFrom (c <> "> " <> maybe "" (\t -> "[" <> t <> "] ") deletedText_)
|
ctIncognito ct <> ttyFrom (viewName c <> "> " <> maybe "" (\t -> "[" <> t <> "] ") deletedText_)
|
||||||
|
|
||||||
ttyGroup :: GroupName -> StyledString
|
ttyGroup :: GroupName -> StyledString
|
||||||
ttyGroup g = styled (colored Blue) $ "#" <> g
|
ttyGroup g = styled (colored Blue) $ "#" <> viewName g
|
||||||
|
|
||||||
ttyGroup' :: GroupInfo -> StyledString
|
ttyGroup' :: GroupInfo -> StyledString
|
||||||
ttyGroup' = ttyGroup . groupName'
|
ttyGroup' = ttyGroup . groupName'
|
||||||
|
|
||||||
|
viewContactName :: Contact -> Text
|
||||||
|
viewContactName = viewName . localDisplayName'
|
||||||
|
|
||||||
|
viewGroupName :: GroupInfo -> Text
|
||||||
|
viewGroupName = viewName . groupName'
|
||||||
|
|
||||||
|
viewMemberName :: GroupMember -> Text
|
||||||
|
viewMemberName GroupMember {localDisplayName = n} = viewName n
|
||||||
|
|
||||||
ttyGroups :: [GroupName] -> StyledString
|
ttyGroups :: [GroupName] -> StyledString
|
||||||
ttyGroups [] = ""
|
ttyGroups [] = ""
|
||||||
ttyGroups [g] = ttyGroup g
|
ttyGroups [g] = ttyGroup g
|
||||||
|
@ -1774,8 +1783,7 @@ ttyFromGroupDeleted g m deletedText_ =
|
||||||
membershipIncognito g <> ttyFrom (fromGroup_ g m <> maybe "" (\t -> "[" <> t <> "] ") deletedText_)
|
membershipIncognito g <> ttyFrom (fromGroup_ g m <> maybe "" (\t -> "[" <> t <> "] ") deletedText_)
|
||||||
|
|
||||||
fromGroup_ :: GroupInfo -> GroupMember -> Text
|
fromGroup_ :: GroupInfo -> GroupMember -> Text
|
||||||
fromGroup_ GroupInfo {localDisplayName = g} GroupMember {localDisplayName = m} =
|
fromGroup_ g m = "#" <> viewGroupName g <> " " <> viewMemberName m <> "> "
|
||||||
"#" <> g <> " " <> m <> "> "
|
|
||||||
|
|
||||||
ttyFrom :: Text -> StyledString
|
ttyFrom :: Text -> StyledString
|
||||||
ttyFrom = styled $ colored Yellow
|
ttyFrom = styled $ colored Yellow
|
||||||
|
@ -1784,12 +1792,13 @@ ttyTo :: Text -> StyledString
|
||||||
ttyTo = styled $ colored Cyan
|
ttyTo = styled $ colored Cyan
|
||||||
|
|
||||||
ttyToGroup :: GroupInfo -> StyledString
|
ttyToGroup :: GroupInfo -> StyledString
|
||||||
ttyToGroup g@GroupInfo {localDisplayName = n} =
|
ttyToGroup g = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> " ")
|
||||||
membershipIncognito g <> ttyTo ("#" <> n <> " ")
|
|
||||||
|
|
||||||
ttyToGroupEdited :: GroupInfo -> StyledString
|
ttyToGroupEdited :: GroupInfo -> StyledString
|
||||||
ttyToGroupEdited g@GroupInfo {localDisplayName = n} =
|
ttyToGroupEdited g = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> " [edited] ")
|
||||||
membershipIncognito g <> ttyTo ("#" <> n <> " [edited] ")
|
|
||||||
|
viewName :: Text -> Text
|
||||||
|
viewName s = if T.any isSpace s then "'" <> s <> "'" else s
|
||||||
|
|
||||||
ttyFilePath :: FilePath -> StyledString
|
ttyFilePath :: FilePath -> StyledString
|
||||||
ttyFilePath = plain
|
ttyFilePath = plain
|
||||||
|
|
|
@ -8,7 +8,7 @@ import ChatTests.Utils
|
||||||
import Control.Concurrent (threadDelay)
|
import Control.Concurrent (threadDelay)
|
||||||
import Control.Concurrent.Async (concurrently_)
|
import Control.Concurrent.Async (concurrently_)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..))
|
import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..), Profile (..))
|
||||||
import System.Directory (copyFile, createDirectoryIfMissing)
|
import System.Directory (copyFile, createDirectoryIfMissing)
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ chatProfileTests = do
|
||||||
describe "user profiles" $ do
|
describe "user profiles" $ do
|
||||||
it "update user profile and notify contacts" testUpdateProfile
|
it "update user profile and notify contacts" testUpdateProfile
|
||||||
it "update user profile with image" testUpdateProfileImage
|
it "update user profile with image" testUpdateProfileImage
|
||||||
|
it "use multiword profile names" testMultiWordProfileNames
|
||||||
describe "user contact link" $ do
|
describe "user contact link" $ do
|
||||||
it "create and connect via contact link" testUserContactLink
|
it "create and connect via contact link" testUserContactLink
|
||||||
it "add contact link to profile" testProfileLink
|
it "add contact link to profile" testProfileLink
|
||||||
|
@ -62,7 +63,7 @@ testUpdateProfile =
|
||||||
createGroup3 "team" alice bob cath
|
createGroup3 "team" alice bob cath
|
||||||
alice ##> "/p"
|
alice ##> "/p"
|
||||||
alice <## "user profile: alice (Alice)"
|
alice <## "user profile: alice (Alice)"
|
||||||
alice <## "use /p <display name> [<full name>] to change it"
|
alice <## "use /p <display name> to change it"
|
||||||
alice <## "(the updated profile will be sent to all your contacts)"
|
alice <## "(the updated profile will be sent to all your contacts)"
|
||||||
alice ##> "/p alice"
|
alice ##> "/p alice"
|
||||||
concurrentlyN_
|
concurrentlyN_
|
||||||
|
@ -117,6 +118,76 @@ testUpdateProfileImage =
|
||||||
bob <## "use @alice2 <message> to send messages"
|
bob <## "use @alice2 <message> to send messages"
|
||||||
(bob </)
|
(bob </)
|
||||||
|
|
||||||
|
testMultiWordProfileNames :: HasCallStack => FilePath -> IO ()
|
||||||
|
testMultiWordProfileNames =
|
||||||
|
testChat3 aliceProfile' bobProfile' cathProfile' $
|
||||||
|
\alice bob cath -> do
|
||||||
|
alice ##> "/c"
|
||||||
|
inv <- getInvitation alice
|
||||||
|
bob ##> ("/c " <> inv)
|
||||||
|
bob <## "confirmation sent!"
|
||||||
|
concurrently_
|
||||||
|
(bob <## "'Alice Jones': contact is connected")
|
||||||
|
(alice <## "'Bob James': contact is connected")
|
||||||
|
alice #> "@'Bob James' hi"
|
||||||
|
bob <# "'Alice Jones'> hi"
|
||||||
|
alice ##> "/g 'Our Team'"
|
||||||
|
alice <## "group #'Our Team' is created"
|
||||||
|
alice <## "to add members use /a 'Our Team' <name> or /create link #'Our Team'"
|
||||||
|
alice ##> "/a 'Our Team' 'Bob James' admin"
|
||||||
|
alice <## "invitation to join the group #'Our Team' sent to 'Bob James'"
|
||||||
|
bob <## "#'Our Team': 'Alice Jones' invites you to join the group as admin"
|
||||||
|
bob <## "use /j 'Our Team' to accept"
|
||||||
|
bob ##> "/j 'Our Team'"
|
||||||
|
bob <## "#'Our Team': you joined the group"
|
||||||
|
alice <## "#'Our Team': 'Bob James' joined the group"
|
||||||
|
bob ##> "/c"
|
||||||
|
inv' <- getInvitation bob
|
||||||
|
cath ##> ("/c " <> inv')
|
||||||
|
cath <## "confirmation sent!"
|
||||||
|
concurrently_
|
||||||
|
(cath <## "'Bob James': contact is connected")
|
||||||
|
(bob <## "'Cath Johnson': contact is connected")
|
||||||
|
bob ##> "/a 'Our Team' 'Cath Johnson'"
|
||||||
|
bob <## "invitation to join the group #'Our Team' sent to 'Cath Johnson'"
|
||||||
|
cath <## "#'Our Team': 'Bob James' invites you to join the group as member"
|
||||||
|
cath <## "use /j 'Our Team' to accept"
|
||||||
|
cath ##> "/j 'Our Team'"
|
||||||
|
concurrentlyN_
|
||||||
|
[ bob <## "#'Our Team': 'Cath Johnson' joined the group",
|
||||||
|
do
|
||||||
|
cath <## "#'Our Team': you joined the group"
|
||||||
|
cath <## "#'Our Team': member 'Alice Jones' is connected",
|
||||||
|
do
|
||||||
|
alice <## "#'Our Team': 'Bob James' added 'Cath Johnson' to the group (connecting...)"
|
||||||
|
alice <## "#'Our Team': new member 'Cath Johnson' is connected"
|
||||||
|
]
|
||||||
|
bob #> "#'Our Team' hi"
|
||||||
|
alice <# "#'Our Team' 'Bob James'> hi"
|
||||||
|
cath <# "#'Our Team' 'Bob James'> hi"
|
||||||
|
alice `send` "@'Cath Johnson' hello"
|
||||||
|
alice <## "member #'Our Team' 'Cath Johnson' does not have direct connection, creating"
|
||||||
|
alice <## "contact for member #'Our Team' 'Cath Johnson' is created"
|
||||||
|
alice <## "sent invitation to connect directly to member #'Our Team' 'Cath Johnson'"
|
||||||
|
alice <# "@'Cath Johnson' hello"
|
||||||
|
cath <## "#'Our Team' 'Alice Jones' is creating direct contact 'Alice Jones' with you"
|
||||||
|
cath <# "'Alice Jones'> hello"
|
||||||
|
cath <## "'Alice Jones': contact is connected"
|
||||||
|
alice <## "'Cath Johnson': contact is connected"
|
||||||
|
cath ##> "/p 'Cath J'"
|
||||||
|
cath <## "user profile is changed to 'Cath J' (your 2 contacts are notified)"
|
||||||
|
alice <## "contact 'Cath Johnson' changed to 'Cath J'"
|
||||||
|
alice <## "use @'Cath J' <message> to send messages"
|
||||||
|
bob <## "contact 'Cath Johnson' changed to 'Cath J'"
|
||||||
|
bob <## "use @'Cath J' <message> to send messages"
|
||||||
|
alice #> "@'Cath J' hi"
|
||||||
|
cath <# "'Alice Jones'> hi"
|
||||||
|
where
|
||||||
|
aliceProfile' = baseProfile {displayName = "Alice Jones"}
|
||||||
|
bobProfile' = baseProfile {displayName = "Bob James"}
|
||||||
|
cathProfile' = baseProfile {displayName = "Cath Johnson"}
|
||||||
|
baseProfile = Profile {displayName = "", fullName = "", image = Nothing, contactLink = Nothing, preferences = defaultPrefs}
|
||||||
|
|
||||||
testUserContactLink :: HasCallStack => FilePath -> IO ()
|
testUserContactLink :: HasCallStack => FilePath -> IO ()
|
||||||
testUserContactLink =
|
testUserContactLink =
|
||||||
testChat3 aliceProfile bobProfile cathProfile $
|
testChat3 aliceProfile bobProfile cathProfile $
|
||||||
|
|
|
@ -435,7 +435,7 @@ lastItemId cc = do
|
||||||
showActiveUser :: HasCallStack => TestCC -> String -> Expectation
|
showActiveUser :: HasCallStack => TestCC -> String -> Expectation
|
||||||
showActiveUser cc name = do
|
showActiveUser cc name = do
|
||||||
cc <## ("user profile: " <> name)
|
cc <## ("user profile: " <> name)
|
||||||
cc <## "use /p <display name> [<full name>] to change it"
|
cc <## "use /p <display name> to change it"
|
||||||
cc <## "(the updated profile will be sent to all your contacts)"
|
cc <## "(the updated profile will be sent to all your contacts)"
|
||||||
|
|
||||||
connectUsers :: HasCallStack => TestCC -> TestCC -> IO ()
|
connectUsers :: HasCallStack => TestCC -> TestCC -> IO ()
|
||||||
|
|
|
@ -61,6 +61,8 @@ mobileTests = do
|
||||||
it "utf8 name 1" $ testFileEncryptionCApi "тест"
|
it "utf8 name 1" $ testFileEncryptionCApi "тест"
|
||||||
it "utf8 name 2" $ testFileEncryptionCApi "👍"
|
it "utf8 name 2" $ testFileEncryptionCApi "👍"
|
||||||
it "no exception on missing file" testMissingFileEncryptionCApi
|
it "no exception on missing file" testMissingFileEncryptionCApi
|
||||||
|
describe "validate name" $ do
|
||||||
|
it "should convert invalid name to a valid name" testValidNameCApi
|
||||||
|
|
||||||
noActiveUser :: LB.ByteString
|
noActiveUser :: LB.ByteString
|
||||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||||
|
@ -266,6 +268,14 @@ testMissingFileEncryptionCApi tmp = do
|
||||||
err' <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath'
|
err' <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath'
|
||||||
err' `shouldContain` toPath
|
err' `shouldContain` toPath
|
||||||
|
|
||||||
|
testValidNameCApi :: FilePath -> IO ()
|
||||||
|
testValidNameCApi _ = do
|
||||||
|
let goodName = "Джон Доу 👍"
|
||||||
|
cName1 <- cChatValidName =<< newCString goodName
|
||||||
|
peekCString cName1 `shouldReturn` goodName
|
||||||
|
cName2 <- cChatValidName =<< newCString " @'Джон' Доу 👍 "
|
||||||
|
peekCString cName2 `shouldReturn` goodName
|
||||||
|
|
||||||
jDecode :: FromJSON a => String -> IO (Maybe a)
|
jDecode :: FromJSON a => String -> IO (Maybe a)
|
||||||
jDecode = pure . J.decode . LB.pack
|
jDecode = pure . J.decode . LB.pack
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import SchemaDump
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import UnliftIO.Temporary (withTempDirectory)
|
import UnliftIO.Temporary (withTempDirectory)
|
||||||
import ViewTests
|
import ViewTests
|
||||||
|
import ValidNames
|
||||||
import WebRTCTests
|
import WebRTCTests
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
|
@ -23,6 +24,7 @@ main = do
|
||||||
describe "SimpleX chat view" viewTests
|
describe "SimpleX chat view" viewTests
|
||||||
describe "SimpleX chat protocol" protocolTests
|
describe "SimpleX chat protocol" protocolTests
|
||||||
describe "WebRTC encryption" webRTCTests
|
describe "WebRTC encryption" webRTCTests
|
||||||
|
describe "Valid names" validNameTests
|
||||||
around testBracket $ do
|
around testBracket $ do
|
||||||
describe "Mobile API Tests" mobileTests
|
describe "Mobile API Tests" mobileTests
|
||||||
describe "SimpleX chat client" chatTests
|
describe "SimpleX chat client" chatTests
|
||||||
|
|
27
tests/ValidNames.hs
Normal file
27
tests/ValidNames.hs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
module ValidNames where
|
||||||
|
|
||||||
|
import Simplex.Chat
|
||||||
|
import Test.Hspec
|
||||||
|
|
||||||
|
validNameTests :: Spec
|
||||||
|
validNameTests = describe "valid chat names" $ do
|
||||||
|
it "should keep valid and fix invalid names" testMkValidName
|
||||||
|
|
||||||
|
testMkValidName :: IO ()
|
||||||
|
testMkValidName = do
|
||||||
|
mkValidName "alice" `shouldBe` "alice"
|
||||||
|
mkValidName "алиса" `shouldBe` "алиса"
|
||||||
|
mkValidName "John Doe" `shouldBe` "John Doe"
|
||||||
|
mkValidName "J.Doe" `shouldBe` "J.Doe"
|
||||||
|
mkValidName "J. Doe" `shouldBe` "J. Doe"
|
||||||
|
mkValidName "J..Doe" `shouldBe` "J.Doe"
|
||||||
|
mkValidName "J ..Doe" `shouldBe` "J Doe"
|
||||||
|
mkValidName "J . . Doe" `shouldBe` "J Doe"
|
||||||
|
mkValidName "@alice" `shouldBe` "alice"
|
||||||
|
mkValidName "#alice" `shouldBe` "alice"
|
||||||
|
mkValidName " alice" `shouldBe` "alice"
|
||||||
|
mkValidName "alice " `shouldBe` "alice"
|
||||||
|
mkValidName "John Doe" `shouldBe` "John Doe"
|
||||||
|
mkValidName "'John Doe'" `shouldBe` "John Doe"
|
||||||
|
mkValidName "\"John Doe\"" `shouldBe` "John Doe"
|
||||||
|
mkValidName "`John Doe`" `shouldBe` "John Doe"
|
Loading…
Add table
Add a link
Reference in a new issue