packages feed

matterhorn-50200.16.0: src/Matterhorn/State/MessageSelect.hs

module Matterhorn.State.MessageSelect
  (
  -- * Message selection mode
    beginMessageSelect
  , flagSelectedMessage
  , pinSelectedMessage
  , viewSelectedMessage
  , fillSelectedGap
  , copyPostLink
  , yankSelectedMessageVerbatim
  , yankSelectedMessage
  , openSelectedMessageURLs
  , beginConfirmDeleteSelectedMessage
  , messageSelectUp
  , messageSelectUpBy
  , messageSelectDown
  , messageSelectDownBy
  , messageSelectFirst
  , messageSelectLast
  , deleteSelectedMessage
  , beginReplyCompose
  , beginEditMessage
  , flagMessage
  , getSelectedMessage
  )
where

import           Prelude ()
import           Matterhorn.Prelude

import           Brick ( invalidateCache )
import           Brick.Widgets.Edit ( applyEdit )
import           Data.Text.Zipper ( clearZipper, insertMany )
import           Data.Maybe ( fromJust )
import           Lens.Micro.Platform

import qualified Network.Mattermost.Endpoints as MM
import           Network.Mattermost.Types

import           Matterhorn.Clipboard ( copyToClipboard )
import           Matterhorn.State.Common
import           Matterhorn.State.Links
import           Matterhorn.State.Messages
import           Matterhorn.Types
import           Matterhorn.Types.RichText ( findVerbatimChunk, makePermalink )
import           Matterhorn.Types.Common
import           Matterhorn.Windows.ViewMessage


-- | In these modes, we allow access to the selected message state.
messageSelectCompatibleModes :: [Mode]
messageSelectCompatibleModes =
    [ MessageSelect
    , MessageSelectDeleteConfirm
    , ReactionEmojiListOverlay
    ]

getSelectedMessage :: TeamId -> ChatState -> Maybe Message
getSelectedMessage tId st
    | not (st^.csTeam(tId).tsMode `elem` messageSelectCompatibleModes) = Nothing
    | otherwise = do
        selMsgId <- selectMessageId $ st^.csTeam(tId).tsMessageSelect
        cId <- st^.csCurrentChannelId(tId)
        chan <- st^?csChannel(cId)
        let chanMsgs = chan ^. ccContents . cdMessages
        findMessage selMsgId chanMsgs

beginMessageSelect :: TeamId -> MH ()
beginMessageSelect tId = do
    withCurrentChannel tId $ \_ chan -> do
        -- Invalidate the rendering cache since we cache messages to
        -- speed up the selection UI responsiveness. (See Draw.Messages
        -- for caching behavior.)
        mh invalidateCache

        -- Get the number of messages in the current channel and set
        -- the currently selected message index to be the most recently
        -- received message that corresponds to a Post (i.e. exclude
        -- informative messages).
        --
        -- If we can't find one at all, we ignore the mode switch
        -- request and just return.
        let chanMsgs = chan ^. ccContents . cdMessages
            recentMsg = getLatestSelectableMessage chanMsgs

        when (isJust recentMsg) $ do
            setMode tId MessageSelect
            csTeam(tId).tsMessageSelect .= MessageSelectState (recentMsg >>= _mMessageId)

-- | Tell the server that the message we currently have selected
-- should have its flagged state toggled.
flagSelectedMessage :: TeamId -> MH ()
flagSelectedMessage tId = do
  selected <- use (to (getSelectedMessage tId))
  case selected of
    Just msg
      | isFlaggable msg, Just pId <- messagePostId msg ->
        flagMessage pId (not (msg^.mFlagged))
    _        -> return ()

-- | Tell the server that the message we currently have selected
-- should have its pinned state toggled.
pinSelectedMessage :: TeamId -> MH ()
pinSelectedMessage tId = do
  selected <- use (to (getSelectedMessage tId))
  case selected of
    Just msg
      | isPinnable msg, Just pId <- messagePostId msg ->
        pinMessage pId (not (msg^.mPinned))
    _ -> return ()

viewSelectedMessage :: TeamId -> MH ()
viewSelectedMessage tId = do
  selected <- use (to (getSelectedMessage tId))
  case selected of
    Just msg
      | not (isGap msg) -> viewMessage tId msg
    _        -> return ()

