2022-01-28 10:41:09 +00:00
{- # LANGUAGE DataKinds # -}
2021-07-12 19:00:03 +01:00
{- # LANGUAGE DuplicateRecordFields # -}
2022-01-26 16:18:27 +04:00
{- # LANGUAGE GADTs # -}
2021-06-25 18:18:24 +01:00
{- # LANGUAGE LambdaCase # -}
2021-07-12 19:00:03 +01:00
{- # LANGUAGE NamedFieldPuns # -}
2021-06-25 18:18:24 +01:00
{- # LANGUAGE OverloadedStrings # -}
2022-01-24 16:07:17 +00:00
module Simplex.Chat.View where
2022-02-22 14:05:45 +00:00
import qualified Data.Aeson as J
2021-09-05 14:08:29 +01:00
import Data.Function ( on )
2021-09-04 07:32:56 +01:00
import Data.Int ( Int64 )
2022-02-25 16:29:36 +04:00
import Data.List ( groupBy , intersperse , partition , sortOn )
import Data.Maybe ( isJust )
2021-07-04 18:42:24 +01:00
import Data.Text ( Text )
2021-06-25 18:18:24 +01:00
import qualified Data.Text as T
2022-01-24 16:07:17 +00:00
import Data.Time.Clock ( DiffTime )
2021-06-25 18:18:24 +01:00
import Data.Time.Format ( defaultTimeLocale , formatTime )
2022-01-24 16:07:17 +00:00
import Data.Time.LocalTime ( ZonedTime ( .. ) , localDay , localTimeOfDay , timeOfDayToTime , utcToZonedTime )
2021-09-04 07:32:56 +01:00
import Numeric ( showFFloat )
2021-06-25 18:18:24 +01:00
import Simplex.Chat.Controller
2022-01-24 16:07:17 +00:00
import Simplex.Chat.Help
2021-06-25 18:18:24 +01:00
import Simplex.Chat.Markdown
2022-02-09 20:58:02 +04:00
import Simplex.Chat.Messages hiding ( NewChatItem ( .. ) )
2022-01-24 16:07:17 +00:00
import Simplex.Chat.Protocol
2021-07-12 19:00:03 +01:00
import Simplex.Chat.Store ( StoreError ( .. ) )
2021-06-25 18:18:24 +01:00
import Simplex.Chat.Styled
2021-07-04 18:42:24 +01:00
import Simplex.Chat.Types
2021-06-25 18:18:24 +01:00
import Simplex.Messaging.Agent.Protocol
2022-01-11 08:50:44 +00:00
import Simplex.Messaging.Encoding.String
2021-12-08 13:09:51 +00:00
import qualified Simplex.Messaging.Protocol as SMP
2022-02-22 14:05:45 +00:00
import Simplex.Messaging.Util ( bshow )
2021-06-25 18:18:24 +01:00
import System.Console.ANSI.Types
2022-01-24 16:07:17 +00:00
serializeChatResponse :: ChatResponse -> String
2022-02-21 12:05:00 +00:00
serializeChatResponse = unlines . map unStyle . responseToView False
2022-01-24 16:07:17 +00:00
2022-02-21 12:05:00 +00:00
responseToView :: Bool -> ChatResponse -> [ StyledString ]
responseToView testView = \ case
CRActiveUser User { profile } -> viewUserProfile profile
CRChatStarted -> [ " chat started " ]
2022-02-26 20:21:32 +00:00
CRChatRunning -> []
2022-02-22 14:05:45 +00:00
CRApiChats chats -> if testView then testViewChats chats else [ plain . bshow $ J . encode chats ]
CRApiChat chat -> if testView then testViewChat chat else [ plain . bshow $ J . encode chat ]
2022-01-26 16:18:27 +04:00
CRNewChatItem ( AChatItem _ _ chat item ) -> viewChatItem chat item
2022-02-07 15:19:34 +04:00
CRChatItemUpdated _ -> []
2022-02-02 11:43:52 +00:00
CRMsgIntegrityError mErr -> viewMsgIntegrityError mErr
2022-02-21 12:05:00 +00:00
CRCmdAccepted _ -> []
CRCmdOk -> [ " ok " ]
2022-01-24 16:07:17 +00:00
CRChatHelp section -> case section of
2022-02-21 12:05:00 +00:00
HSMain -> chatHelpInfo
HSFiles -> filesHelpInfo
HSGroups -> groupsHelpInfo
HSMyAddress -> myAddressHelpInfo
HSMarkdown -> markdownInfo
CRWelcome user -> chatWelcome user
CRContactsList cs -> viewContactsList cs
CRUserContactLink cReqUri _ -> connReqContact_ " Your chat address: " cReqUri
CRUserContactLinkUpdated _ autoAccept -> [ " auto_accept " <> if autoAccept then " on " else " off " ]
CRContactRequestRejected UserContactRequest { localDisplayName = c } -> [ ttyContact c <> " : contact request rejected " ]
CRGroupCreated g -> viewGroupCreated g
CRGroupMembers g -> viewGroupMembers g
CRGroupsList gs -> viewGroupsList gs
CRSentGroupInvitation g c -> [ " invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c ]
CRFileTransferStatus ftStatus -> viewFileTransferStatus ftStatus
CRUserProfile p -> viewUserProfile p
CRUserProfileNoChange -> [ " user profile did not change " ]
CRVersionInfo _ -> [ plain versionStr , plain updateStr ]
CRChatCmdError e -> viewChatError e
CRInvitation cReq -> viewConnReqInvitation cReq
CRSentConfirmation -> [ " confirmation sent! " ]
CRSentInvitation -> [ " connection request sent! " ]
CRContactDeleted c -> [ ttyContact' c <> " : contact is deleted " ]
CRAcceptingContactRequest c -> [ ttyFullContact c <> " : accepting contact request... " ]
CRContactAlreadyExists c -> [ ttyFullContact c <> " : contact already exists " ]
CRContactRequestAlreadyAccepted c -> [ ttyFullContact c <> " : sent you a duplicate contact request, but you are already connected, no action needed " ]
CRUserContactLinkCreated cReq -> connReqContact_ " Your new chat address is created! " cReq
CRUserContactLinkDeleted -> viewUserContactLinkDeleted
CRUserAcceptedGroupSent _g -> [] -- [ttyGroup' g <> ": joining the group..."]
CRUserDeletedMember g m -> [ ttyGroup' g <> " : you removed " <> ttyMember m <> " from the group " ]
CRLeftMemberUser g -> [ ttyGroup' g <> " : you left the group " ] <> groupPreserved g
CRGroupDeletedUser g -> [ ttyGroup' g <> " : you deleted the group " ]
2022-01-24 16:07:17 +00:00
CRRcvFileAccepted RcvFileTransfer { fileId , senderDisplayName = c } filePath ->
2022-02-21 12:05:00 +00:00
[ " saving file " <> sShow fileId <> " from " <> ttyContact c <> " to " <> plain filePath ]
CRRcvFileAcceptedSndCancelled ft -> viewRcvFileSndCancelled ft
CRSndGroupFileCancelled fts -> viewSndGroupFileCancelled fts
CRRcvFileCancelled ft -> receivingFile_ " cancelled " ft
CRUserProfileUpdated p p' -> viewUserProfileUpdated p p'
2022-01-24 16:07:17 +00:00
CRContactUpdated c c' -> viewContactUpdated c c'
CRContactsMerged intoCt mergedCt -> viewContactsMerged intoCt mergedCt
2022-01-31 21:53:53 +04:00
CRReceivedContactRequest UserContactRequest { localDisplayName = c , profile } -> viewReceivedContactRequest c profile
2022-01-24 16:07:17 +00:00
CRRcvFileStart ft -> receivingFile_ " started " ft
CRRcvFileComplete ft -> receivingFile_ " completed " ft
CRRcvFileSndCancelled ft -> viewRcvFileSndCancelled ft
CRSndFileStart ft -> sendingFile_ " started " ft
CRSndFileComplete ft -> sendingFile_ " completed " ft
CRSndFileCancelled ft -> sendingFile_ " cancelled " ft
CRSndFileRcvCancelled ft @ SndFileTransfer { recipientDisplayName = c } ->
[ ttyContact c <> " cancelled receiving " <> sndFile ft ]
2022-02-08 13:04:17 +04:00
CRContactConnecting _ -> []
2022-01-24 16:07:17 +00:00
CRContactConnected ct -> [ ttyFullContact ct <> " : contact is connected " ]
2022-01-26 16:18:27 +04:00
CRContactAnotherClient c -> [ ttyContact' c <> " : contact is connected to another client " ]
CRContactDisconnected c -> [ ttyContact' c <> " : disconnected from server (messages will be queued) " ]
CRContactSubscribed c -> [ ttyContact' c <> " : connected to server " ]
CRContactSubError c e -> [ ttyContact' c <> " : contact error " <> sShow e ]
2022-02-25 16:29:36 +04:00
CRContactSubSummary summary ->
2022-02-26 20:21:32 +00:00
( if null subscribed then [] else [ sShow ( length subscribed ) <> " contacts connected (use " <> highlight' " /cs " <> " for the list) " ] ) <> viewErrorsSummary errors " contact errors "
2022-02-25 16:29:36 +04:00
where
2022-02-26 20:21:32 +00:00
( errors , subscribed ) = partition ( isJust . contactError ) summary
2022-01-26 16:18:27 +04:00
CRGroupInvitation GroupInfo { localDisplayName = ldn , groupProfile = GroupProfile { fullName } } ->
2022-01-24 16:07:17 +00:00
[ groupInvitation ldn fullName ]
CRReceivedGroupInvitation g c role -> viewReceivedGroupInvitation g c role
2022-01-26 16:18:27 +04:00
CRUserJoinedGroup g -> [ ttyGroup' g <> " : you joined the group " ]
CRJoinedGroupMember g m -> [ ttyGroup' g <> " : " <> ttyMember m <> " joined the group " ]
CRJoinedGroupMemberConnecting g host m -> [ ttyGroup' g <> " : " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...) " ]
CRConnectedToGroupMember g m -> [ ttyGroup' g <> " : " <> connectedMember m <> " is connected " ]
CRDeletedMemberUser g by -> [ ttyGroup' g <> " : " <> ttyMember by <> " removed you from the group " ] <> groupPreserved g
CRDeletedMember g by m -> [ ttyGroup' g <> " : " <> ttyMember by <> " removed " <> ttyMember m <> " from the group " ]
CRLeftMember g m -> [ ttyGroup' g <> " : " <> ttyMember m <> " left the group " ]
2022-01-24 16:07:17 +00:00
CRGroupEmpty g -> [ ttyFullGroup g <> " : group is empty " ]
CRGroupRemoved g -> [ ttyFullGroup g <> " : you are no longer a member or group deleted " ]
2022-01-27 22:01:15 +00:00
CRGroupDeleted g m -> [ ttyGroup' g <> " : " <> ttyMember m <> " deleted the group " , " use " <> highlight ( " /d # " <> groupName' g ) <> " to delete the local copy of the group " ]
2022-01-26 16:18:27 +04:00
CRMemberSubError g c e -> [ ttyGroup' g <> " member " <> ttyContact c <> " error: " <> sShow e ]
2022-02-25 16:29:36 +04:00
CRMemberSubErrors summary -> viewErrorsSummary summary " group member errors "
2022-01-24 16:07:17 +00:00
CRGroupSubscribed g -> [ ttyFullGroup g <> " : connected to server(s) " ]
2022-02-27 18:16:38 +00:00
CRPendingSubSummary _ -> []
2022-01-24 16:07:17 +00:00
CRSndFileSubError SndFileTransfer { fileId , fileName } e ->
[ " sent file " <> sShow fileId <> " ( " <> plain fileName <> " ) error: " <> sShow e ]
CRRcvFileSubError RcvFileTransfer { fileId , fileInvitation = FileInvitation { fileName } } e ->
[ " received file " <> sShow fileId <> " ( " <> plain fileName <> " ) error: " <> sShow e ]
CRUserContactLinkSubscribed -> [ " Your address is active! To show: " <> highlight' " /sa " ]
CRUserContactLinkSubError e -> [ " user address error: " <> sShow e , " to delete your address: " <> highlight' " /da " ]
CRMessageError prefix err -> [ plain prefix <> " : " <> plain err ]
CRChatError e -> viewChatError e
where
2022-02-09 20:58:02 +04:00
testViewChats :: [ AChat ] -> [ StyledString ]
testViewChats chats = [ sShow $ map toChatView chats ]
where
toChatView :: AChat -> ( Text , Text )
toChatView ( AChat _ ( Chat ( DirectChat Contact { localDisplayName } ) items _ ) ) = ( " @ " <> localDisplayName , toCIPreview items )
toChatView ( AChat _ ( Chat ( GroupChat GroupInfo { localDisplayName } ) items _ ) ) = ( " # " <> localDisplayName , toCIPreview items )
toChatView ( AChat _ ( Chat ( ContactRequest UserContactRequest { localDisplayName } ) items _ ) ) = ( " <@ " <> localDisplayName , toCIPreview items )
toCIPreview :: [ CChatItem c ] -> Text
toCIPreview ( ( CChatItem _ ChatItem { meta } ) : _ ) = itemText meta
toCIPreview _ = " "
testViewChat :: AChat -> [ StyledString ]
testViewChat ( AChat _ Chat { chatItems } ) = [ sShow $ map toChatView chatItems ]
where
toChatView :: CChatItem c -> ( Int , Text )
toChatView ( CChatItem dir ChatItem { meta } ) = ( msgDirectionInt $ toMsgDirection dir , itemText meta )
2022-02-25 16:29:36 +04:00
viewErrorsSummary :: [ a ] -> StyledString -> [ StyledString ]
viewErrorsSummary summary s = if null summary then [] else [ styled ( colored Red ) ( T . pack . show $ length summary ) <> s <> " (run with -c option to show each error) " ]
2021-06-25 18:18:24 +01:00
2022-01-26 16:18:27 +04:00
viewChatItem :: ChatInfo c -> ChatItem c d -> [ StyledString ]
2022-02-22 14:05:45 +00:00
viewChatItem chat ( ChatItem cd meta content _ ) = case ( chat , cd ) of
2022-01-28 10:41:09 +00:00
( DirectChat c , CIDirectSnd ) -> case content of
CISndMsgContent mc -> viewSentMessage to mc meta
CISndFileInvitation fId fPath -> viewSentFileInvitation to fId fPath meta
2022-01-26 16:18:27 +04:00
where
to = ttyToContact' c
2022-01-28 10:41:09 +00:00
( DirectChat c , CIDirectRcv ) -> case content of
CIRcvMsgContent mc -> viewReceivedMessage from meta mc -- mOk
CIRcvFileInvitation ft -> viewReceivedFileInvitation from meta ft -- mOk
where
2022-01-26 16:18:27 +04:00
from = ttyFromContact' c
2022-01-28 10:41:09 +00:00
( GroupChat g , CIGroupSnd ) -> case content of
CISndMsgContent mc -> viewSentMessage to mc meta
2022-01-26 16:18:27 +04:00
CISndFileInvitation fId fPath -> viewSentFileInvitation to fId fPath meta
where
to = ttyToGroup g
2022-01-28 10:41:09 +00:00
( GroupChat g , CIGroupRcv m ) -> case content of
CIRcvMsgContent mc -> viewReceivedMessage from meta mc -- mOk
2022-01-28 11:52:10 +04:00
CIRcvFileInvitation ft -> viewReceivedFileInvitation from meta ft -- mOk
2022-01-26 16:18:27 +04:00
where
2022-01-28 10:41:09 +00:00
from = ttyFromGroup' g m
2022-01-26 16:18:27 +04:00
where
ttyToContact' Contact { localDisplayName = c } = ttyToContact c
ttyFromContact' Contact { localDisplayName = c } = ttyFromContact c
2022-01-28 10:41:09 +00:00
ttyFromGroup' g GroupMember { localDisplayName = m } = ttyFromGroup g m
2022-01-26 16:18:27 +04:00
2022-02-02 11:43:52 +00:00
viewMsgIntegrityError :: MsgErrorType -> [ StyledString ]
viewMsgIntegrityError err = msgError $ case err of
MsgSkipped fromId toId ->
" skipped message ID " <> show fromId
<> if fromId == toId then " " else " .. " <> show toId
MsgBadId msgId -> " unexpected message ID " <> show msgId
MsgBadHash -> " incorrect message hash "
MsgDuplicate -> " duplicate message ID "
where
msgError :: String -> [ StyledString ]
2022-02-22 14:05:45 +00:00
msgError s = [ styled ( colored Red ) s ]
2022-02-02 11:43:52 +00:00
2022-01-21 11:09:33 +00:00
viewInvalidConnReq :: [ StyledString ]
viewInvalidConnReq =
[ " " ,
" Connection link is invalid, possibly it was created in a previous version. " ,
" Please ask your contact to check " <> highlight' " /version " <> " and update if needed. " ,
plain updateStr
]
2021-08-22 15:56:36 +01:00
2022-01-21 11:09:33 +00:00
viewConnReqInvitation :: ConnReqInvitation -> [ StyledString ]
viewConnReqInvitation cReq =
2021-12-08 13:09:51 +00:00
[ " pass this invitation link to your contact (via another channel): " ,
2021-06-25 18:18:24 +01:00
" " ,
2022-01-11 08:50:44 +00:00
( plain . strEncode ) cReq ,
2021-06-25 18:18:24 +01:00
" " ,
2021-12-08 13:09:51 +00:00
" and ask them to connect: " <> highlight' " /c <invitation_link_above> "
2021-06-25 18:18:24 +01:00
]
2022-01-21 11:09:33 +00:00
viewContactsList :: [ Contact ] -> [ StyledString ]
viewContactsList =
2021-12-10 11:45:58 +00:00
let ldn = T . toLower . ( localDisplayName :: Contact -> ContactName )
in map ttyFullContact . sortOn ldn
2022-01-21 11:09:33 +00:00
viewUserContactLinkDeleted :: [ StyledString ]
viewUserContactLinkDeleted =
2021-12-08 13:09:51 +00:00
[ " Your chat address is deleted - accepted contacts will remain connected. " ,
" To create a new chat address use " <> highlight' " /ad "
]
connReqContact_ :: StyledString -> ConnReqContact -> [ StyledString ]
connReqContact_ intro cReq =
[ intro ,
" " ,
2022-01-11 08:50:44 +00:00
( plain . strEncode ) cReq ,
2021-12-08 13:09:51 +00:00
" " ,
" Anybody can send you contact requests with: " <> highlight' " /c <contact_link_above> " ,
" to show it again: " <> highlight' " /sa " ,
" to delete it: " <> highlight' " /da " <> " (accepted contacts will remain connected) "
]
2022-01-21 11:09:33 +00:00
viewReceivedContactRequest :: ContactName -> Profile -> [ StyledString ]
viewReceivedContactRequest c Profile { fullName } =
2021-12-08 13:09:51 +00:00
[ ttyFullName c fullName <> " wants to connect to you! " ,
" to accept: " <> highlight ( " /ac " <> c ) ,
" to reject: " <> highlight ( " /rc " <> c ) <> " (the sender will NOT be notified) "
]
2022-01-26 16:18:27 +04:00
viewGroupCreated :: GroupInfo -> [ StyledString ]
viewGroupCreated g @ GroupInfo { localDisplayName } =
2021-07-16 07:40:55 +01:00
[ " group " <> ttyFullGroup g <> " is created " ,
2021-07-24 10:26:28 +01:00
" use " <> highlight ( " /a " <> localDisplayName <> " <name> " ) <> " to add members "
2021-07-16 07:40:55 +01:00
]
2022-01-26 16:18:27 +04:00
viewCannotResendInvitation :: GroupInfo -> ContactName -> [ StyledString ]
viewCannotResendInvitation GroupInfo { localDisplayName = gn } c =
[ ttyContact c <> " is already invited to group " <> ttyGroup gn ,
" to re-send invitation: " <> highlight ( " /rm " <> gn <> " " <> c ) <> " , " <> highlight ( " /a " <> gn <> " " <> c )
2022-01-06 23:39:58 +04:00
]
2022-01-26 16:18:27 +04:00
viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [ StyledString ]
viewReceivedGroupInvitation g c role =
[ ttyFullGroup g <> " : " <> ttyContact' c <> " invites you to join the group as " <> plain ( strEncode role ) ,
2022-01-27 22:01:15 +00:00
" use " <> highlight ( " /j " <> groupName' g ) <> " to accept "
2021-07-16 07:40:55 +01:00
]
2022-01-26 16:18:27 +04:00
groupPreserved :: GroupInfo -> [ StyledString ]
2022-01-27 22:01:15 +00:00
groupPreserved g = [ " use " <> highlight ( " /d # " <> groupName' g ) <> " to delete the group " ]
2021-08-02 20:10:24 +01:00
2021-07-24 10:26:28 +01:00
connectedMember :: GroupMember -> StyledString
connectedMember m = case memberCategory m of
GCPreMember -> " member " <> ttyFullMember m
GCPostMember -> " new member " <> ttyMember m -- without fullName as as it was shown in joinedGroupMemberConnecting
_ -> " member " <> ttyMember m -- these case is not used
2022-01-21 11:09:33 +00:00
viewGroupMembers :: Group -> [ StyledString ]
2022-01-26 16:18:27 +04:00
viewGroupMembers ( Group GroupInfo { membership } members ) = map groupMember . filter ( not . removedOrLeft ) $ membership : members
2021-07-27 08:08:05 +01:00
where
removedOrLeft m = let s = memberStatus m in s == GSMemRemoved || s == GSMemLeft
groupMember m = ttyFullMember m <> " : " <> role m <> " , " <> category m <> status m
2022-01-11 08:50:44 +00:00
role m = plain . strEncode $ memberRole ( m :: GroupMember )
2021-07-27 08:08:05 +01:00
category m = case memberCategory m of
GCUserMember -> " you, "
GCInviteeMember -> " invited, "
GCHostMember -> " host, "
_ -> " "
status m = case memberStatus m of
GSMemRemoved -> " removed "
GSMemLeft -> " left "
GSMemInvited -> " not yet joined "
GSMemConnected -> " connected "
GSMemComplete -> " connected "
GSMemCreator -> " created group "
_ -> " "
2022-01-24 16:07:17 +00:00
viewGroupsList :: [ GroupInfo ] -> [ StyledString ]
2022-01-21 11:09:33 +00:00
viewGroupsList [] = [ " you have no groups! " , " to create: " <> highlight' " /g <name> " ]
2022-01-24 16:07:17 +00:00
viewGroupsList gs = map groupSS $ sortOn ldn_ gs
2021-12-18 10:23:47 +00:00
where
2022-01-24 16:07:17 +00:00
ldn_ = T . toLower . ( localDisplayName :: GroupInfo -> GroupName )
2022-01-26 16:18:27 +04:00
groupSS GroupInfo { localDisplayName = ldn , groupProfile = GroupProfile { fullName } , membership } =
case memberStatus membership of
2022-01-24 16:07:17 +00:00
GSMemInvited -> groupInvitation ldn fullName
_ -> ttyGroup ldn <> optFullName ldn fullName
2022-01-21 11:09:33 +00:00
2022-01-06 23:39:58 +04:00
groupInvitation :: GroupName -> Text -> StyledString
groupInvitation displayName fullName =
2022-01-06 14:24:33 +04:00
highlight ( " # " <> displayName )
<> optFullName displayName fullName
<> " - you are invited ( "
<> highlight ( " /j " <> displayName )
<> " to join, "
<> highlight ( " /d # " <> displayName )
<> " to delete invitation) "
2021-12-10 11:45:58 +00:00
2022-01-21 11:09:33 +00:00
viewContactsMerged :: Contact -> Contact -> [ StyledString ]
2022-01-24 16:07:17 +00:00
viewContactsMerged _into @ Contact { localDisplayName = c1 } _merged @ Contact { localDisplayName = c2 } =
2021-07-27 08:08:05 +01:00
[ " contact " <> ttyContact c2 <> " is merged into " <> ttyContact c1 ,
" use " <> ttyToContact c1 <> highlight' " <message> " <> " to send messages "
]
2022-01-21 11:09:33 +00:00
viewUserProfile :: Profile -> [ StyledString ]
viewUserProfile Profile { displayName , fullName } =
2021-08-22 15:56:36 +01:00
[ " user profile: " <> ttyFullName displayName fullName ,
2021-09-04 07:32:56 +01:00
" use " <> highlight' " /p <display name> [<full name>] " <> " to change it " ,
2021-08-22 15:56:36 +01:00
" (the updated profile will be sent to all your contacts) "
]
2022-01-24 16:07:17 +00:00
viewUserProfileUpdated :: Profile -> Profile -> [ StyledString ]
viewUserProfileUpdated Profile { displayName = n , fullName } Profile { displayName = n' , fullName = fullName' }
| n == n' && fullName == fullName' = []
| n == n' = [ " user full name " <> ( if T . null fullName' || fullName' == n' then " removed " else " changed to " <> plain fullName' ) <> notified ]
| otherwise = [ " user profile is changed to " <> ttyFullName n' fullName' <> notified ]
where
notified = " (your contacts are notified) "
2021-08-22 15:56:36 +01:00
2022-01-21 11:09:33 +00:00
viewContactUpdated :: Contact -> Contact -> [ StyledString ]
viewContactUpdated
2021-08-22 15:56:36 +01:00
Contact { localDisplayName = n , profile = Profile { fullName } }
Contact { localDisplayName = n' , profile = Profile { fullName = fullName' } }
| n == n' && fullName == fullName' = []
| n == n' = [ " contact " <> ttyContact n <> fullNameUpdate ]
| otherwise =
[ " contact " <> ttyContact n <> " changed to " <> ttyFullName n' fullName' ,
" use " <> ttyToContact n' <> highlight' " <message> " <> " to send messages "
]
where
fullNameUpdate = if T . null fullName' || fullName' == n' then " removed full name " else " updated full name: " <> plain fullName'
2022-02-07 15:19:34 +04:00
viewReceivedMessage :: StyledString -> CIMeta d -> MsgContent -> [ StyledString ]
2022-01-24 16:07:17 +00:00
viewReceivedMessage from meta mc = receivedWithTime_ from meta ( ttyMsgContent mc )
2021-09-04 07:32:56 +01:00
2022-02-07 15:19:34 +04:00
receivedWithTime_ :: StyledString -> CIMeta d -> [ StyledString ] -> [ StyledString ]
2022-01-28 10:41:09 +00:00
receivedWithTime_ from CIMeta { localItemTs , createdAt } styledMsg = do
2022-01-28 11:52:10 +04:00
prependFirst ( formattedTime <> " " <> from ) styledMsg -- ++ showIntegrity mOk
2021-06-25 18:18:24 +01:00
where
2022-01-24 16:07:17 +00:00
formattedTime :: StyledString
formattedTime =
2022-01-26 16:18:27 +04:00
let localTime = zonedTimeToLocalTime localItemTs
tz = zonedTimeZone localItemTs
2021-06-25 18:18:24 +01:00
format =
2022-01-24 16:07:17 +00:00
if ( localDay localTime < localDay ( zonedTimeToLocalTime $ utcToZonedTime tz createdAt ) )
2021-06-25 18:18:24 +01:00
&& ( timeOfDayToTime ( localTimeOfDay localTime ) > ( 6 * 60 * 60 :: DiffTime ) )
then " %m-%d " -- if message is from yesterday or before and 6 hours has passed since midnight
else " %H:%M "
in styleTime $ formatTime defaultTimeLocale format localTime
2022-02-07 15:19:34 +04:00
viewSentMessage :: StyledString -> MsgContent -> CIMeta d -> [ StyledString ]
2022-01-24 16:07:17 +00:00
viewSentMessage to = sentWithTime_ . prependFirst to . ttyMsgContent
2022-01-21 11:09:33 +00:00
2022-02-07 15:19:34 +04:00
viewSentFileInvitation :: StyledString -> FileTransferId -> FilePath -> CIMeta d -> [ StyledString ]
2022-01-24 16:07:17 +00:00
viewSentFileInvitation to fId fPath = sentWithTime_ $ ttySentFile to fId fPath
2022-01-21 11:09:33 +00:00
2022-02-07 15:19:34 +04:00
sentWithTime_ :: [ StyledString ] -> CIMeta d -> [ StyledString ]
2022-01-28 10:41:09 +00:00
sentWithTime_ styledMsg CIMeta { localItemTs } =
2022-01-26 16:18:27 +04:00
prependFirst ( ttyMsgTime localItemTs <> " " ) styledMsg
2022-01-21 11:09:33 +00:00
2022-01-24 16:07:17 +00:00
ttyMsgTime :: ZonedTime -> StyledString
ttyMsgTime = styleTime . formatTime defaultTimeLocale " %H:%M "
2021-09-05 14:08:29 +01:00
2022-01-24 16:07:17 +00:00
ttyMsgContent :: MsgContent -> [ StyledString ]
ttyMsgContent = \ case
MCText t -> msgPlain t
2022-03-03 08:32:25 +00:00
MCUnknown _ t -> msgPlain t
2021-09-05 14:08:29 +01:00
2022-01-24 16:07:17 +00:00
ttySentFile :: StyledString -> FileTransferId -> FilePath -> [ StyledString ]
ttySentFile to fId fPath = [ " /f " <> to <> ttyFilePath fPath , " use " <> highlight ( " /fc " <> show fId ) <> " to cancel sending " ]
2021-06-25 18:18:24 +01:00
prependFirst :: StyledString -> [ StyledString ] -> [ StyledString ]
prependFirst s [] = [ s ]
prependFirst s ( s' : ss ) = ( s <> s' ) : ss
2021-07-04 18:42:24 +01:00
msgPlain :: Text -> [ StyledString ]
2022-02-22 14:05:45 +00:00
msgPlain = map ( styleMarkdownList . parseMarkdownList ) . T . lines
2021-07-04 18:42:24 +01:00
2022-01-24 16:07:17 +00:00
viewRcvFileSndCancelled :: RcvFileTransfer -> [ StyledString ]
viewRcvFileSndCancelled ft @ RcvFileTransfer { senderDisplayName = c } =
[ ttyContact c <> " cancelled sending " <> rcvFile ft ]
2021-09-04 07:32:56 +01:00
2022-01-21 11:09:33 +00:00
viewSndGroupFileCancelled :: [ SndFileTransfer ] -> [ StyledString ]
viewSndGroupFileCancelled fts =
2021-09-05 14:08:29 +01:00
case filter ( \ SndFileTransfer { fileStatus = s } -> s /= FSCancelled && s /= FSComplete ) fts of
[] -> [ " sending file can't be cancelled " ]
ts @ ( ft : _ ) -> [ " cancelled sending " <> sndFile ft <> " to " <> listMembers ts ]
2021-09-04 07:32:56 +01:00
2021-09-05 14:08:29 +01:00
sendingFile_ :: StyledString -> SndFileTransfer -> [ StyledString ]
sendingFile_ status ft @ SndFileTransfer { recipientDisplayName = c } =
[ status <> " sending " <> sndFile ft <> " to " <> ttyContact c ]
2021-09-04 07:32:56 +01:00
2021-09-05 14:08:29 +01:00
sndFile :: SndFileTransfer -> StyledString
2022-01-26 21:20:08 +00:00
sndFile SndFileTransfer { fileId , fileName } = fileTransferStr fileId fileName
2021-09-05 14:08:29 +01:00
2022-02-07 15:19:34 +04:00
viewReceivedFileInvitation :: StyledString -> CIMeta d -> RcvFileTransfer -> [ StyledString ]
2022-01-24 16:07:17 +00:00
viewReceivedFileInvitation from meta ft = receivedWithTime_ from meta ( receivedFileInvitation_ ft )
2022-01-21 11:09:33 +00:00
receivedFileInvitation_ :: RcvFileTransfer -> [ StyledString ]
receivedFileInvitation_ RcvFileTransfer { fileId , fileInvitation = FileInvitation { fileName , fileSize } } =
2021-09-05 14:08:29 +01:00
[ " sends file " <> ttyFilePath fileName <> " ( " <> humanReadableSize fileSize <> " / " <> sShow fileSize <> " bytes) " ,
2021-09-04 07:32:56 +01:00
" use " <> highlight ( " /fr " <> show fileId <> " [<dir>/ | <path>] " ) <> " to receive it "
]
humanReadableSize :: Integer -> StyledString
humanReadableSize size
| size < kB = sShow size <> " bytes "
| size < mB = hrSize kB " KiB "
| size < gB = hrSize mB " MiB "
| otherwise = hrSize gB " GiB "
where
hrSize sB name = plain $ unwords [ showFFloat ( Just 1 ) ( fromIntegral size / ( fromIntegral sB :: Double ) ) " " , name ]
kB = 1024
mB = kB * 1024
gB = mB * 1024
2021-09-05 14:08:29 +01:00
receivingFile_ :: StyledString -> RcvFileTransfer -> [ StyledString ]
receivingFile_ status ft @ RcvFileTransfer { senderDisplayName = c } =
[ status <> " receiving " <> rcvFile ft <> " from " <> ttyContact c ]
2021-09-04 07:32:56 +01:00
2021-09-05 14:08:29 +01:00
rcvFile :: RcvFileTransfer -> StyledString
2022-01-26 21:20:08 +00:00
rcvFile RcvFileTransfer { fileId , fileInvitation = FileInvitation { fileName } } = fileTransferStr fileId fileName
2021-09-05 14:08:29 +01:00
2022-01-26 21:20:08 +00:00
fileTransferStr :: Int64 -> String -> StyledString
fileTransferStr fileId fileName = " file " <> sShow fileId <> " ( " <> ttyFilePath fileName <> " ) "
2021-09-04 07:32:56 +01:00
2022-01-21 11:09:33 +00:00
viewFileTransferStatus :: ( FileTransfer , [ Integer ] ) -> [ StyledString ]
viewFileTransferStatus ( FTSnd [ ft @ SndFileTransfer { fileStatus , fileSize , chunkSize } ] , chunksNum ) =
2021-09-05 14:08:29 +01:00
[ " sending " <> sndFile ft <> " " <> sndStatus ]
2021-09-04 07:32:56 +01:00
where
sndStatus = case fileStatus of
2021-09-05 14:08:29 +01:00
FSNew -> " not accepted yet "
2021-09-04 07:32:56 +01:00
FSAccepted -> " just started "
2021-09-05 14:08:29 +01:00
FSConnected -> " progress " <> fileProgress chunksNum chunkSize fileSize
FSComplete -> " complete "
FSCancelled -> " cancelled "
2022-01-21 11:09:33 +00:00
viewFileTransferStatus ( FTSnd [] , _ ) = [ " no file transfers (empty group) " ]
viewFileTransferStatus ( FTSnd fts @ ( ft : _ ) , chunksNum ) =
2021-09-05 14:08:29 +01:00
case concatMap membersTransferStatus $ groupBy ( ( == ) ` on ` fs ) $ sortOn fs fts of
[ membersStatus ] -> [ " sending " <> sndFile ft <> " " <> membersStatus ]
membersStatuses -> ( " sending " <> sndFile ft <> " : " ) : map ( " " <> ) membersStatuses
where
fs = fileStatus :: SndFileTransfer -> FileStatus
membersTransferStatus [] = []
membersTransferStatus ts @ ( SndFileTransfer { fileStatus , fileSize , chunkSize } : _ ) = [ sndStatus <> " : " <> listMembers ts ]
where
sndStatus = case fileStatus of
FSNew -> " not accepted "
FSAccepted -> " just started "
FSConnected -> " in progress ( " <> sShow ( sum chunksNum * chunkSize * 100 ` div ` ( toInteger ( length chunksNum ) * fileSize ) ) <> " %) "
FSComplete -> " complete "
FSCancelled -> " cancelled "
2022-01-21 11:09:33 +00:00
viewFileTransferStatus ( FTRcv ft @ RcvFileTransfer { fileId , fileInvitation = FileInvitation { fileSize } , fileStatus , chunkSize } , chunksNum ) =
2021-09-05 14:08:29 +01:00
[ " receiving " <> rcvFile ft <> " " <> rcvStatus ]
2021-09-04 07:32:56 +01:00
where
rcvStatus = case fileStatus of
2021-09-05 14:08:29 +01:00
RFSNew -> " not accepted yet, use " <> highlight ( " /fr " <> show fileId ) <> " to receive file "
2021-09-04 07:32:56 +01:00
RFSAccepted _ -> " just started "
2021-09-05 14:08:29 +01:00
RFSConnected _ -> " progress " <> fileProgress chunksNum chunkSize fileSize
RFSComplete RcvFileInfo { filePath } -> " complete, path: " <> plain filePath
RFSCancelled RcvFileInfo { filePath } -> " cancelled, received part path: " <> plain filePath
listMembers :: [ SndFileTransfer ] -> StyledString
listMembers = mconcat . intersperse " , " . map ( ttyContact . recipientDisplayName )
2021-09-04 07:32:56 +01:00
fileProgress :: [ Integer ] -> Integer -> Integer -> StyledString
fileProgress chunksNum chunkSize fileSize =
sShow ( sum chunksNum * chunkSize * 100 ` div ` fileSize ) <> " % of " <> humanReadableSize fileSize
2022-01-21 11:09:33 +00:00
viewChatError :: ChatError -> [ StyledString ]
viewChatError = \ case
2021-07-16 07:40:55 +01:00
ChatError err -> case err of
2022-02-06 16:18:01 +00:00
CENoActiveUser -> [ " error: active user is required " ]
CEActiveUserExists -> [ " error: active user already exists " ]
CEChatNotStarted -> [ " error: chat not started " ]
2022-01-24 16:07:17 +00:00
CEInvalidConnReq -> viewInvalidConnReq
2022-02-06 16:18:01 +00:00
CEInvalidChatMessage e -> [ " chat message error: " <> sShow e ]
2022-02-14 18:49:42 +04:00
CEContactNotReady c -> [ ttyContact' c <> " : not ready " ]
CEContactGroups c gNames -> [ ttyContact' c <> " : contact cannot be deleted, it is a member of the group(s) " <> ttyGroups gNames ]
2021-07-16 07:40:55 +01:00
CEGroupDuplicateMember c -> [ " contact " <> ttyContact c <> " is already in the group " ]
CEGroupDuplicateMemberId -> [ " cannot add member - duplicate member ID " ]
2021-07-24 10:26:28 +01:00
CEGroupUserRole -> [ " you have insufficient permissions for this group command " ]
CEGroupContactRole c -> [ " contact " <> ttyContact c <> " has insufficient permissions for this group action " ]
2022-01-27 22:01:15 +00:00
CEGroupNotJoined g -> [ " you did not join this group, use " <> highlight ( " /join # " <> groupName' g ) ]
2021-07-24 10:26:28 +01:00
CEGroupMemberNotActive -> [ " you cannot invite other members yet, try later " ]
2022-01-05 20:46:35 +04:00
CEGroupMemberUserRemoved -> [ " you are no longer a member of the group " ]
2021-08-02 20:10:24 +01:00
CEGroupMemberNotFound c -> [ " contact " <> ttyContact c <> " is not a group member " ]
2022-01-24 16:07:17 +00:00
CEGroupMemberIntroNotFound c -> [ " group member intro not found for " <> ttyContact c ]
CEGroupCantResendInvitation g c -> viewCannotResendInvitation g c
2021-07-16 07:40:55 +01:00
CEGroupInternal s -> [ " chat group bug: " <> plain s ]
2021-09-04 07:32:56 +01:00
CEFileNotFound f -> [ " file not found: " <> plain f ]
CEFileAlreadyReceiving f -> [ " file is already accepted: " <> plain f ]
CEFileAlreadyExists f -> [ " file already exists: " <> plain f ]
CEFileRead f e -> [ " cannot read file " <> plain f , sShow e ]
CEFileWrite f e -> [ " cannot write file " <> plain f , sShow e ]
CEFileSend fileId e -> [ " error sending file " <> sShow fileId <> " : " <> sShow e ]
CEFileRcvChunk e -> [ " error receiving file: " <> plain e ]
CEFileInternal e -> [ " file error: " <> plain e ]
2022-01-11 08:50:44 +00:00
CEAgentVersion -> [ " unsupported agent version " ]
2022-01-24 16:07:17 +00:00
CECommandError e -> [ " bad chat command: " <> plain e ]
2021-09-04 07:32:56 +01:00
-- e -> ["chat error: " <> sShow e]
2021-07-12 19:00:03 +01:00
ChatErrorStore err -> case err of
2021-07-14 20:11:41 +01:00
SEDuplicateName -> [ " this display name is already used by user, contact or group " ]
2022-01-29 16:06:08 +04:00
SEContactNotFoundByName c -> [ " no contact " <> ttyContact c ]
2021-07-12 19:00:03 +01:00
SEContactNotReady c -> [ " contact " <> ttyContact c <> " is not active yet " ]
2022-01-29 16:06:08 +04:00
SEGroupNotFoundByName g -> [ " no group " <> ttyGroup g ]
2021-07-16 07:40:55 +01:00
SEGroupAlreadyJoined -> [ " you already joined this group " ]
2021-09-04 07:32:56 +01:00
SEFileNotFound fileId -> fileNotFound fileId
SESndFileNotFound fileId -> fileNotFound fileId
SERcvFileNotFound fileId -> fileNotFound fileId
2021-12-08 13:09:51 +00:00
SEDuplicateContactLink -> [ " you already have chat address, to show: " <> highlight' " /sa " ]
SEUserContactLinkNotFound -> [ " no chat address, to create: " <> highlight' " /ad " ]
2022-01-31 21:53:53 +04:00
SEContactRequestNotFoundByName c -> [ " no contact request from " <> ttyContact c ]
2022-02-02 23:50:43 +04:00
SEConnectionNotFound _ -> [] -- TODO mutes delete group error, but also mutes any error from getConnectionEntity
2021-09-04 07:32:56 +01:00
e -> [ " chat db error: " <> sShow e ]
2021-12-08 13:09:51 +00:00
ChatErrorAgent err -> case err of
SMP SMP . AUTH -> [ " error: this connection is deleted " ]
e -> [ " smp agent error: " <> sShow e ]
2021-09-04 07:32:56 +01:00
where
fileNotFound fileId = [ " file " <> sShow fileId <> " not found " ]
2021-06-25 18:18:24 +01:00
2021-07-14 20:11:41 +01:00
ttyContact :: ContactName -> StyledString
2022-02-22 14:05:45 +00:00
ttyContact = styled ( colored Green )
2021-06-25 18:18:24 +01:00
2022-01-26 16:18:27 +04:00
ttyContact' :: Contact -> StyledString
ttyContact' Contact { localDisplayName = c } = ttyContact c
2021-07-16 07:40:55 +01:00
ttyFullContact :: Contact -> StyledString
ttyFullContact Contact { localDisplayName , profile = Profile { fullName } } =
2021-07-24 10:26:28 +01:00
ttyFullName localDisplayName fullName
ttyMember :: GroupMember -> StyledString
ttyMember GroupMember { localDisplayName } = ttyContact localDisplayName
ttyFullMember :: GroupMember -> StyledString
ttyFullMember GroupMember { localDisplayName , memberProfile = Profile { fullName } } =
ttyFullName localDisplayName fullName
ttyFullName :: ContactName -> Text -> StyledString
ttyFullName c fullName = ttyContact c <> optFullName c fullName
2021-07-16 07:40:55 +01:00
2021-07-14 20:11:41 +01:00
ttyToContact :: ContactName -> StyledString
2022-02-22 14:05:45 +00:00
ttyToContact c = styled ( colored Cyan ) $ " @ " <> c <> " "
2021-06-25 18:18:24 +01:00
2021-07-14 20:11:41 +01:00
ttyFromContact :: ContactName -> StyledString
2022-02-22 14:05:45 +00:00
ttyFromContact c = styled ( colored Yellow ) $ c <> " > "
2021-06-25 18:18:24 +01:00
2021-07-14 20:11:41 +01:00
ttyGroup :: GroupName -> StyledString
2022-02-22 14:05:45 +00:00
ttyGroup g = styled ( colored Blue ) $ " # " <> g
2021-06-25 18:18:24 +01:00
2022-01-26 16:18:27 +04:00
ttyGroup' :: GroupInfo -> StyledString
2022-01-27 22:01:15 +00:00
ttyGroup' = ttyGroup . groupName'
2022-01-26 16:18:27 +04:00
2022-01-24 16:07:17 +00:00
ttyGroups :: [ GroupName ] -> StyledString
ttyGroups [] = " "
ttyGroups [ g ] = ttyGroup g
ttyGroups ( g : gs ) = ttyGroup g <> " , " <> ttyGroups gs
2022-01-26 16:18:27 +04:00
ttyFullGroup :: GroupInfo -> StyledString
ttyFullGroup GroupInfo { localDisplayName = g , groupProfile = GroupProfile { fullName } } =
ttyGroup g <> optFullName g fullName
2021-07-16 07:40:55 +01:00
2022-01-26 16:18:27 +04:00
ttyFromGroup :: GroupInfo -> ContactName -> StyledString
2022-02-22 14:05:45 +00:00
ttyFromGroup GroupInfo { localDisplayName = g } c = styled ( colored Yellow ) $ " # " <> g <> " " <> c <> " > "
2021-07-24 10:26:28 +01:00
2022-01-26 16:18:27 +04:00
ttyToGroup :: GroupInfo -> StyledString
2022-02-22 14:05:45 +00:00
ttyToGroup GroupInfo { localDisplayName = g } = styled ( colored Cyan ) $ " # " <> g <> " "
2021-07-24 10:26:28 +01:00
2021-09-05 14:08:29 +01:00
ttyFilePath :: FilePath -> StyledString
ttyFilePath = plain
2021-07-24 10:26:28 +01:00
optFullName :: ContactName -> Text -> StyledString
2021-07-16 07:40:55 +01:00
optFullName localDisplayName fullName
2021-07-24 10:26:28 +01:00
| T . null fullName || localDisplayName == fullName = " "
2021-07-16 07:40:55 +01:00
| otherwise = plain ( " ( " <> fullName <> " ) " )
highlight :: StyledFormat a => a -> StyledString
2022-02-22 14:05:45 +00:00
highlight = styled ( colored Cyan )
2021-07-16 07:40:55 +01:00
highlight' :: String -> StyledString
highlight' = highlight
2021-06-25 18:18:24 +01:00
styleTime :: String -> StyledString
styleTime = Styled [ SetColor Foreground Vivid Black ]