packages feed

matterhorn 50200.18.0 → 50200.19.0

raw patch · 19 files changed

+254/−140 lines, 19 filesdep ~brickdep ~mattermost-apidep ~mtlPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: brick, mattermost-api, mtl, strict, tasty, text, text-zipper, unix

API changes (from Hackage documentation)

+ Matterhorn.FilePaths: xdgDataDirs :: IO [FilePath]
+ Matterhorn.State.MessageSelect: openSelectedMessageInEditor :: Lens' ChatState (MessageInterface n i) -> MH ()
+ Matterhorn.Types.Core: OpenMessageInExternalEditorEvent :: KeyEvent
+ Matterhorn.Types.EditState: autocompleteAlternativeText :: AutocompleteAlternative -> Text
- Matterhorn.Types: listWindowEnterHandler :: forall a_a1oNS b_a1oNT. Lens' (ListWindowState a_a1oNS b_a1oNT) (a_a1oNS -> MH Bool)
+ Matterhorn.Types: listWindowEnterHandler :: forall a_a1p6j b_a1p6k. Lens' (ListWindowState a_a1p6j b_a1p6k) (a_a1p6j -> MH Bool)
- Matterhorn.Types: listWindowFetchResults :: forall a_a1oNS b_a1oNT. Lens' (ListWindowState a_a1oNS b_a1oNT) (b_a1oNT -> Session -> Text -> IO (Vector a_a1oNS))
+ Matterhorn.Types: listWindowFetchResults :: forall a_a1p6j b_a1p6k. Lens' (ListWindowState a_a1p6j b_a1p6k) (b_a1p6k -> Session -> Text -> IO (Vector a_a1p6j))
- Matterhorn.Types: listWindowNewList :: forall a_a1oNS b_a1oNT. Lens' (ListWindowState a_a1oNS b_a1oNT) (Vector a_a1oNS -> List Name a_a1oNS)
+ Matterhorn.Types: listWindowNewList :: forall a_a1p6j b_a1p6k. Lens' (ListWindowState a_a1p6j b_a1p6k) (Vector a_a1p6j -> List Name a_a1p6j)
- Matterhorn.Types: listWindowRecordCount :: forall a_a1oNS b_a1oNT. Lens' (ListWindowState a_a1oNS b_a1oNT) (Maybe Int)
+ Matterhorn.Types: listWindowRecordCount :: forall a_a1p6j b_a1p6k. Lens' (ListWindowState a_a1p6j b_a1p6k) (Maybe Int)
- Matterhorn.Types: listWindowSearchInput :: forall a_a1oNS b_a1oNT. Lens' (ListWindowState a_a1oNS b_a1oNT) (Editor Text Name)
+ Matterhorn.Types: listWindowSearchInput :: forall a_a1p6j b_a1p6k. Lens' (ListWindowState a_a1p6j b_a1p6k) (Editor Text Name)
- Matterhorn.Types: listWindowSearchResults :: forall a_a1oNS b_a1oNT. Lens' (ListWindowState a_a1oNS b_a1oNT) (List Name a_a1oNS)
+ Matterhorn.Types: listWindowSearchResults :: forall a_a1p6j b_a1p6k. Lens' (ListWindowState a_a1p6j b_a1p6k) (List Name a_a1p6j)
- Matterhorn.Types: listWindowSearchScope :: forall a_a1oNS b_a1oNT. Lens' (ListWindowState a_a1oNS b_a1oNT) b_a1oNT
+ Matterhorn.Types: listWindowSearchScope :: forall a_a1p6j b_a1p6k. Lens' (ListWindowState a_a1p6j b_a1p6k) b_a1p6k
- Matterhorn.Types: listWindowSearching :: forall a_a1oNS b_a1oNT. Lens' (ListWindowState a_a1oNS b_a1oNT) Bool
+ Matterhorn.Types: listWindowSearching :: forall a_a1p6j b_a1p6k. Lens' (ListWindowState a_a1p6j b_a1p6k) Bool
- Matterhorn.Types.EditState: acCachedResponses :: forall n_a19wJ. Lens' (AutocompleteState n_a19wJ) (HashMap Text [AutocompleteAlternative])
+ Matterhorn.Types.EditState: acCachedResponses :: forall n_a19JD. Lens' (AutocompleteState n_a19JD) (HashMap Text [AutocompleteAlternative])
- Matterhorn.Types.EditState: acCompletionList :: forall n_a19wJ n_a19Zs. Lens (AutocompleteState n_a19wJ) (AutocompleteState n_a19Zs) (List n_a19wJ AutocompleteAlternative) (List n_a19Zs AutocompleteAlternative)
+ Matterhorn.Types.EditState: acCompletionList :: forall n_a19JD n_a1acH. Lens (AutocompleteState n_a19JD) (AutocompleteState n_a1acH) (List n_a19JD AutocompleteAlternative) (List n_a1acH AutocompleteAlternative)
- Matterhorn.Types.EditState: acPreviousSearchString :: forall n_a19wJ. Lens' (AutocompleteState n_a19wJ) Text
+ Matterhorn.Types.EditState: acPreviousSearchString :: forall n_a19JD. Lens' (AutocompleteState n_a19JD) Text
- Matterhorn.Types.EditState: acType :: forall n_a19wJ. Lens' (AutocompleteState n_a19wJ) AutocompletionType
+ Matterhorn.Types.EditState: acType :: forall n_a19JD. Lens' (AutocompleteState n_a19JD) AutocompletionType
- Matterhorn.Types.EditState: esAttachmentList :: forall n_a19wI. Lens' (EditState n_a19wI) (List n_a19wI AttachmentData)
+ Matterhorn.Types.EditState: esAttachmentList :: forall n_a19JC. Lens' (EditState n_a19JC) (List n_a19JC AttachmentData)
- Matterhorn.Types.EditState: esAutocomplete :: forall n_a19wI. Lens' (EditState n_a19wI) (Maybe (AutocompleteState n_a19wI))
+ Matterhorn.Types.EditState: esAutocomplete :: forall n_a19JC. Lens' (EditState n_a19JC) (Maybe (AutocompleteState n_a19JC))
- Matterhorn.Types.EditState: esAutocompletePending :: forall n_a19wI. Lens' (EditState n_a19wI) (Maybe Text)
+ Matterhorn.Types.EditState: esAutocompletePending :: forall n_a19JC. Lens' (EditState n_a19JC) (Maybe Text)
- Matterhorn.Types.EditState: esChannelId :: forall n_a19wI. Lens' (EditState n_a19wI) ChannelId
+ Matterhorn.Types.EditState: esChannelId :: forall n_a19JC. Lens' (EditState n_a19JC) ChannelId
- Matterhorn.Types.EditState: esEditMode :: forall n_a19wI. Lens' (EditState n_a19wI) EditMode
+ Matterhorn.Types.EditState: esEditMode :: forall n_a19JC. Lens' (EditState n_a19JC) EditMode
- Matterhorn.Types.EditState: esEditor :: forall n_a19wI. Lens' (EditState n_a19wI) (Editor Text n_a19wI)
+ Matterhorn.Types.EditState: esEditor :: forall n_a19JC. Lens' (EditState n_a19JC) (Editor Text n_a19JC)
- Matterhorn.Types.EditState: esEphemeral :: forall n_a19wI. Lens' (EditState n_a19wI) EphemeralEditState
+ Matterhorn.Types.EditState: esEphemeral :: forall n_a19JC. Lens' (EditState n_a19JC) EphemeralEditState
- Matterhorn.Types.EditState: esFileBrowser :: forall n_a19wI. Lens' (EditState n_a19wI) (Maybe (FileBrowser n_a19wI))
+ Matterhorn.Types.EditState: esFileBrowser :: forall n_a19JC. Lens' (EditState n_a19JC) (Maybe (FileBrowser n_a19JC))
- Matterhorn.Types.EditState: esJustCompleted :: forall n_a19wI. Lens' (EditState n_a19wI) Bool
+ Matterhorn.Types.EditState: esJustCompleted :: forall n_a19JC. Lens' (EditState n_a19JC) Bool
- Matterhorn.Types.EditState: esMisspellings :: forall n_a19wI. Lens' (EditState n_a19wI) (Set Text)
+ Matterhorn.Types.EditState: esMisspellings :: forall n_a19JC. Lens' (EditState n_a19JC) (Set Text)
- Matterhorn.Types.EditState: esResetEditMode :: forall n_a19wI. Lens' (EditState n_a19wI) EditMode
+ Matterhorn.Types.EditState: esResetEditMode :: forall n_a19JC. Lens' (EditState n_a19JC) EditMode
- Matterhorn.Types.EditState: esShowReplyPrompt :: forall n_a19wI. Lens' (EditState n_a19wI) Bool
+ Matterhorn.Types.EditState: esShowReplyPrompt :: forall n_a19JC. Lens' (EditState n_a19JC) Bool
- Matterhorn.Types.EditState: esSpellCheckTimerReset :: forall n_a19wI. Lens' (EditState n_a19wI) (Maybe (IO ()))
+ Matterhorn.Types.EditState: esSpellCheckTimerReset :: forall n_a19JC. Lens' (EditState n_a19JC) (Maybe (IO ()))
- Matterhorn.Types.EditState: esTeamId :: forall n_a19wI. Lens' (EditState n_a19wI) (Maybe TeamId)
+ Matterhorn.Types.EditState: esTeamId :: forall n_a19JC. Lens' (EditState n_a19JC) (Maybe TeamId)
- Matterhorn.Types.MessageInterface: attachmentPathDialogFocus :: forall n_a1cQz. Lens' (SaveAttachmentDialogState n_a1cQz) (FocusRing n_a1cQz)
+ Matterhorn.Types.MessageInterface: attachmentPathDialogFocus :: forall n_a1d7T. Lens' (SaveAttachmentDialogState n_a1d7T) (FocusRing n_a1d7T)
- Matterhorn.Types.MessageInterface: attachmentPathEditor :: forall n_a1cQz. Lens' (SaveAttachmentDialogState n_a1cQz) (Editor Text n_a1cQz)
+ Matterhorn.Types.MessageInterface: attachmentPathEditor :: forall n_a1d7T. Lens' (SaveAttachmentDialogState n_a1d7T) (Editor Text n_a1d7T)
- Matterhorn.Types.MessageInterface: miChannelId :: forall n_a1cQB i_a1cQC. Lens' (MessageInterface n_a1cQB i_a1cQC) ChannelId
+ Matterhorn.Types.MessageInterface: miChannelId :: forall n_a1d7V i_a1d7W. Lens' (MessageInterface n_a1d7V i_a1d7W) ChannelId
- Matterhorn.Types.MessageInterface: miEditor :: forall n_a1cQB i_a1cQC. Lens' (MessageInterface n_a1cQB i_a1cQC) (EditState n_a1cQB)
+ Matterhorn.Types.MessageInterface: miEditor :: forall n_a1d7V i_a1d7W. Lens' (MessageInterface n_a1d7V i_a1d7W) (EditState n_a1d7V)
- Matterhorn.Types.MessageInterface: miMessageSelect :: forall n_a1cQB i_a1cQC. Lens' (MessageInterface n_a1cQB i_a1cQC) MessageSelectState
+ Matterhorn.Types.MessageInterface: miMessageSelect :: forall n_a1d7V i_a1d7W. Lens' (MessageInterface n_a1d7V i_a1d7W) MessageSelectState
- Matterhorn.Types.MessageInterface: miMessages :: forall n_a1cQB i_a1cQC. Lens' (MessageInterface n_a1cQB i_a1cQC) Messages
+ Matterhorn.Types.MessageInterface: miMessages :: forall n_a1d7V i_a1d7W. Lens' (MessageInterface n_a1d7V i_a1d7W) Messages
- Matterhorn.Types.MessageInterface: miMode :: forall n_a1cQB i_a1cQC. Lens' (MessageInterface n_a1cQB i_a1cQC) MessageInterfaceMode
+ Matterhorn.Types.MessageInterface: miMode :: forall n_a1d7V i_a1d7W. Lens' (MessageInterface n_a1d7V i_a1d7W) MessageInterfaceMode
- Matterhorn.Types.MessageInterface: miRootPostId :: forall n_a1cQB i_a1cQC i_a1cZz. Lens (MessageInterface n_a1cQB i_a1cQC) (MessageInterface n_a1cQB i_a1cZz) i_a1cQC i_a1cZz
+ Matterhorn.Types.MessageInterface: miRootPostId :: forall n_a1d7V i_a1d7W i_a1dgT. Lens (MessageInterface n_a1d7V i_a1d7W) (MessageInterface n_a1d7V i_a1dgT) i_a1d7W i_a1dgT
- Matterhorn.Types.MessageInterface: miSaveAttachmentDialog :: forall n_a1cQB i_a1cQC. Lens' (MessageInterface n_a1cQB i_a1cQC) (SaveAttachmentDialogState n_a1cQB)
+ Matterhorn.Types.MessageInterface: miSaveAttachmentDialog :: forall n_a1d7V i_a1d7W. Lens' (MessageInterface n_a1d7V i_a1d7W) (SaveAttachmentDialogState n_a1d7V)
- Matterhorn.Types.MessageInterface: miTarget :: forall n_a1cQB i_a1cQC. Lens' (MessageInterface n_a1cQB i_a1cQC) MessageInterfaceTarget
+ Matterhorn.Types.MessageInterface: miTarget :: forall n_a1d7V i_a1d7W. Lens' (MessageInterface n_a1d7V i_a1d7W) MessageInterfaceTarget
- Matterhorn.Types.MessageInterface: miUrlList :: forall n_a1cQB i_a1cQC. Lens' (MessageInterface n_a1cQB i_a1cQC) (URLList n_a1cQB)
+ Matterhorn.Types.MessageInterface: miUrlList :: forall n_a1d7V i_a1d7W. Lens' (MessageInterface n_a1d7V i_a1d7W) (URLList n_a1d7V)
- Matterhorn.Types.MessageInterface: miUrlListSource :: forall n_a1cQB i_a1cQC. Lens' (MessageInterface n_a1cQB i_a1cQC) URLListSource
+ Matterhorn.Types.MessageInterface: miUrlListSource :: forall n_a1d7V i_a1d7W. Lens' (MessageInterface n_a1d7V i_a1d7W) URLListSource
- Matterhorn.Types.MessageInterface: ulList :: forall n_a1cQA n_a1d8m. Lens (URLList n_a1cQA) (URLList n_a1d8m) (List n_a1cQA (Int, LinkChoice)) (List n_a1d8m (Int, LinkChoice))
+ Matterhorn.Types.MessageInterface: ulList :: forall n_a1d7U n_a1dpG. Lens (URLList n_a1d7U) (URLList n_a1dpG) (List n_a1d7U (Int, LinkChoice)) (List n_a1dpG (Int, LinkChoice))
- Matterhorn.Types.MessageInterface: ulSource :: forall n_a1cQA. Lens' (URLList n_a1cQA) (Maybe URLListSource)
+ Matterhorn.Types.MessageInterface: ulSource :: forall n_a1d7U. Lens' (URLList n_a1d7U) (Maybe URLListSource)
- Matterhorn.Types.Posts: toClientPost :: Maybe TeamBaseURL -> Post -> Maybe PostId -> ClientPost
+ Matterhorn.Types.Posts: toClientPost :: Text -> Maybe TeamBaseURL -> Post -> Maybe PostId -> ClientPost