fillSelectedGap :: TeamId -> MH ()
fillSelectedGap tId = do
    withCurrentChannel tId $ \cId _ -> do
        selected <- use (to (getSelectedMessage tId))
        case selected of
          Just msg
            | isGap msg -> asyncFetchMessagesForGap cId msg
          _        -> return ()

copyPostLink :: TeamId -> MH ()
copyPostLink tId = do
  selected <- use (to (getSelectedMessage tId))
  case selected of
    Just msg | isPostMessage msg -> do
        baseUrl <- getServerBaseUrl tId
        let pId = fromJust (messageIdPostId =<< _mMessageId msg)
        copyToClipboard $ makePermalink baseUrl pId
        setMode tId Main
    _ -> return ()

viewMessage :: TeamId -> Message -> MH ()
viewMessage tId m = do
    let w = tabbedWindow VMTabMessage (viewMessageWindowTemplate tId) MessageSelect (78, 25)
    csTeam(tId).tsViewedMessage .= Just (m, w)
    runTabShowHandlerFor (twValue w) w
    setMode tId ViewMessage

yankSelectedMessageVerbatim :: TeamId -> MH ()
yankSelectedMessageVerbatim tId = do
    selectedMessage <- use (to (getSelectedMessage tId))
    case selectedMessage of
        Nothing -> return ()
        Just m -> do
            setMode tId Main
            case findVerbatimChunk (m^.mText) of
                Just txt -> copyToClipboard txt
                Nothing  -> return ()

yankSelectedMessage :: TeamId -> MH ()
yankSelectedMessage tId = do
    selectedMessage <- use (to (getSelectedMessage tId))
    case selectedMessage of
        Nothing -> return ()
        Just m -> do
            setMode tId Main
            copyToClipboard $ m^.mMarkdownSource

openSelectedMessageURLs :: TeamId -> MH ()
openSelectedMessageURLs tId = whenMode tId MessageSelect $ do
    mCurMsg <- use (to (getSelectedMessage tId))
    curMsg <- case mCurMsg of
        Nothing -> error "BUG: openSelectedMessageURLs: no selected message available"
        Just m -> return m

    let urls = msgURLs curMsg
    when (not (null urls)) $ do
        mapM_ (openLinkTarget . _linkTarget) urls

beginConfirmDeleteSelectedMessage :: TeamId -> MH ()
beginConfirmDeleteSelectedMessage tId = do
    st <- use id
    selected <- use (to (getSelectedMessage tId))
    case selected of
        Just msg | isDeletable msg && isMine st msg ->
            setMode tId MessageSelectDeleteConfirm
        _ -> return ()

messageSelectUp :: TeamId -> MH ()
messageSelectUp tId = do
    withCurrentChannel tId $ \_ chan -> do
        mode <- use (csTeam(tId).tsMode)
        selected <- use (csTeam(tId).tsMessageSelect.to selectMessageId)
        case selected of
            Just _ | mode == MessageSelect -> do
                let chanMsgs = chan^.ccContents.cdMessages
                    nextMsgId = getPrevMessageId selected chanMsgs
                csTeam(tId).tsMessageSelect .= MessageSelectState (nextMsgId <|> selected)
            _ -> return ()

messageSelectDown :: TeamId -> MH ()
messageSelectDown tId = do
    withCurrentChannel tId $ \_ chan -> do
        selected <- use (csTeam(tId).tsMessageSelect.to selectMessageId)
        case selected of
            Just _ ->
                whenMode tId MessageSelect $ do
                    let chanMsgs = chan^.ccContents.cdMessages
                        nextMsgId = getNextMessageId selected chanMsgs
                    csTeam(tId).tsMessageSelect .= MessageSelectState (nextMsgId <|> selected)
            _ -> return ()

messageSelectDownBy :: TeamId -> Int -> MH ()
messageSelectDownBy tId amt
    | amt <= 0 = return ()
    | otherwise =
        messageSelectDown tId >> messageSelectDownBy tId (amt - 1)

messageSelectUpBy :: TeamId -> Int -> MH ()
messageSelectUpBy tId amt
    | amt <= 0 = return ()
    | otherwise =
      messageSelectUp tId >> messageSelectUpBy tId (amt - 1)

