{-# LANGUAGE MultiWayIf #-}
module Matterhorn.State.Messages
( PostToAdd(..)
, addDisconnectGaps
, lastMsg
, sendMessage
, editMessage
, deleteMessage
, addNewPostedMessage
, addObtainedMessages
, asyncFetchMoreMessages
, asyncFetchMessagesForGap
, asyncFetchMessagesSurrounding
, fetchVisibleIfNeeded
, disconnectChannels
, toggleMessageTimestamps
, jumpToPost
)
where
import Prelude ()
import Matterhorn.Prelude
import Brick.Main ( getVtyHandle, invalidateCacheEntry, invalidateCache )
import qualified Brick.Widgets.FileBrowser as FB
import Control.Exception ( SomeException, try )
import qualified Data.Foldable as F
import qualified Data.HashMap.Strict as HM
import qualified Data.Set as Set
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import Graphics.Vty ( outputIface )
import Graphics.Vty.Output.Interface ( ringTerminalBell )
import Lens.Micro.Platform ( Traversal', (.=), (%=), (%~), (.~), (^?)
, to, at, traversed, filtered, ix, _1 )
import Network.Mattermost
import qualified Network.Mattermost.Endpoints as MM
import Network.Mattermost.Lenses
import Network.Mattermost.Types
import Matterhorn.Constants
import Matterhorn.State.Channels
import Matterhorn.State.Common
import Matterhorn.State.Reactions
import Matterhorn.State.Users
import Matterhorn.TimeUtils
import Matterhorn.Types
import Matterhorn.Types.Common ( sanitizeUserText )
import Matterhorn.Types.DirectionalSeq ( DirectionalSeq, SeqDirection )
addDisconnectGaps :: MH ()
addDisconnectGaps = mapM_ onEach . filteredChannelIds (const True) =<< use csChannels
where onEach c = do addEndGap c
clearPendingFlags c
mh $ invalidateCacheEntry (ChannelMessages c)
disconnectChannels :: MH ()
disconnectChannels = addDisconnectGaps
toggleMessageTimestamps :: MH ()
toggleMessageTimestamps = do
mh invalidateCache
let toggle c = c { configShowMessageTimestamps = not (configShowMessageTimestamps c)
}
csResources.crConfiguration %= toggle
clearPendingFlags :: ChannelId -> MH ()
clearPendingFlags c = csChannel(c).ccContents.cdFetchPending .= False
addEndGap :: ChannelId -> MH ()
addEndGap cId = withChannel cId $ \chan ->
let lastmsg_ = chan^.ccContents.cdMessages.to reverseMessages.to lastMsg
lastIsGap = maybe False isGap lastmsg_
gapMsg = newGapMessage timeJustAfterLast
timeJustAfterLast = maybe t0 (justAfter . _mDate) lastmsg_
t0 = ServerTime $ originTime
newGapMessage = newMessageOfType
(T.pack "Disconnected. Will refresh when connected.")
(C UnknownGapAfter)
in unless lastIsGap
(csChannels %= modifyChannelById cId (ccContents.cdMessages %~ addMessage gapMsg))
lastMsg :: RetrogradeMessages -> Maybe Message
lastMsg = withFirstMessage id
sendMessage :: ChannelId -> EditMode -> Text -> [AttachmentData] -> MH ()
sendMessage chanId mode msg attachments =
when (not $ shouldSkipMessage msg) $ do
status <- use csConnectionStatus
case status of
Disconnected -> do
let m = T.concat [ "Cannot send messages while disconnected. Enable logging to "
, "get disconnection information. If Matterhorn's reconnection "
, "attempts are failing, use `/reconnect` to attempt to "
, "reconnect manually."
]
mhError $ GenericError m
Connected -> do
session <- getSession
doAsync Preempt $ do
fileInfos <- forM attachments $ \a -> do
MM.mmUploadFile chanId (FB.fileInfoFilename $ attachmentDataFileInfo a)
(attachmentDataBytes a) session
let fileIds = Seq.fromList $
fmap fileInfoId $
concat $
(F.toList . MM.uploadResponseFileInfos) <$> fileInfos
case mode of
NewPost -> do
let pendingPost = (rawPost msg chanId) { rawPostFileIds = fileIds }
void $ MM.mmCreatePost pendingPost session
Replying _ p -> do
let pendingPost = (rawPost msg chanId) { rawPostRootId = postRootId p <|> (Just $ postId p)
, rawPostFileIds = fileIds
}
void $ MM.mmCreatePost pendingPost session
Editing p ty -> do
let body = case ty of
CP Emote -> addEmoteFormatting msg
_ -> msg
update = (postUpdateBody body) { postUpdateFileIds = if null fileIds
then Nothing
else Just fileIds
}
void $ MM.mmPatchPost (postId p) update session
shouldSkipMessage :: Text -> Bool
shouldSkipMessage "" = True
shouldSkipMessage s = T.all (`elem` (" \t"::String)) s
editMessage :: Post -> MH ()
editMessage new = do
myId <- gets myUserId
baseUrl <- getServerBaseUrl
let isEditedMessage m = m^.mMessageId == Just (MessagePostId $ new^.postIdL)
(msg, mentionedUsers) = clientPostToMessage (toClientPost baseUrl new (new^.postRootIdL))
chan = csChannel (new^.postChannelIdL)
chan . ccContents . cdMessages . traversed . filtered isEditedMessage .= msg
mh $ invalidateCacheEntry (ChannelMessages $ new^.postChannelIdL)
fetchMentionedUsers mentionedUsers
when (postUserId new /= Just myId) $
chan %= adjustEditedThreshold new
csPostMap.ix(postId new) .= msg
asyncFetchReactionsForPost (postChannelId new) new
asyncFetchAttachments new
deleteMessage :: Post -> MH ()
deleteMessage new = do
let isDeletedMessage m = m^.mMessageId == Just (MessagePostId $ new^.postIdL) ||
isReplyTo (new^.postIdL) m
chan :: Traversal' ChatState ClientChannel
chan = csChannel (new^.postChannelIdL)
chan.ccContents.cdMessages.traversed.filtered isDeletedMessage %= (& mDeleted .~ True)
chan %= adjustUpdated new
mh $ invalidateCacheEntry (ChannelMessages $ new^.postChannelIdL)
addNewPostedMessage :: PostToAdd -> MH ()
addNewPostedMessage p =
addMessageToState True True p >>= postProcessMessageAdd
addObtainedMessages :: ChannelId -> Int -> Bool -> Posts -> MH PostProcessMessageAdd
addObtainedMessages cId reqCnt addTrailingGap posts =
if null $ posts^.postsOrderL
then do when addTrailingGap $
csChannels %= modifyChannelById cId
(ccContents.cdMessages %~
\msgs -> let startPoint = join $ _mMessageId <$> getLatestPostMsg msgs
in fst $ removeMatchesFromSubset isGap startPoint Nothing msgs)
return NoAction
else
withChannelOrDefault cId NoAction $ \chan -> do
let pIdList = toList (posts^.postsOrderL)
earliestPId = last pIdList
latestPId = head pIdList
earliestDate = postCreateAt $ (posts^.postsPostsL) HM.! earliestPId
latestDate = postCreateAt $ (posts^.postsPostsL) HM.! latestPId
localMessages = chan^.ccContents . cdMessages
match = snd $ removeMatchesFromSubset
(\m -> maybe False (\p -> p `elem` pIdList) (messagePostId m))
(Just (MessagePostId earliestPId))
(Just (MessagePostId latestPId))
localMessages
accum m l =
case messagePostId m of
Just pId -> pId : l
Nothing -> l
dupPIds = foldr accum [] match
newGapMessage d isOlder =
do uuid <- generateUUID
let txt = "Load " <>
(if isOlder then "older" else "newer") <>
" messages" <>
(if isOlder then " ↥↥↥" else " ↧↧↧")
ty = if isOlder
then C UnknownGapBefore
else C UnknownGapAfter
return (newMessageOfType txt ty d
& mMessageId .~ Just (MessageUUID uuid))
addingAtEnd = maybe True (latestDate >=) $
(^.mDate) <$> getLatestPostMsg localMessages
addingAtStart = maybe True (earliestDate <=) $
(^.mDate) <$> getEarliestPostMsg localMessages
removeStart = if addingAtStart && noMoreBefore
then Nothing
else Just (MessagePostId earliestPId)
removeEnd = if addTrailingGap || (addingAtEnd && noMoreAfter)
then Nothing
else Just (MessagePostId latestPId)
noMoreBefore = reqCnt < 0 && length pIdList < (-reqCnt)
noMoreAfter = addTrailingGap || reqCnt > 0 && length pIdList < reqCnt
reAddGapBefore = earliestPId `elem` dupPIds || noMoreBefore
reAddGapAfter = latestPId `elem` dupPIds || noMoreAfter
void $ installMessagesFromPosts posts
action <- foldr andProcessWith NoAction <$>
mapM (addMessageToState False False . OldPost)
[ (posts^.postsPostsL) HM.! p
| p <- toList (posts^.postsOrderL)
, not (p `elem` dupPIds)
]
withChannelOrDefault cId () $ \updchan -> do
let updMsgs = updchan ^. ccContents . cdMessages
let (resultMessages, removedMessages) =
removeMatchesFromSubset isGap removeStart removeEnd updMsgs
csChannels %= modifyChannelById cId
(ccContents.cdMessages .~ resultMessages)
selMsgId <- use (csMessageSelect.to selectMessageId)
let rmvdSel = do
i <- selMsgId
findMessage i removedMessages
rmvdSelType = _mType <$> rmvdSel
case rmvdSel of
Nothing -> return ()
Just rm ->
if isGap rm
then return ()
else do
setMode Main
csMessageSelect .= MessageSelectState Nothing
if reAddGapBefore
then
case rmvdSelType of
Just (C UnknownGapBefore) ->
csMessageSelect .= MessageSelectState (pure $ MessagePostId earliestPId)
_ -> return ()
else do
gapMsg <- newGapMessage (justBefore earliestDate) True
csChannels %= modifyChannelById cId
(ccContents.cdMessages %~ addMessage gapMsg)
case rmvdSelType of
Just (C UnknownGapBefore) -> do
csMessageSelect .= MessageSelectState (gapMsg^.mMessageId)
_ -> return ()
if reAddGapAfter
then
case rmvdSelType of
Just (C UnknownGapAfter) ->
csMessageSelect .= MessageSelectState (pure $ MessagePostId latestPId)
_ -> return ()
else do
gapMsg <- newGapMessage (justAfter latestDate) False
csChannels %= modifyChannelById cId
(ccContents.cdMessages %~ addMessage gapMsg)
case rmvdSelType of
Just (C UnknownGapAfter) ->
csMessageSelect .= MessageSelectState (gapMsg^.mMessageId)
_ -> return ()
let users = foldr (\post s -> maybe s (flip Set.insert s) (postUserId post))
Set.empty (posts^.postsPostsL)
addUnknownUsers inputUserIds = do
knownUserIds <- Set.fromList <$> gets allUserIds
let unknownUsers = Set.difference inputUserIds knownUserIds
if Set.null unknownUsers
then return ()
else handleNewUsers (Seq.fromList $ toList unknownUsers) (return ())
addUnknownUsers users
return action
addMessageToState :: Bool -> Bool -> PostToAdd -> MH PostProcessMessageAdd
addMessageToState doFetchMentionedUsers fetchAuthor newPostData = do
let (new, wasMentioned) = case newPostData of
OldPost p -> (p, False)
RecentPost p m -> (p, m)
st <- use id
case st ^? csChannel(postChannelId new) of
Nothing -> do
session <- getSession
doAsyncWith Preempt $ do
nc <- MM.mmGetChannel (postChannelId new) session
member <- MM.mmGetChannelMember (postChannelId new) UserMe session
let chType = nc^.channelTypeL
pref = showGroupChannelPref (postChannelId new) (myUserId st)
case channelDeleted nc of
True -> return Nothing
False -> return $ Just $ do
if chType == Group
then applyPreferenceChange pref
else refreshChannel SidebarUpdateImmediate nc member
addMessageToState doFetchMentionedUsers fetchAuthor newPostData >>=
postProcessMessageAdd
return NoAction
Just _ -> do
baseUrl <- getServerBaseUrl
let cp = toClientPost baseUrl new (new^.postRootIdL)
fromMe = (cp^.cpUser == (Just $ myUserId st)) &&
(isNothing $ cp^.cpUserOverride)
userPrefs = st^.csResources.crUserPreferences
isJoinOrLeave = case cp^.cpType of
Join -> True
Leave -> True
_ -> False
ignoredJoinLeaveMessage =
not (userPrefs^.userPrefShowJoinLeave) && isJoinOrLeave
cId = postChannelId new
doAddMessage = do
case cp^.cpUser of
Nothing -> return ()
Just authorId -> when fetchAuthor $ do
authorResult <- gets (userById authorId)
when (isNothing authorResult) $
handleNewUsers (Seq.singleton authorId) (return ())
currCId <- use csCurrentChannelId
flags <- use (csResources.crFlaggedPosts)
let (msg', mentionedUsers) = clientPostToMessage cp
& _1.mFlagged .~ ((cp^.cpPostId) `Set.member` flags)
when doFetchMentionedUsers $
fetchMentionedUsers mentionedUsers
csPostMap.at(postId new) .= Just msg'
mh $ invalidateCacheEntry (ChannelMessages cId)
csChannels %= modifyChannelById cId
((ccContents.cdMessages %~ addMessage msg') .
(if not ignoredJoinLeaveMessage then adjustUpdated new else id) .
(\c -> if currCId == cId
then c
else case newPostData of
OldPost _ -> c
RecentPost _ _ ->
updateNewMessageIndicator new c) .
(\c -> if wasMentioned
then c & ccInfo.cdMentionCount %~ succ
else c)
)
asyncFetchReactionsForPost cId new
asyncFetchAttachments new
postedChanMessage
doHandleAddedMessage = do
case cp^.cpInReplyToPost of
Just parentId ->
case getMessageForPostId st parentId of
Nothing -> do
doAsyncChannelMM Preempt cId
(\s _ -> MM.mmGetThread parentId s)
(\_ p -> Just $ updatePostMap p)
_ -> return ()
_ -> return ()
doAddMessage
postedChanMessage =
withChannelOrDefault (postChannelId new) NoAction $ \chan -> do
currCId <- use csCurrentChannelId
let notifyPref = notifyPreference (myUser st) chan
curChannelAction = if postChannelId new == currCId
then UpdateServerViewed
else NoAction
originUserAction =
if | fromMe -> NoAction
| ignoredJoinLeaveMessage -> NoAction
| notifyPref == NotifyOptionAll -> NotifyUser [newPostData]
| notifyPref == NotifyOptionMention
&& wasMentioned -> NotifyUser [newPostData]
| otherwise -> NoAction
return $ curChannelAction `andProcessWith` originUserAction
doHandleAddedMessage
data PostProcessMessageAdd = NoAction
| NotifyUser [PostToAdd]
| UpdateServerViewed
| NotifyUserAndServer [PostToAdd]
andProcessWith
:: PostProcessMessageAdd -> PostProcessMessageAdd -> PostProcessMessageAdd
andProcessWith NoAction x = x
andProcessWith x NoAction = x
andProcessWith (NotifyUserAndServer p) UpdateServerViewed = NotifyUserAndServer p
andProcessWith (NotifyUserAndServer p1) (NotifyUser p2) = NotifyUserAndServer (p1 <> p2)
andProcessWith (NotifyUserAndServer p1) (NotifyUserAndServer p2) = NotifyUserAndServer (p1 <> p2)
andProcessWith (NotifyUser p1) (NotifyUserAndServer p2) = NotifyUser (p1 <> p2)
andProcessWith (NotifyUser p1) (NotifyUser p2) = NotifyUser (p1 <> p2)
andProcessWith (NotifyUser p) UpdateServerViewed = NotifyUserAndServer p
andProcessWith UpdateServerViewed UpdateServerViewed = UpdateServerViewed
andProcessWith UpdateServerViewed (NotifyUserAndServer p) = NotifyUserAndServer p
andProcessWith UpdateServerViewed (NotifyUser p) = NotifyUserAndServer p
postProcessMessageAdd :: PostProcessMessageAdd -> MH ()
postProcessMessageAdd ppma = postOp ppma
where
postOp NoAction = return ()
postOp UpdateServerViewed = updateViewed False
postOp (NotifyUser p) = maybeRingBell >> mapM_ maybeNotify p
postOp (NotifyUserAndServer p) = updateViewed False >> maybeRingBell >> mapM_ maybeNotify p
maybeNotify :: PostToAdd -> MH ()
maybeNotify (OldPost _) = do
return ()
maybeNotify (RecentPost post mentioned) = runNotifyCommand post mentioned
maybeRingBell :: MH ()
maybeRingBell = do
doBell <- use (csResources.crConfiguration.to configActivityBell)
when doBell $ do
vty <- mh getVtyHandle
liftIO $ ringTerminalBell $ outputIface vty
data PostToAdd =
OldPost Post
| RecentPost Post Bool
runNotifyCommand :: Post -> Bool -> MH ()
runNotifyCommand post mentioned = do
outputChan <- use (csResources.crSubprocessLog)
st <- use id
notifyCommand <- use (csResources.crConfiguration.to configActivityNotifyCommand)
case notifyCommand of
Nothing -> return ()
Just cmd ->
doAsyncWith Preempt $ do
let messageString = T.unpack $ sanitizeUserText $ postMessage post
notified = if mentioned then "1" else "2"
sender = T.unpack $ maybePostUsername st post
runLoggedCommand outputChan (T.unpack cmd)
[notified, sender, messageString] Nothing Nothing
return Nothing
maybePostUsername :: ChatState -> Post -> T.Text
maybePostUsername st p =
fromMaybe T.empty $ do
uId <- postUserId p
usernameForUserId uId st
asyncFetchMoreMessages :: MH ()
asyncFetchMoreMessages = do
cId <- use csCurrentChannelId
withChannel cId $ \chan ->
let offset = max 0 $ length (chan^.ccContents.cdMessages) - 2
page = offset `div` pageAmount
usefulMsgs = getTwoContiguousPosts Nothing (chan^.ccContents.cdMessages.to reverseMessages)
sndOldestId = (messagePostId . snd) =<< usefulMsgs
query = MM.defaultPostQuery
{ MM.postQueryPage = maybe (Just page) (const Nothing) sndOldestId
, MM.postQueryPerPage = Just pageAmount
, MM.postQueryBefore = sndOldestId
}
addTrailingGap = MM.postQueryBefore query == Nothing &&
MM.postQueryPage query == Just 0
in doAsyncChannelMM Preempt cId
(\s c -> MM.mmGetPostsForChannel c query s)
(\c p -> Just $ do
pp <- addObtainedMessages c (-pageAmount) addTrailingGap p
postProcessMessageAdd pp
mh $ invalidateCacheEntry (ChannelMessages cId))
getTwoContiguousPosts :: SeqDirection dir =>
Maybe Message
-> DirectionalSeq dir Message
-> Maybe (Message, Message)
getTwoContiguousPosts startMsg msgs =
let go start =
do anchor <- getRelMessageId (_mMessageId =<< start) msgs
hinge <- getRelMessageId (anchor^.mMessageId) msgs
if isGap anchor || isGap hinge
then go $ Just anchor
else Just (anchor, hinge)
in go startMsg
asyncFetchMessagesForGap :: ChannelId -> Message -> MH ()
asyncFetchMessagesForGap cId gapMessage =
when (isGap gapMessage) $
withChannel cId $ \chan ->
let offset = max 0 $ length (chan^.ccContents.cdMessages) - 2
page = offset `div` pageAmount
chanMsgs = chan^.ccContents.cdMessages
fromMsg = Just gapMessage
fetchNewer = case gapMessage^.mType of
C UnknownGapAfter -> True
C UnknownGapBefore -> False
_ -> error "fetch gap messages: unknown gap message type"
baseId = messagePostId . snd =<<
case gapMessage^.mType of
C UnknownGapAfter -> getTwoContiguousPosts fromMsg $
reverseMessages chanMsgs
C UnknownGapBefore -> getTwoContiguousPosts fromMsg chanMsgs
_ -> error "fetch gap messages: unknown gap message type"
query = MM.defaultPostQuery
{ MM.postQueryPage = maybe (Just page) (const Nothing) baseId
, MM.postQueryPerPage = Just pageAmount
, MM.postQueryBefore = if fetchNewer then Nothing else baseId
, MM.postQueryAfter = if fetchNewer then baseId else Nothing
}
addTrailingGap = MM.postQueryBefore query == Nothing &&
MM.postQueryPage query == Just 0
in doAsyncChannelMM Preempt cId
(\s c -> MM.mmGetPostsForChannel c query s)
(\c p -> Just $ do
void $ addObtainedMessages c (-pageAmount) addTrailingGap p
mh $ invalidateCacheEntry (ChannelMessages cId))
asyncFetchMessagesSurrounding :: ChannelId -> PostId -> MH ()
asyncFetchMessagesSurrounding cId pId = do
let query = MM.defaultPostQuery
{ MM.postQueryBefore = Just pId
, MM.postQueryPerPage = Just reqAmt
}
reqAmt = 5
doAsyncChannelMM Preempt cId
(\s c -> MM.mmGetPostsForChannel c query s)
(\c p -> Just $ do
let last2ndId = secondToLastPostId p
void $ addObtainedMessages c (-reqAmt) False p
mh $ invalidateCacheEntry (ChannelMessages cId)
let query' = MM.defaultPostQuery
{ MM.postQueryAfter = last2ndId
, MM.postQueryPerPage = Just $ reqAmt + 2
}
doAsyncChannelMM Preempt cId
(\s' c' -> MM.mmGetPostsForChannel c' query' s')
(\c' p' -> Just $ do
void $ addObtainedMessages c' (reqAmt + 2) False p'
mh $ invalidateCacheEntry (ChannelMessages cId)
)
)
where secondToLastPostId posts =
let pl = toList $ postsOrder posts
in if length pl > 1 then Just $ last $ init pl else Nothing
fetchVisibleIfNeeded :: MH ()
fetchVisibleIfNeeded = do
sts <- use csConnectionStatus
when (sts == Connected) $ do
cId <- use csCurrentChannelId
withChannel cId $ \chan ->
let msgs = chan^.ccContents.cdMessages.to reverseMessages
(numRemaining, gapInDisplayable, _, rel'pId, overlap) =
foldl gapTrail (numScrollbackPosts, False, Nothing, Nothing, 2) msgs
gapTrail a@(_, True, _, _, _) _ = a
gapTrail a@(0, _, _, _, _) _ = a
gapTrail (a, False, b, c, d) m | isGap m = (a, True, b, c, d)
gapTrail (remCnt, _, prev'pId, prev''pId, ovl) msg =
(remCnt - 1, False, msg^.mMessageId <|> prev'pId, prev'pId <|> prev''pId,
ovl + if not (isPostMessage msg) then 1 else 0)
numToReq = numRemaining + overlap
query = MM.defaultPostQuery
{ MM.postQueryPage = Just 0
, MM.postQueryPerPage = Just numToReq
}
finalQuery = case rel'pId of
Just (MessagePostId pid) -> query { MM.postQueryBefore = Just pid }
_ -> query
op = \s c -> MM.mmGetPostsForChannel c finalQuery s
addTrailingGap = MM.postQueryBefore finalQuery == Nothing &&
MM.postQueryPage finalQuery == Just 0
in when ((not $ chan^.ccContents.cdFetchPending) && gapInDisplayable) $ do
csChannel(cId).ccContents.cdFetchPending .= True
doAsyncChannelMM Preempt cId op
(\c p -> Just $ do
addObtainedMessages c (-numToReq) addTrailingGap p >>= postProcessMessageAdd
csChannel(c).ccContents.cdFetchPending .= False)
asyncFetchAttachments :: Post -> MH ()
asyncFetchAttachments p = do
let cId = p^.postChannelIdL
pId = p^.postIdL
session <- getSession
host <- use (csResources.crConn.cdHostnameL)
F.forM_ (p^.postFileIdsL) $ \fId -> doAsyncWith Normal $ do
info <- MM.mmGetMetadataForFile fId session
let scheme = "https://"
attUrl = scheme <> host <> urlForFile fId
attachment = mkAttachment (fileInfoName info) attUrl fId
addIfMissing a as =
if isNothing $ Seq.elemIndexL a as
then a Seq.<| as
else as
addAttachment m
| m^.mMessageId == Just (MessagePostId pId) =
m & mAttachments %~ (addIfMissing attachment)
| otherwise =
m
return $ Just $ do
csChannel(cId).ccContents.cdMessages.traversed %= addAttachment
mh $ invalidateCacheEntry $ ChannelMessages cId
jumpToPost :: PostId -> MH ()
jumpToPost pId = do
st <- use id
case getMessageForPostId st pId of
Just msg ->
case msg ^. mChannelId of
Just cId -> do
case findChannelById cId (st^.csChannels) of
Nothing ->
joinChannel' cId (Just $ jumpToPost pId)
Just _ -> do
setFocus cId
setMode MessageSelect
csMessageSelect .= MessageSelectState (msg^.mMessageId)
Nothing ->
error "INTERNAL: selected Post ID not associated with a channel"
Nothing -> do
session <- getSession
doAsyncWith Preempt $ do
result <- try $ MM.mmGetPost pId session
return $ Just $ do
case result of
Right p -> do
case findChannelById (postChannelId p) (st^.csChannels) of
Nothing -> do
joinChannel' (postChannelId p) (Just $ jumpToPost pId)
Just _ -> do
void $ addMessageToState True True (OldPost p)
jumpToPost pId
Left (_::SomeException) ->
postErrorMessage' "Could not fetch linked post"