Files

CHANGELOG.md view
@@ -1,4 +1,29 @@ +50200.19.0+==========++Improvements:+* Matterhorn's network performance is now greatly improved due to a more+  efficient use of the server API. Matterhorn now makes significantly+  fewer API requests at startup and whenever the channel is changed.+  ([#804](https://github.com/matterhorn-chat/matterhorn/issues/804))+* Autocompletion now has a less surprising default behavior by+  preferring a match that has the input as its prefix.+* The message selection mode (reachable with `C-s` by default) now has+  a new keybinding (event `open-message-in-editor`, default binding `O`)+  that opens the selected message in `$EDITOR` (or `vi` if `$EDITOR` is+  not set).+* Unless specified in the configuration, Matterhorn now+  also searches XDG data directories for syntax files.+  ([#584](https://github.com/matterhorn-chat/matterhorn/issues/584))++Bug fixes:+* `ClearUnreadEvent` (`M-l`) now also refreshes the sidebar+  ([#801](https://github.com/matterhorn-chat/matterhorn/issues/801))+* Reactions with no users no longer linger in+  the reactions tab of the message view window.+  ([#802](https://github.com/matterhorn-chat/matterhorn/issues/802))+ 50200.18.0 ========== 
docs/keybindings.md view
@@ -110,6 +110,7 @@ | `Enter` | `fetch-for-gap` | Fetch messages for the selected gap | | `a` | `react-to-message` | Post a reaction to the selected message | | `l` | `copy-post-link` | Copy a post's link to the clipboard |+| `O` | `open-message-in-editor` | Open the message's source in $EDITOR |  # User Listings | Keybinding | Event Name | Description |
matterhorn.cabal view
@@ -1,5 +1,5 @@ name:                matterhorn-version:             50200.18.0+version:             50200.19.0 synopsis:            Terminal client for the Mattermost chat system description:         This is a terminal client for the Mattermost chat                      system. Please see the README for a list of@@ -170,7 +170,7 @@     ghc-options: -fhide-source-paths    build-depends:       base                 >=4.8      && <5-                     , mattermost-api       == 50200.14.0+                     , mattermost-api       == 50200.15.0                      , base-compat          >= 0.9     && < 0.13                      , unordered-containers >= 0.2     && < 0.3                      , containers           >= 0.5.7   && < 0.7@@ -178,24 +178,24 @@                      , data-clist           >= 0.1.2   && < 0.2                      , semigroups           >= 0.18    && < 0.20                      , connection           >= 0.2     && < 0.4-                     , text                 >= 1.2     && < 2.1+                     , text                 >= 1.2.5.0 && < 2.0                      , bytestring           >= 0.10    && < 0.12                      , stm                  >= 2.4     && < 2.6                      , config-ini           >= 0.2.2.0 && < 0.3                      , process              >= 1.4     && < 1.7                      , microlens-platform   >= 0.3     && < 0.5-                     , brick                >= 1.6     && < 1.7+                     , brick                >= 1.8     && < 1.9                      , brick-skylighting    >= 1.0     && < 1.1                      , vty                  >= 5.38    && < 5.39                      , word-wrap            >= 0.4.0   && < 0.5                      , transformers         >= 0.4     && < 0.6-                     , text-zipper          >= 0.12    && < 0.13+                     , text-zipper          >= 0.13    && < 0.14                      , time                 >= 1.6     && < 2.0                      , xdg-basedir          >= 0.2     && < 0.3                      , filepath             >= 1.4     && < 1.5                      , directory            >= 1.3     && < 1.4                      , vector               < 0.13-                     , strict               >= 0.3     && < 0.5+                     , strict               >= 0.3     && < 0.6                      , hashable             >= 1.2     && < 1.5                      , commonmark           >= 0.2.1   && < 0.3                      , commonmark-extensions >= 0.2.1.2 && < 0.3@@ -204,10 +204,10 @@                      , temporary            >= 1.2     && < 1.4                      , gitrev               >= 1.3     && < 1.4                      , Hclip                >= 3.0     && < 3.1-                     , mtl                  >= 2.2     && < 2.3+                     , mtl                  >= 2.2     && < 2.4                      , aspell-pipe          >= 0.6     && < 0.7                      , stm-delay            >= 0.1     && < 0.2-                     , unix                 >= 2.7.1.0 && < 2.7.3.0+                     , unix                 >= 2.7.1.0 && < 2.9                      , skylighting-core     >= 0.12    && < 0.13                      , timezone-olson       >= 0.2     && < 0.3                      , timezone-series      >= 0.1.6.1 && < 0.2@@ -249,7 +249,7 @@   build-depends:      base                 >=4.7      && <5                     , Unique               >= 0.4     && < 0.5                     , checkers             >= 0.4     && < 0.6-                    , tasty                >= 0.11    && < 1.3+                    , tasty                >= 0.11    && < 1.5                     , tasty-hunit          >= 0.9     && < 0.12                     , tasty-quickcheck     >= 0.8     && < 0.12                     , bytestring
src/Matterhorn/Config.hs view
@@ -49,9 +49,10 @@ defaultSkylightingPaths :: IO [FilePath] defaultSkylightingPaths = do     xdg <- xdgSyntaxDir+    dataDirs <- xdgDataDirs     adjacent <- getBundledSyntaxPath     cabalDataFiles <- Paths.getDataFileName syntaxDirName-    return [xdg, adjacent, cabalDataFiles]+    return $ [xdg, adjacent, cabalDataFiles] <> dataDirs  getBundledSyntaxPath :: IO FilePath getBundledSyntaxPath = do@@ -214,6 +215,7 @@     , (FlagMessageEvent                 , [ bind 'f' ])     , (OpenThreadEvent                  , [ bind 't' ])     , (PinMessageEvent                  , [ bind 'p' ])+    , (OpenMessageInExternalEditorEvent , [ bind 'O' ])     , (YankMessageEvent                 , [ bind 'y' ])     , (YankWholeMessageEvent            , [ bind 'Y' ])     , (DeleteMessageEvent               , [ bind 'd' ])
src/Matterhorn/Draw/MessageInterface.hs view
@@ -130,86 +130,95 @@             let optionList = if null usableOptions                              then txt "(no actions available for this message)"                              else hBox $ intersperse (txt " ") usableOptions-                usableOptions = catMaybes $ mkOption <$> options-                mkOption (f, k, desc) = if f postMsg-                                        then Just $ withDefAttr messageSelectStatusAttr (txt k) <+>-                                                    txt (":" <> desc)-                                        else Nothing-                numURLs = Seq.length $ msgURLs postMsg-                s = if numURLs == 1 then "" else "s"-                hasURLs = numURLs > 0-                openUrlsMsg = "open " <> (T.pack $ show numURLs) <> " URL" <> s-                hasVerb = isJust (findVerbatimChunk (postMsg^.mText))-                ev = keyEventBindings st (messageSelectKeybindings tId which)-                -- make sure these keybinding pieces are up-to-date!-                options = [ ( not . isGap-                            , ev YankWholeMessageEvent-                            , "yank-all"-                            )-                          , ( \m -> isFlaggable m && not (m^.mFlagged)-                            , ev FlagMessageEvent-                            , "flag"-                            )-                          , ( \m -> isFlaggable m && m^.mFlagged-                            , ev FlagMessageEvent-                            , "unflag"-                            )-                          , ( isReplyable-                            , ev OpenThreadEvent-                            , "thread"-                            )-                          , ( isPostMessage-                            , ev CopyPostLinkEvent-                            , "copy-link"-                            )-                          , ( \m -> isPinnable m && not (m^.mPinned)-                            , ev PinMessageEvent-                            , "pin"-                            )-                          , ( \m -> isPinnable m && m^.mPinned-                            , ev PinMessageEvent-                            , "unpin"-                            )-                          , ( isReplyable-                            , ev ReplyMessageEvent-                            , "reply"-                            )-                          , ( not . isGap-                            , ev ViewMessageEvent-                            , "view"-                            )-                          , ( isGap-                            , ev FillGapEvent-                            , "load messages"-                            )-                          , ( \m -> isMine st m && isEditable m-                            , ev EditMessageEvent-                            , "edit"-                            )-                          , ( \m -> isMine st m && isDeletable m-                            , ev DeleteMessageEvent-                            , "delete"-                            )-                          , ( const hasURLs-                            , ev OpenMessageURLEvent-                            , openUrlsMsg-                            )-                          , ( const hasVerb-                            , ev YankMessageEvent-                            , "yank-code"-                            )-                          , ( isReactable-                            , ev ReactToMessageEvent-                            , "react"-                            )-                          ]-+                usableOptions = mkOption <$> messageSelectionKeyOptions st tId which postMsg+                mkOption (k, desc) = withDefAttr messageSelectStatusAttr (txt k) <+>+                                     txt (":" <> desc)             in hBox [ hLimit 1 hBorder                     , txt "["                     , optionList                     , txt "]"                     , hBorder                     ]++messageSelectionKeyOptions :: ChatState+                           -> TeamId+                           -> Lens' ChatState (MessageInterface Name i)+                           -> Message+                           -> [(T.Text, T.Text)]+messageSelectionKeyOptions st tId which msg =+    let ev = keyEventBindings st (messageSelectKeybindings tId which)+        hasVerb = isJust (findVerbatimChunk (msg^.mText))+        hasURLs = numURLs > 0+        numURLs = Seq.length $ msgURLs msg+        s = if numURLs == 1 then "" else "s"+        openUrlsMsg = "open " <> (T.pack $ show numURLs) <> " URL" <> s+        getUsable (usable, key, label) = if usable then Just (key, label) else Nothing+        options = [ ( not $ isGap msg+                  , ev YankWholeMessageEvent+                  , "yank-all"+                  )+                , ( isFlaggable msg && not (msg^.mFlagged)+                  , ev FlagMessageEvent+                  , "flag"+                  )+                , ( isFlaggable msg && msg^.mFlagged+                  , ev FlagMessageEvent+                  , "unflag"+                  )+                , ( isReplyable msg+                  , ev OpenThreadEvent+                  , "thread"+                  )+                , ( isPostMessage msg+                  , ev CopyPostLinkEvent+                  , "copy-link"+                  )+                , ( isPinnable msg && not (msg^.mPinned)+                  , ev PinMessageEvent+                  , "pin"+                  )+                , ( isPinnable msg && msg^.mPinned+                  , ev PinMessageEvent+                  , "unpin"+                  )+                , ( isReplyable msg+                  , ev ReplyMessageEvent+                  , "reply"+                  )+                , ( not $ isGap msg+                  , ev ViewMessageEvent+                  , "view"+                  )+                , ( not $ isGap msg+                  , ev OpenMessageInExternalEditorEvent+                  , "open"+                  )+                , ( isGap msg+                  , ev FillGapEvent+                  , "load messages"+                  )+                , ( isMine st msg && isEditable msg+                  , ev EditMessageEvent+                  , "edit"+                  )+                , ( isMine st msg && isDeletable msg+                  , ev DeleteMessageEvent+                  , "delete"+                  )+                , ( hasURLs+                  , ev OpenMessageURLEvent+                  , openUrlsMsg+                  )+                , ( hasVerb+                  , ev YankMessageEvent+                  , "yank-code"+                  )+                , ( isReactable msg+                  , ev ReactToMessageEvent+                  , "react"+                  )+                ]+    in catMaybes $ getUsable <$> options  renderMessageListing :: ChatState                      -> Bool
src/Matterhorn/Draw/Messages.hs view
@@ -240,7 +240,7 @@   Widget Greedy Greedy $ do     ctx <- getContext     curMsgResult <- withReaderT relaxHeight $ render $-                    forceAttr messageSelectAttr $+                    forceAttrAllowStyle messageSelectAttr $                     padRight Max $ doMsgRender curMsg curThreadState tag      let targetHeight = ctx^.availHeightL@@ -465,7 +465,7 @@                             , mdRenderReplyParent = False                             , mdIndentBlocks      = False                             }-                      in withParent (addEllipsis $ forceAttr replyParentAttr parentMsg)+                      in withParent (addEllipsis $ forceAttrAllowStyle replyParentAttr parentMsg)      where         renderBlocks :: HighlightSet -> Maybe Int -> [Widget Name] -> Seq Block
src/Matterhorn/Draw/RichText.hs view
@@ -299,7 +299,7 @@             Just u -> B.withDefAttr urlAttr . B.hyperlink (unURL u)          addClickable w = case mName of-            Nothing -> id w+            Nothing -> w             Just nm -> B.clickable nm w          widget = case val of@@ -319,12 +319,12 @@  textWithCursor :: Text -> Widget a textWithCursor t-    | T.any (== cursorSentinel) t = B.visible $ B.txt $ removeCursor t+    | cursorSentinel `T.elem` t = B.visible $ B.txt $ removeCursor t     | otherwise = B.txt t  wrappedTextWithCursor :: Text -> Widget a wrappedTextWithCursor t-    | T.any (== cursorSentinel) t = B.visible $ B.txtWrap $ removeCursor t+    | cursorSentinel `T.elem` t = B.visible $ B.txtWrap $ removeCursor t     | otherwise = B.txtWrap t  removeCursor :: Text -> Text
src/Matterhorn/Emoji.hs view
@@ -12,7 +12,8 @@ import           Matterhorn.Prelude  import qualified Control.Exception as E-import           Control.Monad.Except+import           Control.Monad.Except ( MonadError(..), ExceptT(..), runExceptT )+import           Control.Monad.Trans ( MonadTrans(..) ) import qualified Data.Aeson as A import qualified Data.ByteString.Lazy as BSL import qualified Data.Foldable as F
src/Matterhorn/Events/Main.hs view
@@ -16,6 +16,7 @@ import           Matterhorn.Events.ThreadWindow import           Matterhorn.State.ChannelSelect import           Matterhorn.State.Channels+import           Matterhorn.State.ChannelList ( updateSidebar ) import           Matterhorn.State.Help import           Matterhorn.State.Teams import           Matterhorn.State.PostListWindow ( enterFlaggedPostListMode )@@ -90,6 +91,7 @@     , onEvent ClearUnreadEvent "Clear the current channel's unread / edited indicators" $ do            withCurrentChannel tId $ \cId _ -> do                clearChannelUnreadStatus cId+               updateSidebar $ Just tId      , onEvent EnterFlaggedPostsEvent "View currently flagged posts" $          enterFlaggedPostListMode tId
src/Matterhorn/Events/MessageSelect.hs view
@@ -118,4 +118,7 @@      , onEvent CopyPostLinkEvent "Copy a post's link to the clipboard" $          copyPostLink tId which++    , onEvent OpenMessageInExternalEditorEvent "Open the message's source in $EDITOR" $+         openSelectedMessageInEditor which     ]
src/Matterhorn/FilePaths.hs view
@@ -11,6 +11,7 @@   , xdgName   , locateConfig   , xdgSyntaxDir+  , xdgDataDirs   , syntaxDirName   , userEmojiJsonPath   , bundledEmojiJsonPath@@ -38,6 +39,7 @@ import System.Environment.XDG.BaseDir ( getUserConfigFile                                       , getAllConfigFiles                                       , getUserConfigDir+                                      , getAllDataDirs                                       ) import System.FilePath ( (</>), takeBaseName, takeDirectory, splitPath, joinPath ) @@ -65,6 +67,12 @@ -- The path does not necessarily exist. xdgSyntaxDir :: IO FilePath xdgSyntaxDir = (</> syntaxDirName) <$> getUserConfigDir xdgName++-- | Get the list of syntax paths based on XDG data directory paths.+xdgDataDirs :: IO [FilePath]+xdgDataDirs = do+    dirs <- getAllDataDirs xdgName+    return $ (</> syntaxDirName) <$> dirs  -- | Get the XDG path to the user-specific emoji JSON file. The path -- does not necessarily exist.
src/Matterhorn/State/Common.hs view
@@ -87,10 +87,12 @@       Nothing -> return Nothing       Just tId -> Just <$> getServerBaseUrl tId +  hostname <- use (csResources.crConn.cdHostnameL)+   -- Build the ordered list of posts. Note that postsOrder lists the   -- posts most recent first, but we want most recent last.   let postsInOrder = findPost <$> (Seq.reverse $ postsOrder postCollection)-      mkClientPost p = toClientPost mBaseUrl p (postId <$> parent p)+      mkClientPost p = toClientPost hostname mBaseUrl p (postId <$> parent p)       clientPosts = mkClientPost <$> postsInOrder        addNext cp (msgs, us) =@@ -125,9 +127,11 @@       Nothing -> return Nothing       Just tId -> Just <$> getServerBaseUrl tId +  hostname <- use (csResources.crConn.cdHostnameL)+   let postMap = HM.fromList           [ ( pId-            , fst $ clientPostToMessage (toClientPost mBaseUrl x Nothing)+            , fst $ clientPostToMessage (toClientPost hostname mBaseUrl x Nothing)             )           | (pId, x) <- HM.toList (postCollection^.postsPostsL)           ]
src/Matterhorn/State/Editing.hs view
@@ -530,13 +530,39 @@  tabComplete :: Traversal' ChatState (EditState Name) -> Direction -> MH () tabComplete which dir = do+    searchStr <- use (which.esAutocomplete._Just.acPreviousSearchString)+     let transform list =             let len = list^.L.listElementsL.to length+                prefixMatch alt =+                    T.toLower searchStr `T.isPrefixOf` (T.toLower $ autocompleteAlternativeText alt)             in case dir of                 Forwards ->-                    if (L.listSelected list == Just (len - 1)) ||-                       (L.listSelected list == Nothing && len > 0)-                    then L.listMoveTo 0 list+                    if L.listSelected list == Just (len - 1)+                       then L.listMoveTo 0 list+                       else if L.listSelected list == Nothing && len > 0+                            -- If the list has nothing selected, then+                            -- make the initial selection the best match+                            -- by prefix rather than always selecting+                            -- the first entry. If there is no match+                            -- based on prefix, then select the first+                            -- entry.+                            --+                            -- Note that we only bother with this+                            -- behavior in the Forward case because in+                            -- the Backwards case we don't want the+                            -- first selection to be based on prefix+                            -- match since that doesn't make sense.+                            then let new = L.listFindBy prefixMatch list+                                 in if L.listSelected new == Nothing+                                    -- If, after attempting to select+                                    -- by prefix, nothing matched, then+                                    -- there is still no autocomplete+                                    -- alternative selected, so move to+                                    -- the selection to the first entry+                                    -- in the list.+                                    then L.listMoveBy 1 new+                                    else new                     else L.listMoveBy 1 list                 Backwards ->                     if (L.listSelected list == Just 0) ||
src/Matterhorn/State/MessageSelect.hs view
@@ -6,6 +6,7 @@   , flagSelectedMessage   , pinSelectedMessage   , viewSelectedMessage+  , openSelectedMessageInEditor   , fillSelectedGap   , copyPostLink   , yankSelectedMessageVerbatim@@ -34,9 +35,14 @@ 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@@ -125,6 +131,12 @@     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@@ -154,6 +166,28 @@     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 ()
src/Matterhorn/State/Messages.hs view
@@ -48,7 +48,6 @@ import           Matterhorn.State.Common import           Matterhorn.State.ThreadWindow import           Matterhorn.State.MessageSelect-import           Matterhorn.State.Reactions import           Matterhorn.State.Users import           Matterhorn.TimeUtils import           Matterhorn.Types@@ -177,7 +176,9 @@             Nothing -> return Nothing             Just tId -> Just <$> getServerBaseUrl tId -        let (msg, mentionedUsers) = clientPostToMessage (toClientPost mBaseUrl new (new^.postRootIdL))+        hostname <- use (csResources.crConn.cdHostnameL)++        let (msg, mentionedUsers) = clientPostToMessage (toClientPost hostname mBaseUrl new (new^.postRootIdL))             isEditedMessage m = m^.mMessageId == Just (MessagePostId $ new^.postIdL)          csChannel (new^.postChannelIdL) . ccMessageInterface.miMessages . traversed . filtered isEditedMessage .= msg@@ -193,8 +194,6 @@             csChannel (new^.postChannelIdL) %= adjustEditedThreshold new          csPostMap.ix(postId new) .= msg-        asyncFetchReactionsForPost (postChannelId new) new-        asyncFetchAttachments new  deleteMessage :: Post -> MH () deleteMessage new = do@@ -530,6 +529,8 @@           OldPost p      -> (p, False)           RecentPost p m -> (p, m) +    hostname <- use (csResources.crConn.cdHostnameL)+     st <- use id     case st ^? csChannel(postChannelId new) of         Nothing -> do@@ -566,7 +567,7 @@                 Nothing -> return Nothing                 Just tId -> Just <$> getServerBaseUrl tId -            let cp = toClientPost mBaseUrl new (new^.postRootIdL)+            let cp = toClientPost hostname mBaseUrl new (new^.postRootIdL)                 fromMe = (cp^.cpUser == (Just $ myUserId st)) &&                          (isNothing $ cp^.cpUserOverride)                 userPrefs = st^.csResources.crUserPreferences@@ -627,8 +628,6 @@                     -- since all DM channels appear in all teams.                     addPostToOpenThread (mTId <|> mcurTId) new msg' -                    asyncFetchReactionsForPost cId new-                    asyncFetchAttachments new                     postedChanMessage                  doHandleAddedMessage = do@@ -1053,39 +1052,6 @@                        (\c p -> Just $ do                            csChannel(c).ccInfo.cdFetchPending .= False                            addObtainedMessages c (-numToRequest) addTrailingGap p >>= postProcessMessageAdd)--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-            csChannelMessages(cId).traversed %= addAttachment--            curTId <- use csCurrentTeamId-            withChannel cId $ \chan -> do-                let mTId = chan^.ccInfo.cdTeamId <|> curTId-                case mTId of-                    Nothing -> return ()-                    Just tId -> modifyEachThreadMessage tId cId addAttachment--            invalidateChannelRenderingCache cId-            invalidateMessageRenderingCacheByPostId pId  -- | Given a post ID, switch to that post's channel and select the post -- in message selection mode.
src/Matterhorn/State/Reactions.hs view
@@ -80,9 +80,10 @@      invalidateRenderCache   where upd m | m^.mMessageId == Just (MessagePostId $ r^.reactionPostIdL) =-                  m & mReactions %~ (Map.alter delReaction (r^.reactionEmojiNameL))+                  m & mReactions %~ (removeEmptySets . Map.alter delReaction (r^.reactionEmojiNameL))               | otherwise = m         delReaction mUs = S.delete (r^.reactionUserIdL) <$> mUs+        removeEmptySets = Map.filter (not . S.null)         invalidateRenderCache =             invalidateMessageRenderingCacheByPostId $ r^.reactionPostIdL 
src/Matterhorn/Types/Core.hs view
@@ -360,6 +360,7 @@   | EditMessageEvent   | ReplyMessageEvent   | ReactToMessageEvent+  | OpenMessageInExternalEditorEvent    -- Attachments   | AttachmentListAddEvent@@ -462,6 +463,7 @@     , ("activate-list-item", ActivateListItemEvent)     , ("open-thread", OpenThreadEvent)     , ("flag-message", FlagMessageEvent)+    , ("open-message-in-editor", OpenMessageInExternalEditorEvent)     , ("pin-message", PinMessageEvent)     , ("view-message", ViewMessageEvent)     , ("fetch-for-gap", FillGapEvent)
src/Matterhorn/Types/EditState.hs view
@@ -43,6 +43,7 @@    , AutocompleteAlternative(..)   , autocompleteAlternativeReplacement+  , autocompleteAlternativeText   ) where @@ -88,6 +89,22 @@     -- ^ Source, name of a slash command, argspec, and description     | EmojiCompletion Text     -- ^ The text of an emoji completion++autocompleteAlternativeText :: AutocompleteAlternative -> Text+autocompleteAlternativeText (UserCompletion u _) =+    userUsername u+autocompleteAlternativeText (SpecialMention MentionChannel) =+    "channel"+autocompleteAlternativeText (SpecialMention MentionAll) =+    "all"+autocompleteAlternativeText (ChannelCompletion _ c) =+    unsafeUserText $ channelName c+autocompleteAlternativeText (SyntaxCompletion n) =+    n+autocompleteAlternativeText (CommandCompletion _ t _ _) =+    t+autocompleteAlternativeText (EmojiCompletion t) =+    t  -- | The source of an autocompletion alternative. data CompletionSource = Server | Client
src/Matterhorn/Types/Posts.hs view
@@ -48,6 +48,7 @@ import           Prelude () import           Matterhorn.Prelude +import qualified Data.Foldable as F import qualified Data.Map.Strict as Map import qualified Data.Sequence as Seq import qualified Data.Text as T@@ -193,10 +194,17 @@                   else t unEmote _ t = t +attachmentFromFileInfo :: T.Text -> FileInfo -> Attachment+attachmentFromFileInfo hostname info =+    let scheme = "https://"+        attUrl = scheme <> hostname <> urlForFile fId+        fId = fileInfoId info+    in mkAttachment (fileInfoName info) attUrl fId+ -- | Convert a Mattermost 'Post' to a 'ClientPost', passing in a --   'ParentId' if it has a known one.-toClientPost :: Maybe TeamBaseURL -> Post -> Maybe PostId -> ClientPost-toClientPost baseUrl p parentId =+toClientPost :: T.Text -> Maybe TeamBaseURL -> Post -> Maybe PostId -> ClientPost+toClientPost hostname baseUrl p parentId =   let src = unEmote (postClientPostType p) $ sanitizeUserText $ postMessage p   in ClientPost { _cpText          = parseMarkdown baseUrl src <> getAttachmentText p                 , _cpMarkdownSource = src@@ -207,14 +215,19 @@                 , _cpPending       = False                 , _cpDeleted       = False                 , _cpPinned        = fromMaybe False $ postPinned p-                , _cpAttachments   = Seq.empty+                , _cpAttachments   = attachmentFromFileInfo hostname <$> postMetadataFiles (postMetadata p)                 , _cpInReplyToPost = parentId                 , _cpPostId        = p^.postIdL                 , _cpChannelId     = p^.postChannelIdL-                , _cpReactions     = Map.empty+                , _cpReactions     = mkReactionMap $ postMetadataReactions (postMetadata p)                 , _cpOriginalPost  = p                 , _cpFromWebhook   = fromMaybe False $ p^.postPropsL.postPropsFromWebhookL                 }++mkReactionMap :: Seq Reaction -> Map.Map T.Text (S.Set UserId)+mkReactionMap rs =+    let inserter r m = Map.insertWith S.union (reactionEmojiName r) (S.singleton $ reactionUserId r) m+    in F.foldr inserter mempty rs  -- | Right now, instead of treating 'attachment' properties specially, we're --   just going to roll them directly into the message text