messageSelectFirst :: TeamId -> MH ()
messageSelectFirst tId = do
    withCurrentChannel tId $ \_ chan -> do
        selected <- use (csTeam(tId).tsMessageSelect.to selectMessageId)
        case selected of
            Just _ ->
                whenMode tId MessageSelect $ do
                    let chanMsgs = chan^.ccContents.cdMessages
                    case getEarliestSelectableMessage chanMsgs of
                      Just firstMsg ->
                        csTeam(tId).tsMessageSelect .= MessageSelectState (firstMsg^.mMessageId <|> selected)
                      Nothing -> mhLog LogError "No first message found from current message?!"
            _ -> return ()

messageSelectLast :: TeamId -> MH ()
messageSelectLast tId = do
    withCurrentChannel tId $ \_ chan -> do
        selected <- use (csTeam(tId).tsMessageSelect.to selectMessageId)
        case selected of
            Just _ ->
                whenMode tId MessageSelect $ do
                    let chanMsgs = chan^.ccContents.cdMessages
                    case getLatestSelectableMessage chanMsgs of
                      Just lastSelMsg ->
                        csTeam(tId).tsMessageSelect .= MessageSelectState (lastSelMsg^.mMessageId <|> selected)
                      Nothing -> mhLog LogError "No last message found from current message?!"
            _ -> return ()

deleteSelectedMessage :: TeamId -> MH ()
deleteSelectedMessage tId = do
    withCurrentChannel tId $ \cId _ -> do
        selectedMessage <- use (to (getSelectedMessage tId))
        st <- use id
        case selectedMessage of
            Just msg | isMine st msg && isDeletable msg ->
                case msg^.mOriginalPost of
                  Just p ->
                      doAsyncChannelMM Preempt cId
                          (\s _ -> MM.mmDeletePost (postId p) s)
                          (\_ _ -> Just $ do
                              csTeam(tId).tsEditState.cedEditMode .= NewPost
                              setMode tId Main)
                  Nothing -> return ()
            _ -> return ()

beginReplyCompose :: TeamId -> MH ()
beginReplyCompose tId = do
    selected <- use (to (getSelectedMessage tId))
    case selected of
        Just msg | isReplyable msg -> do
            rootMsg <- getReplyRootMessage msg
            let p = fromJust $ rootMsg^.mOriginalPost
            setMode tId Main
            csTeam(tId).tsEditState.cedEditMode .= Replying rootMsg p
        _ -> return ()

beginEditMessage :: TeamId -> MH ()
beginEditMessage tId = do
    selected <- use (to (getSelectedMessage tId))
    st <- use id
    case selected of
        Just msg | isMine st msg && isEditable msg -> do
            let p = fromJust $ msg^.mOriginalPost
            setMode tId Main
            csTeam(tId).tsEditState.cedEditMode .= Editing p (msg^.mType)
            -- If the post that we're editing is an emote, we need
            -- to strip the formatting because that's only there to
            -- indicate that the post is an emote. This is annoying and
            -- can go away one day when there is an actual post type
            -- value of "emote" that we can look at. Note that the
            -- removed formatting needs to be reinstated just prior to
            -- issuing the API call to update the post.
            let sanitized = sanitizeUserText $ postMessage p
            let toEdit = if isEmote msg
                         then removeEmoteFormatting sanitized
                         else sanitized
            csTeam(tId).tsEditState.cedEditor %= applyEdit (insertMany toEdit . clearZipper)
        _ -> return ()

-- | Tell the server that we have flagged or unflagged a message.
flagMessage :: PostId -> Bool -> MH ()
flagMessage pId f = do
  session <- getSession
  myId <- gets myUserId
  doAsyncWith Normal $ do
    let doFlag = if f then MM.mmFlagPost else MM.mmUnflagPost
    doFlag myId pId session
    return Nothing

-- | Tell the server that we have pinned or unpinned a message.
pinMessage :: PostId -> Bool -> MH ()
pinMessage pId f = do
  session <- getSession
  doAsyncWith Normal $ do
    let doPin = if f then MM.mmPinPostToChannel else MM.mmUnpinPostToChannel
    void $ doPin pId session
    return Nothing