packages feed

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

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

import           Prelude ()
import           Matterhorn.Prelude

import           Brick ( invalidateCache )
import           Brick.Widgets.Edit ( applyEdit )
import           Control.Monad ( replicateM_ )
import qualified Data.Text as T
import           Data.Text.Zipper ( clearZipper, insertMany )
import           Data.Maybe ( fromJust )
import           Lens.Micro.Platform
import qualified System.Environment as Sys
import           System.IO (hClose, hPutStr)
import qualified System.IO.Temp as Sys
import qualified System.Process as Sys

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

import           Matterhorn.Clipboard ( copyToClipboard )
import           Matterhorn.State.Common
import           Matterhorn.State.Links
import {-# SOURCE #-} Matterhorn.State.Messages ( asyncFetchMessagesForGap )
import           Matterhorn.Types
import           Matterhorn.Types.RichText ( findVerbatimChunk, makePermalink )
import           Matterhorn.Types.Common
import           Matterhorn.Windows.ViewMessage
import qualified Matterhorn.State.ThreadWindow as TW


getSelectedMessage :: Lens' ChatState (MessageInterface n i)
                   -> ChatState
                   -> Maybe Message
getSelectedMessage which st = do
    selMsgId <- selectMessageId $ st^.which.miMessageSelect
    let chanMsgs = st^.which.miMessages
    findMessage selMsgId chanMsgs

withSelectedMessage :: Lens' ChatState (MessageInterface n i)
                    -> (Message -> MH ())
                    -> MH ()
withSelectedMessage which act = do
    selectedMessage <- use (to (getSelectedMessage which))
    case selectedMessage of
        Nothing -> return ()
        Just m -> act m

beginMessageSelect :: Lens' ChatState (MessageInterface n i)
                   -> MH ()
beginMessageSelect which = 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 listing 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.
    msgs <- use (which.miMessages)
    let recentMsg = getLatestSelectableMessage msgs

    when (isJust recentMsg) $ do
        which.miMode .= MessageSelect
        which.miMessageSelect .= MessageSelectState (recentMsg >>= _mMessageId)

exitMessageSelect :: Lens' ChatState (MessageInterface n i) -> MH ()
exitMessageSelect which = do
    m <- use (which.miMode)
    when (m == MessageSelect) $
        which.miMode .= Compose

-- | Tell the server that the message we currently have selected
-- should have its flagged state toggled.
flagSelectedMessage :: Lens' ChatState (MessageInterface n i)
                    -> MH ()
flagSelectedMessage which =
    withSelectedMessage which $ \msg ->
        when (isFlaggable msg) $ do
            case messagePostId msg of
                Just pId -> flagMessage pId (not (msg^.mFlagged))
                Nothing -> return ()

-- | Tell the server that the message we currently have selected
-- should have its pinned state toggled.
pinSelectedMessage :: Lens' ChatState (MessageInterface n i)
                   -> MH ()
pinSelectedMessage which =
    withSelectedMessage which $ \msg -> do
        when (isPinnable msg) $ do
            case messagePostId msg of
                Just pId -> pinMessage pId (not (msg^.mPinned))
                Nothing -> return ()

viewSelectedMessage :: TeamId
                    -> Lens' ChatState (MessageInterface n i)
                    -> MH ()
viewSelectedMessage tId which =
    withSelectedMessage which $ \msg ->
        when (not (isGap msg)) $ viewMessage tId msg

openSelectedMessageInEditor :: Lens' ChatState (MessageInterface n i)
                            -> MH ()
openSelectedMessageInEditor which =
    withSelectedMessage which $ \msg ->
        when (not (isGap msg)) $ openMessageInEditor msg which

-- This will only work for channel message selection, not thread message
-- selection, since there will never be gap entries in the thread view.
-- But this is generalized enough that it looks like it should work for
-- thread views, but it won't because asyncFetchMessagesForGap only
-- works for channel message selection (and should).
fillSelectedGap :: Lens' ChatState (MessageInterface n i)
                -> MH ()
fillSelectedGap which = do
    cId <- use (which.miChannelId)
    withSelectedMessage which $ \msg ->
        when (isGap msg) $ asyncFetchMessagesForGap cId msg

copyPostLink :: TeamId
             -> Lens' ChatState (MessageInterface n i)
             -> MH ()
copyPostLink tId which =
    withSelectedMessage which $ \msg ->
        when (isPostMessage msg) $ do
            baseUrl <- getServerBaseUrl tId
            let pId = fromJust (messageIdPostId =<< _mMessageId msg)
            copyToClipboard $ makePermalink baseUrl pId
            exitMessageSelect which

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

openMessageInEditor :: Message -> Lens' ChatState (MessageInterface n i) -> MH ()
openMessageInEditor m which = do
    -- If EDITOR is in the environment, write the current message to a
    -- temp file, invoke EDITOR on it, read the result, remove the temp
    -- file, and update the program state.
    --
    -- If EDITOR is not present, fall back to 'vi'.
    mEnv <- liftIO $ Sys.lookupEnv "EDITOR"
    let editorProgram = maybe "vi" id mEnv

    case m^.mOriginalPost of
        Nothing -> return ()
        Just p -> do
            mhSuspendAndResume $ \s -> do
                Sys.withSystemTempFile "matterhorn_editor.md" $ \tmpFileName tmpFileHandle -> do
                    hPutStr tmpFileHandle $ T.unpack $ unsafeUserText $ postMessage p
                    hClose tmpFileHandle
                    void $ Sys.system (editorProgram <> " " <> tmpFileName)
                    return s

    exitMessageSelect which

yankSelectedMessageVerbatim :: Lens' ChatState (MessageInterface n i)
                            -> MH ()
yankSelectedMessageVerbatim which =
    withSelectedMessage which $ \msg -> do
        exitMessageSelect which
        case findVerbatimChunk (msg^.mText) of
            Just txt -> copyToClipboard txt
            Nothing  -> return ()

openThreadWindow :: TeamId
                 -> Lens' ChatState (MessageInterface n i)
                 -> MH ()
openThreadWindow tId which =
    withSelectedMessage which $ \msg -> do
        when (isPostMessage msg) $ do
            rootMsg <- getReplyRootMessage msg
            let p = fromJust $ rootMsg^.mOriginalPost
            case msg^.mChannelId of
                Nothing -> return ()
                Just cId -> do
                    -- Leave message selection mode
                    exitMessageSelect which
                    TW.openThreadWindow tId cId (postId p)

yankSelectedMessage :: Lens' ChatState (MessageInterface n i)
                    -> MH ()
yankSelectedMessage which =
    withSelectedMessage which $ \msg -> do
        exitMessageSelect which
        copyToClipboard $ msg^.mMarkdownSource

openSelectedMessageURLs :: Lens' ChatState (MessageInterface n i)
                        -> MH ()
openSelectedMessageURLs which =
    withSelectedMessage which $ \msg -> do
        let urls = msgURLs msg
        when (not (null urls)) $ do
            mapM_ (openLinkTarget . _linkTarget) urls

beginConfirmDeleteSelectedMessage :: TeamId
                                  -> Lens' ChatState (MessageInterface n i)
                                  -> MH ()
beginConfirmDeleteSelectedMessage tId which = do
    st <- use id
    target <- use (which.miTarget)
    withSelectedMessage which $ \msg ->
        when (isDeletable msg && isMine st msg) $
            pushMode tId $ MessageSelectDeleteConfirm target

messageSelectUp :: Lens' ChatState (MessageInterface n i)
                -> MH ()
messageSelectUp which =
    withSelectedMessage which $ \msg -> do
        let selected = _mMessageId msg
        msgs <- use (which.miMessages)
        let nextMsgId = getPrevMessageId selected msgs
        which.miMessageSelect .= MessageSelectState (nextMsgId <|> selected)

messageSelectDown :: Lens' ChatState (MessageInterface n i)
                  -> MH ()
messageSelectDown which =
    withSelectedMessage which $ \msg -> do
        let selected = _mMessageId msg
        msgs <- use (which.miMessages)
        let nextMsgId = getNextMessageId selected msgs
        which.miMessageSelect .= MessageSelectState (nextMsgId <|> selected)

messageSelectDownBy :: Lens' ChatState (MessageInterface n i)
                    -> Int
                    -> MH ()
messageSelectDownBy which amt =
    replicateM_ amt $ messageSelectDown which

messageSelectUpBy :: Lens' ChatState (MessageInterface n i)
                  -> Int
                  -> MH ()
messageSelectUpBy which amt =
    replicateM_ amt $ messageSelectUp which

messageSelectFirst :: Lens' ChatState (MessageInterface n i)
                   -> MH ()
messageSelectFirst which =
    withSelectedMessage which $ \msg -> do
        let selected = _mMessageId msg
        msgs <- use (which.miMessages)
        case getEarliestSelectableMessage msgs of
          Just firstMsg ->
            which.miMessageSelect .= MessageSelectState (firstMsg^.mMessageId <|> selected)
          Nothing -> mhLog LogError "No first message found from current message?!"

messageSelectLast :: Lens' ChatState (MessageInterface n i)
                  -> MH ()
messageSelectLast which =
    withSelectedMessage which $ \msg -> do
        let selected = _mMessageId msg
        msgs <- use (which.miMessages)
        case getLatestSelectableMessage msgs of
          Just lastSelMsg ->
            which.miMessageSelect .= MessageSelectState (lastSelMsg^.mMessageId <|> selected)
          Nothing -> mhLog LogError "No last message found from current message?!"

deleteSelectedMessage :: Lens' ChatState (MessageInterface n i)
                      -> MH ()
deleteSelectedMessage which = do
    st <- use id
    withSelectedMessage which $ \msg ->
        when (isMine st msg && isDeletable msg) $ do
            exitMessageSelect which
            case msg^.mOriginalPost of
                Just p ->
                    doAsyncMM Preempt
                        (\s -> MM.mmDeletePost (postId p) s)
                        (const Nothing)
                Nothing -> return ()

beginReplyCompose :: Lens' ChatState (MessageInterface n i)
                  -> MH ()
beginReplyCompose which = do
    withSelectedMessage which $ \msg ->
        when (isReplyable msg) $ do
            rootMsg <- getReplyRootMessage msg
            let p = fromJust $ rootMsg^.mOriginalPost
            exitMessageSelect which
            which.miEditor.esEditMode .= Replying rootMsg p

beginEditMessage :: Lens' ChatState (MessageInterface n i)
                 -> MH ()
beginEditMessage which = do
    st <- use id
    withSelectedMessage which $ \msg ->
        when (isMine st msg && isEditable msg) $ do
            let p = fromJust $ msg^.mOriginalPost
            exitMessageSelect which
            which.miEditor.esEditMode .= 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
            which.miEditor.esEditor %= applyEdit (insertMany toEdit . clearZipper)

-- | 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