matterhorn-40700.0.0: src/State/UserListOverlay.hs
module State.UserListOverlay
( enterChannelMembersUserList
, enterChannelInviteUserList
, enterDMSearchUserList
, resetUserListSearch
, exitUserListMode
, userListActivateCurrent
, userListSelectDown
, userListSelectUp
, userListPageDown
, userListPageUp
, userListSearchString
)
where
import Control.Monad (when)
import Data.Monoid ((<>))
import qualified Data.Vector as Vec
import qualified Data.Foldable as F
import qualified Data.Text as T
import qualified Data.Sequence as Seq
import qualified Data.HashMap.Strict as HM
import Data.Maybe (fromMaybe)
import Lens.Micro.Platform
import qualified Network.Mattermost.Endpoints as MM
import Network.Mattermost.Types
import qualified Data.Text.Zipper as Z
import qualified Brick.Widgets.List as L
import qualified Brick.Widgets.Edit as E
import Types
import Types.Users
import State.Common
import State (changeChannel, addUserToCurrentChannel)
-- | Show the user list overlay for searching/showing members of the
-- current channel.
enterChannelMembersUserList :: MH ()
enterChannelMembersUserList = do
cId <- use csCurrentChannelId
myId <- gets myUserId
enterUserListMode (ChannelMembers cId)
(\u -> case u^.uiId /= myId of
True -> changeChannel (u^.uiName) >> return True
False -> return False
)
-- | Show the user list overlay for showing users that are not members
-- of the current channel for the purpose of adding them to the
-- channel.
enterChannelInviteUserList :: MH ()
enterChannelInviteUserList = do
cId <- use csCurrentChannelId
myId <- gets myUserId
enterUserListMode (ChannelNonMembers cId)
(\u -> case u^.uiId /= myId of
True -> addUserToCurrentChannel (u^.uiName) >> return True
False -> return False
)
-- | Show the user list overlay for showing all users for the purpose of
-- starting a direct message channel with another user.
enterDMSearchUserList :: MH ()
enterDMSearchUserList = do
myId <- gets myUserId
enterUserListMode AllUsers
(\u -> case u^.uiId /= myId of
True -> changeChannel (u^.uiName) >> return True
False -> return False
)
-- | Interact with the currently-selected user (depending on how the
-- overlay is configured).
userListActivateCurrent :: MH ()
userListActivateCurrent = do
mItem <- L.listSelectedElement <$> use (csUserListOverlay.userListSearchResults)
case mItem of
Nothing -> return ()
Just (_, user) -> do
handler <- use (csUserListOverlay.userListEnterHandler)
activated <- handler user
if activated
then setMode Main
else return ()
-- | Show the user list overlay with the given search scope, and issue a
-- request to gather the first search results.
enterUserListMode :: UserSearchScope -> (UserInfo -> MH Bool) -> MH ()
enterUserListMode scope enterHandler = do
csUserListOverlay.userListSearchScope .= scope
csUserListOverlay.userListSelected .= Nothing
csUserListOverlay.userListSearchInput.E.editContentsL %= Z.clearZipper
csUserListOverlay.userListEnterHandler .= enterHandler
csUserListOverlay.userListSearching .= False
csUserListOverlay.userListHasAllResults .= False
setMode UserListOverlay
resetUserListSearch
resetUserListSearch :: MH ()
resetUserListSearch = do
searchPending <- use (csUserListOverlay.userListSearching)
when (not searchPending) $ do
searchString <- userListSearchString
csUserListOverlay.userListSearching .= True
csUserListOverlay.userListHasAllResults .= False
csUserListOverlay.userListSearchResults .= listFromUserSearchResults mempty
session <- getSession
scope <- use (csUserListOverlay.userListSearchScope)
myTId <- gets myTeamId
doAsyncWith Preempt $ do
results <- fetchInitialResults myTId scope session searchString
return $ do
let lst = listFromUserSearchResults results
csUserListOverlay.userListSearchResults .= lst
-- NOTE: Disabled for now. See the hack note below for
-- details.
--
-- csUserListOverlay.userListHasAllResults .= (length results < searchResultsChunkSize)
csUserListOverlay.userListSearching .= False
-- Now that the results are available, check to see if the
-- search string changed since this request was submitted.
-- If so, issue another search.
afterSearchString <- userListSearchString
when (searchString /= afterSearchString) resetUserListSearch
userInfoFromPair :: User -> T.Text -> UserInfo
userInfoFromPair u status =
userInfoFromUser u True & uiStatus .~ statusFromText status
-- | Clear out the state of the user list overlay and return to the Main
-- mode.
exitUserListMode :: MH ()
exitUserListMode = do
csUserListOverlay.userListSearchResults .= listFromUserSearchResults mempty
csUserListOverlay.userListSelected .= Nothing
csUserListOverlay.userListEnterHandler .= (const $ return False)
setMode Main
-- | Move the selection up in the user list overlay by one user.
userListSelectUp :: MH ()
userListSelectUp = userListMove L.listMoveUp
-- | Move the selection down in the user list overlay by one user.
userListSelectDown :: MH ()
userListSelectDown = userListMove L.listMoveDown
-- | Move the selection up in the user list overlay by a page of users
-- (userListPageSize).
userListPageUp :: MH ()
userListPageUp = userListMove (L.listMoveBy (-1 * userListPageSize))
-- | Move the selection down in the user list overlay by a page of users
-- (userListPageSize).
userListPageDown :: MH ()
userListPageDown = userListMove (L.listMoveBy userListPageSize)
-- | Transform the user list results in some way, e.g. by moving the
-- cursor, and then check to see whether the modification warrants a
-- prefetch of more search results.
userListMove :: (L.List Name UserInfo -> L.List Name UserInfo) -> MH ()
userListMove f = do
csUserListOverlay.userListSearchResults %= f
-- NOTE! Do not enable this. See the docs for maybePrefetchNextChunk.
-- For now we want to keep the code around in case it can be
-- reinstated in the future.
-- maybePrefetchNextChunk
-- | We'll attempt to prefetch the next page of results if the cursor
-- gets within this many positions of the last result we have.
selectionPrefetchDelta :: Int
selectionPrefetchDelta = 10
-- Prefetch the next chunk of user list search results if all of the
-- following are true:
--
-- * the search string is empty (because we can't paginate searches,
-- just fetches for all users), and
-- * cursor is within selectionPrefetchDelta positions of the end of
-- list, and
-- * the length of the current results list is exactly a multiple of
-- fetching chunk size (thus indicating a very high probability that
-- there are more results to be fetched).
--
-- NOTE: this function should be reinstated and called in 'userListMove'
-- if we start using the /users endpoint again in the future. See the
-- hack note in the getUserSearchResultsPage below for details. In the
-- mean time, no pagination of results is possible so no prefetching
-- should be done.
_maybePrefetchNextChunk :: MH ()
_maybePrefetchNextChunk = do
gettingMore <- use (csUserListOverlay.userListRequestingMore)
hasAll <- use (csUserListOverlay.userListHasAllResults)
searchString <- userListSearchString
curIdx <- use (csUserListOverlay.userListSearchResults.L.listSelectedL)
numResults <- use (csUserListOverlay.userListSearchResults.to F.length)
let selectionNearEnd = case curIdx of
Nothing -> False
Just i -> numResults - (i + 1) < selectionPrefetchDelta
when (not hasAll && T.null searchString && not gettingMore && selectionNearEnd) $ do
let pageNum = numResults `div` searchResultsChunkSize
csUserListOverlay.userListRequestingMore .= True
session <- getSession
scope <- use (csUserListOverlay.userListSearchScope)
myTId <- gets myTeamId
doAsyncWith Preempt $ do
newChunk <- getUserSearchResultsPage pageNum myTId scope session searchString
return $ do
-- Because we only ever append, this is safe to do w.r.t.
-- the selected index of the list. If we ever prepended or
-- removed, we'd also need to manage the selection index
-- to ensure it stays in bounds.
csUserListOverlay.userListSearchResults.L.listElementsL %= (<> newChunk)
csUserListOverlay.userListRequestingMore .= False
-- If we got fewer results than we asked for, then we have
-- them all!
--
-- NOTE: disabled for now, see the hack note below.
--
-- csUserListOverlay.userListHasAllResults .=
-- (length newChunk < searchResultsChunkSize)
-- | The number of users in a "page" for cursor movement purposes.
userListPageSize :: Int
userListPageSize = 10
-- | Perform an initial request for search results in the specified
-- scope.
fetchInitialResults :: TeamId -> UserSearchScope -> Session -> T.Text -> IO (Vec.Vector UserInfo)
fetchInitialResults = getUserSearchResultsPage 0
searchResultsChunkSize :: Int
searchResultsChunkSize = 40
getUserSearchResultsPage :: Int
-- ^ The page number of results to fetch, starting at zero.
-> TeamId
-- ^ My team ID.
-> UserSearchScope
-- ^ The scope to search
-> Session
-- ^ The connection session
-> T.Text
-- ^ The search string
-> IO (Vec.Vector UserInfo)
getUserSearchResultsPage _pageNum myTId scope s searchString = do
-- Unfortunately, we don't get pagination control when there is a
-- search string in effect. We'll get at most 100 results from a
-- search.
let query = UserSearch { userSearchTerm = if T.null searchString then " " else searchString
-- Hack alert: Searching with the string " "
-- above is a hack to use the search
-- endpoint to get "all users" instead of
-- those matching a particular non-empty
-- non-whitespace string. This is because
-- only the search endpoint provides a
-- control to eliminate deleted users from
-- the results. If we don't do this, and
-- use the /users endpoint instead, we'll
-- get deleted users in those results and
-- then those deleted users will disappear
-- from the results once the user enters a
-- non-empty string string.
, userSearchAllowInactive = False
, userSearchWithoutTeam = False
, userSearchInChannelId = case scope of
ChannelMembers cId -> Just cId
_ -> Nothing
, userSearchNotInTeamId = Nothing
, userSearchNotInChannelId = case scope of
ChannelNonMembers cId -> Just cId
_ -> Nothing
, userSearchTeamId = case scope of
ChannelNonMembers _ -> Just myTId
_ -> Nothing
}
users <- MM.mmSearchUsers query s
let uList = F.toList users
uIds = userId <$> uList
-- Now fetch status info for the users we got.
case null uList of
False -> do
statuses <- MM.mmGetUserStatusByIds (Seq.fromList uIds) s
let statusMap = HM.fromList [ (statusUserId e, statusStatus e) | e <- F.toList statuses ]
usersWithStatus = [ userInfoFromPair u (fromMaybe "" $ HM.lookup (userId u) statusMap)
| u <- uList
]
return $ Vec.fromList usersWithStatus
True -> return mempty
userListSearchString :: MH T.Text
userListSearchString =
(head . E.getEditContents) <$> use (csUserListOverlay.userListSearchInput)