diff --git a/ChangeLog.md b/ChangeLog.md index 687f20ba..557f666b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,13 +3,23 @@ This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released, and the date should reflect that release. ## Unreleased -(Creating a separate list, because the previous changes probably deserve their own release) +* Open source some "incremental view" infra (name is provision). + +## v1.1.0.0 2024-05-24 + +* Breaking: Remove Reflex.Dom.Modal.Base and Reflex.Dom.Modal.Class. The `` element is now broadly supported by browsers and provides a simpler solution to the problem of opening modals that is also more accessible. See the [documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog), and, in particular, this [example](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement#opening_a_modal_dialog), which uses `showModal` and describes how to style the modal backdrop. +* Breaking: [Make authentication easier to use and fix some things about ErrorV #213](https://github.com/obsidiansystems/rhyolite/pull/213) +* Make it possible to use Rhyolite.Backend.Account without notifications. See Rhyolite.Backend.Account.Db for versions of createAccount and ensureAccountExists that don't send notifications. +* Update to obelisk v1.3.0.0 + +## v1.0.0.0 2023-08-03 * Breaking: Drop groundhog support * Breaking: Use Commutative from commutative-semigroups instead of Additive from patch * Update to vessel-0.3 * Support ghc-8.10 * Add Data.Vessel.Void +* Move .obelisk/impl to dep/obelisk * Breaking: handleAuthMapQuery and handlePersonalAuthMapQuery now take pure functions for decrypting user tokens. This is fine in practice because it should almost always be readSignedWithKey from signed-data, partially applied to a CSK. We had a major performance issue when someone stuck a database query inside diff --git a/README.md b/README.md index 0b7ea9ce..beb66dfe 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,12 @@ Rhyolite provides: { system ? builtins.currentSystem, obelisk ? import ./.obelisk/impl { inherit system; iosSdkVersion = "13.2"; - + # You must accept the Android Software Development Kit License Agreement at # https://developer.android.com/studio/terms in order to build Android apps. # Uncomment and set this to `true` to indicate your acceptance: # config.android_sdk.accept_license = false; - + # In order to use Let's Encrypt for HTTPS deployments you must accept # their terms of service at https://letsencrypt.org/repository/. # Uncomment and set this to `true` to indicate your acceptance: @@ -41,7 +41,7 @@ Rhyolite provides: } }: with obelisk; project ./. ({ pkgs, hackGet, ... }@args: { - + overrides = pkgs.lib.composeExtensions (pkgs.callPackage (hackGet ./dep/rhyolite) args).haskellOverrides (self: super: @@ -49,7 +49,7 @@ Rhyolite provides: { # Your custom overrides go here. }); - + android.applicationId = "systems.obsidian.obelisk.examples.minimal"; android.displayName = "Obelisk Minimal Example"; ios.bundleIdentifier = "systems.obsidian.obelisk.examples.minimal"; @@ -74,8 +74,9 @@ with this Rhyolite thunk: "owner": "obsidiansystems", "repo": "rhyolite", "branch": "master", - "rev": "06b9851a101408a86a4ec0b7df5b2f71bc532ab0", - "sha256": "18adbc1nnj94qhggpcxmpd5i1rz0zx93cpphl09mw4c7s65rzah7" + "private": false, + "rev": "9f13d8d8a2233aae54e15c39acf68181893b859a", + "sha256": "1vhbw9bdqpfddavfjfdrq6kk5wwsd8hbgb8pnna9i2db3x3cmzvy" } ``` @@ -96,5 +97,5 @@ You can use `nix-shell` to enter a shell from which you can build any of the sub Because of the inter-related nature of these packages, `rhyolite-test-suite` tests that all of them can be built against one another. To test, run: ```bash -nix-shell --run cabal build test +nix-shell --run "cabal build test" ``` diff --git a/account/backend/rhyolite-account-backend.cabal b/account/backend/rhyolite-account-backend.cabal index 71033d12..2449f253 100644 --- a/account/backend/rhyolite-account-backend.cabal +++ b/account/backend/rhyolite-account-backend.cabal @@ -13,6 +13,8 @@ category: Web library exposed-modules: Rhyolite.Backend.Account + Rhyolite.Backend.Account.Db + Rhyolite.Backend.Account.Notify build-depends: base , aeson , beam-core diff --git a/account/backend/src/Rhyolite/Backend/Account.hs b/account/backend/src/Rhyolite/Backend/Account.hs index 2241d7f3..5d7ceda9 100644 --- a/account/backend/src/Rhyolite/Backend/Account.hs +++ b/account/backend/src/Rhyolite/Backend/Account.hs @@ -8,9 +8,8 @@ Description: {-# Language OverloadedStrings #-} module Rhyolite.Backend.Account ( createAccount - , login , ensureAccountExists - , ensureAccountExistsNoNotify + , login , setAccountPassword , setAccountPasswordHash , makePasswordHash @@ -20,200 +19,5 @@ module Rhyolite.Backend.Account , resetPasswordHash ) where -import Control.Monad (guard) -import Control.Monad.Trans.Maybe -import Crypto.PasswordStore -import Data.Aeson -import Data.ByteString -import Data.Constraint.Extras -import Data.Constraint.Forall -import Data.Functor.Identity -import Data.Maybe -import Data.Text -import Data.Text.Encoding (encodeUtf8) -import qualified Data.Text.Encoding as T -import Data.Time -import Database.Beam -import Database.Beam.Backend.SQL.BeamExtensions -import Database.Beam.Postgres -import Data.Signed -import Data.Signed.ClientSession -import Database.Beam.Postgres.Full hiding (insert) -import Database.Beam.Postgres.Syntax -import Database.PostgreSQL.Simple.Beam () -import Rhyolite.Account -import Rhyolite.DB.Beam (current_timestamp_) -import Rhyolite.DB.NotifyListen -import Rhyolite.DB.NotifyListen.Beam -import Web.ClientSession as CS - --- | Creates a new account and emits a db notification about it -createAccount - :: (Has' ToJSON notice Identity, ForallF ToJSON notice) - => DatabaseEntity Postgres db (TableEntity Account) - -> notice (PrimaryKey Account Identity) - -> Text - -> Text - -> Pg (Either Text (PrimaryKey Account Identity)) -createAccount accountTable noticeWrapper email pass = do - hash <- makePasswordHash pass - accountIds <- runPgInsertReturningList $ flip returning _account_id $ insert accountTable $ insertExpressions - [ Account - { _account_id = default_ - , _account_email = lower_ (val_ email) - , _account_password = val_ (Just hash) - , _account_passwordResetNonce = just_ current_timestamp_ - } - ] - case accountIds of - [accountId] -> do - notify NotificationType_Insert noticeWrapper (AccountId accountId) - pure $ Right $ AccountId accountId - _ -> pure $ Left "Failed to create account" - --- | Attempts to login a user given some credentials. -login - :: Database Postgres db - => DatabaseEntity Postgres db (TableEntity Account) - -> Text - -> Text - -> Pg (Maybe (PrimaryKey Account Identity)) -login accountTable email pass = runMaybeT $ do - (aid, mPwHash) <- MaybeT $ fmap listToMaybe $ runSelectReturningList $ select $ do - acc <- all_ accountTable - guard_ $ lower_ (_account_email acc) ==. lower_ (val_ email) - pure (_account_id acc, _account_password acc) - pwHash <- MaybeT $ pure mPwHash - guard $ verifyPasswordWith pbkdf2 (2^) (T.encodeUtf8 pass) pwHash - pure (AccountId aid) - -ensureAccountExistsNoNotify - :: (Database Postgres db) - => DatabaseEntity Postgres db (TableEntity Account) - -> Text - -> Pg (Bool, PrimaryKey Account Identity) -ensureAccountExistsNoNotify accountTable email = do - existingAccountId <- runSelectReturningOne $ select $ fmap primaryKey $ filter_ (\x -> - lower_ (_account_email x) ==. lower_ (val_ email)) $ all_ accountTable - case existingAccountId of - Just existing -> return (False, existing) - Nothing -> do - results <- runInsertReturningList $ insert accountTable $ insertExpressions - [ Account - { _account_id = default_ - , _account_email = lower_ (val_ email) - , _account_password = nothing_ - , _account_passwordResetNonce = nothing_ - } - ] - case results of - [acc] -> do - let aid = primaryKey acc - -- notify NotificationType_Insert (notification accountTable) aid - pure (True, aid) - _ -> error "ensureAccountExists: Creating account failed" - -ensureAccountExists - :: (Database Postgres db, HasNotification n Account, Has' ToJSON n Identity, ForallF ToJSON n) - => DatabaseEntity Postgres db (TableEntity Account) - -> Text - -> Pg (Bool, PrimaryKey Account Identity) -ensureAccountExists accountTable email = do - existingAccountId <- runSelectReturningOne $ select $ fmap primaryKey $ filter_ (\x -> - lower_ (_account_email x) ==. lower_ (val_ email)) $ all_ accountTable - case existingAccountId of - Just existing -> return (False, existing) - Nothing -> do - results <- runInsertReturningList $ insert accountTable $ insertExpressions - [ Account - { _account_id = default_ - , _account_email = lower_ (val_ email) - , _account_password = nothing_ - , _account_passwordResetNonce = nothing_ - } - ] - case results of - [acc] -> do - let aid = primaryKey acc - notify NotificationType_Insert (notification accountTable) aid - pure (True, aid) - _ -> error "ensureAccountExists: Creating account failed" - -setAccountPassword - :: DatabaseEntity Postgres db (TableEntity Account) - -> PrimaryKey Account Identity - -> Text - -> Pg () -setAccountPassword tbl aid password = do - pw <- liftIO $ makePasswordHash password - setAccountPasswordHash tbl aid pw - -setAccountPasswordHash - :: DatabaseEntity Postgres db (TableEntity Account) - -> PrimaryKey Account Identity - -> ByteString - -> Pg () -setAccountPasswordHash accountTable aid hash = runUpdate $ update accountTable - (\x -> mconcat - [ _account_password x <-. val_ (Just hash) - , _account_passwordResetNonce x <-. nothing_ - ] - ) - (\x -> primaryKey x ==. val_ aid) - -makePasswordHash - :: MonadIO m - => Text - -> m ByteString -makePasswordHash pw = do - salt <- liftIO genSaltIO - return $ makePasswordSaltWith pbkdf2 (2^) (encodeUtf8 pw) salt 14 - -resetPassword - :: (Database Postgres db) - => DatabaseEntity Postgres db (TableEntity Account) - -> PrimaryKey Account Identity - -> UTCTime - -> Text - -> Pg (Maybe (PrimaryKey Account Identity)) -resetPassword tbl aid t pw = do - hash <- makePasswordHash pw - resetPasswordHash tbl aid t hash - -resetPasswordHash - :: (Database Postgres db) - => DatabaseEntity Postgres db (TableEntity Account) - -> PrimaryKey Account Identity - -> UTCTime - -> ByteString - -> Pg (Maybe (PrimaryKey Account Identity)) -resetPasswordHash accountTable aid nonce pwhash = do - macc <- runSelectReturningOne $ lookup_ accountTable aid - case macc of - Nothing -> return Nothing - Just a -> if _account_passwordResetNonce a == Just nonce - then do - setAccountPasswordHash accountTable aid pwhash - return $ Just aid - else fail "nonce mismatch" - -passwordResetToken - :: MonadIO m - => CS.Key - -> PrimaryKey Account Identity - -> UTCTime - -> m (Signed PasswordResetToken) -passwordResetToken csk aid nonce = do - liftIO $ signWithKey csk $ PasswordResetToken (aid, nonce) - -newNonce - :: DatabaseEntity Postgres db (TableEntity Account) - -> PrimaryKey Account Identity - -> Pg (Maybe UTCTime) -newNonce accountTable aid = do - a <- runUpdateReturningList $ update accountTable - (\x -> _account_passwordResetNonce x <-. just_ current_timestamp_) - (\x -> primaryKey x ==. val_ aid) - pure $ case a of - [acc] -> _account_passwordResetNonce acc - _ -> Nothing +import Rhyolite.Backend.Account.Db hiding (createAccount, ensureAccountExists) +import Rhyolite.Backend.Account.Notify (createAccount, ensureAccountExists) diff --git a/account/backend/src/Rhyolite/Backend/Account/Db.hs b/account/backend/src/Rhyolite/Backend/Account/Db.hs new file mode 100644 index 00000000..565ffd05 --- /dev/null +++ b/account/backend/src/Rhyolite/Backend/Account/Db.hs @@ -0,0 +1,184 @@ +{-| +Description: + Create or modify accounts in the database + + This module does not handle notifications. See + Rhyolite.Backend.Account.Notify for that +-} +{-# Language DeriveGeneric #-} +{-# Language FlexibleContexts #-} +{-# Language MonoLocalBinds #-} +{-# Language OverloadedStrings #-} +module Rhyolite.Backend.Account.Db + ( createAccount + , login + , ensureAccountExists + , setAccountPassword + , setAccountPasswordHash + , makePasswordHash + , passwordResetToken + , newNonce + , resetPassword + , resetPasswordHash + ) where + +import Control.Monad (guard) +import Control.Monad.Trans.Maybe +import Crypto.PasswordStore +import Data.ByteString +import Data.Functor.Identity +import Data.Maybe +import Data.Signed +import Data.Signed.ClientSession +import Data.Text +import Data.Text.Encoding (encodeUtf8) +import qualified Data.Text.Encoding as T +import Data.Time +import Database.Beam +import Database.Beam.Backend.SQL.BeamExtensions +import Database.Beam.Postgres +import Database.Beam.Postgres.Full hiding (insert) +import Database.PostgreSQL.Simple.Beam () +import Rhyolite.Account +import Rhyolite.DB.Beam (current_timestamp_) +import Web.ClientSession as CS + +-- | Creates a new account and emits a db notification about it +createAccount + :: DatabaseEntity Postgres db (TableEntity Account) + -> Text + -> Text + -> Pg (Either Text (PrimaryKey Account Identity)) +createAccount accountTable email pass = do + hash <- makePasswordHash pass + accountIds <- runPgInsertReturningList $ flip returning _account_id $ insert accountTable $ insertExpressions + [ Account + { _account_id = default_ + , _account_email = lower_ (val_ email) + , _account_password = val_ (Just hash) + , _account_passwordResetNonce = just_ current_timestamp_ + } + ] + case accountIds of + [accountId] -> pure $ Right $ AccountId accountId + _ -> pure $ Left "Failed to create account" + +-- | Attempts to login a user given some credentials. +login + :: Database Postgres db + => DatabaseEntity Postgres db (TableEntity Account) + -> Text + -> Text + -> Pg (Maybe (PrimaryKey Account Identity)) +login accountTable email pass = runMaybeT $ do + (aid, mPwHash) <- MaybeT $ fmap listToMaybe $ runSelectReturningList $ select $ do + acc <- all_ accountTable + guard_ $ lower_ (_account_email acc) ==. lower_ (val_ email) + pure (_account_id acc, _account_password acc) + pwHash <- MaybeT $ pure mPwHash + guard $ verifyPasswordWith pbkdf2 (2^) (T.encodeUtf8 pass) pwHash + pure (AccountId aid) + +ensureAccountExists + :: (Database Postgres db) + => DatabaseEntity Postgres db (TableEntity Account) + -> Text + -> Pg (Bool, PrimaryKey Account Identity) +ensureAccountExists accountTable email = do + existingAccountId <- runSelectReturningOne $ select $ fmap primaryKey $ filter_ (\x -> + lower_ (_account_email x) ==. lower_ (val_ email)) $ all_ accountTable + case existingAccountId of + Just existing -> return (False, existing) + Nothing -> do + results <- runInsertReturningList $ insert accountTable $ insertExpressions + [ Account + { _account_id = default_ + , _account_email = lower_ (val_ email) + , _account_password = nothing_ + , _account_passwordResetNonce = nothing_ + } + ] + case results of + [acc] -> do + let aid = primaryKey acc + pure (True, aid) + _ -> error "ensureAccountExists: Creating account failed" + +setAccountPassword + :: DatabaseEntity Postgres db (TableEntity Account) + -> PrimaryKey Account Identity + -> Text + -> Pg () +setAccountPassword tbl aid password = do + pw <- liftIO $ makePasswordHash password + setAccountPasswordHash tbl aid pw + +setAccountPasswordHash + :: DatabaseEntity Postgres db (TableEntity Account) + -> PrimaryKey Account Identity + -> ByteString + -> Pg () +setAccountPasswordHash accountTable aid hash = runUpdate $ update accountTable + (\x -> mconcat + [ _account_password x <-. val_ (Just hash) + , _account_passwordResetNonce x <-. nothing_ + ] + ) + (\x -> primaryKey x ==. val_ aid) + +makePasswordHash + :: MonadIO m + => Text + -> m ByteString +makePasswordHash pw = do + salt <- liftIO genSaltIO + return $ makePasswordSaltWith pbkdf2 (2^) (encodeUtf8 pw) salt 14 + +resetPassword + :: (Database Postgres db) + => DatabaseEntity Postgres db (TableEntity Account) + -> PrimaryKey Account Identity + -> UTCTime + -> Text + -> Pg (Maybe (PrimaryKey Account Identity)) +resetPassword tbl aid t pw = do + hash <- makePasswordHash pw + resetPasswordHash tbl aid t hash + +resetPasswordHash + :: (Database Postgres db) + => DatabaseEntity Postgres db (TableEntity Account) + -> PrimaryKey Account Identity + -> UTCTime + -> ByteString + -> Pg (Maybe (PrimaryKey Account Identity)) +resetPasswordHash accountTable aid nonce pwhash = do + macc <- runSelectReturningOne $ lookup_ accountTable aid + case macc of + Nothing -> return Nothing + Just a -> if _account_passwordResetNonce a == Just nonce + then do + setAccountPasswordHash accountTable aid pwhash + return $ Just aid + else fail "nonce mismatch" + +passwordResetToken + :: MonadIO m + => CS.Key + -> PrimaryKey Account Identity + -> UTCTime + -> m (Signed PasswordResetToken) +passwordResetToken csk aid nonce = do + liftIO $ signWithKey csk $ PasswordResetToken (aid, nonce) + +newNonce + :: DatabaseEntity Postgres db (TableEntity Account) + -> PrimaryKey Account Identity + -> Pg (Maybe UTCTime) +newNonce accountTable aid = do + a <- runUpdateReturningList $ update accountTable + (\x -> _account_passwordResetNonce x <-. just_ current_timestamp_) + (\x -> primaryKey x ==. val_ aid) + pure $ case a of + [acc] -> _account_passwordResetNonce acc + _ -> Nothing diff --git a/account/backend/src/Rhyolite/Backend/Account/Notify.hs b/account/backend/src/Rhyolite/Backend/Account/Notify.hs new file mode 100644 index 00000000..13feca22 --- /dev/null +++ b/account/backend/src/Rhyolite/Backend/Account/Notify.hs @@ -0,0 +1,60 @@ +{-| +Description: + Create or modify accounts in the database, and send LISTEN notifications +-} +{-# Language DeriveGeneric #-} +{-# Language FlexibleContexts #-} +{-# Language MonoLocalBinds #-} +{-# Language OverloadedStrings #-} +module Rhyolite.Backend.Account.Notify + ( createAccount + , login + , ensureAccountExists + , setAccountPassword + , setAccountPasswordHash + , makePasswordHash + , passwordResetToken + , newNonce + , resetPassword + , resetPasswordHash + ) where + +import Data.Aeson +import Data.Constraint.Extras +import Data.Constraint.Forall +import Data.Functor.Identity +import Data.Text +import Database.Beam +import Database.Beam.Postgres +import Database.PostgreSQL.Simple.Beam () +import Rhyolite.Account +import Rhyolite.Backend.Account.Db hiding (createAccount, ensureAccountExists) +import qualified Rhyolite.Backend.Account.Db as Acc +import Rhyolite.DB.NotifyListen +import Rhyolite.DB.NotifyListen.Beam + +-- | Creates a new account and emits a db notification about it +createAccount + :: (Has' ToJSON notice Identity, ForallF ToJSON notice) + => DatabaseEntity Postgres db (TableEntity Account) + -> notice (PrimaryKey Account Identity) + -> Text + -> Text + -> Pg (Either Text (PrimaryKey Account Identity)) +createAccount accountTable noticeWrapper email pass = do + result <- Acc.createAccount accountTable email pass + case result of + Right accountId -> + notify NotificationType_Insert noticeWrapper accountId + _ -> pure () + pure result + +ensureAccountExists + :: (Database Postgres db, HasNotification n Account, Has' ToJSON n Identity, ForallF ToJSON n) + => DatabaseEntity Postgres db (TableEntity Account) + -> Text + -> Pg (Bool, PrimaryKey Account Identity) +ensureAccountExists accountTable email = do + aid <- Acc.ensureAccountExists accountTable email + notify NotificationType_Insert (notification accountTable) $ snd aid + pure aid diff --git a/account/types/src/Rhyolite/Account.hs b/account/types/src/Rhyolite/Account.hs index c9101b89..cc9efc7e 100644 --- a/account/types/src/Rhyolite/Account.hs +++ b/account/types/src/Rhyolite/Account.hs @@ -51,6 +51,8 @@ instance Table Account where instance Beamable (PrimaryKey Account) +type AccountId = PrimaryKey Account Identity + type HasAccountIdConstraint (c :: * -> Constraint) f = c (Columnar f (SqlSerial Int64)) deriving instance HasAccountIdConstraint ToJSON f => ToJSON (PrimaryKey Account f) diff --git a/common/Rhyolite/Vessel/AuthMapV.hs b/common/Rhyolite/Vessel/AuthMapV.hs index bebfa92c..329d87c1 100644 --- a/common/Rhyolite/Vessel/AuthMapV.hs +++ b/common/Rhyolite/Vessel/AuthMapV.hs @@ -25,6 +25,7 @@ import Data.Aeson import Data.Constraint.Extras import Data.Maybe import qualified Data.Map.Monoidal as MMap +import Data.Map.Monoidal (MonoidalMap) import qualified Data.Map as DataMap import qualified Data.Map.Strict as Map import Data.Patch @@ -56,9 +57,9 @@ getAuthMapV (AuthMapV v) = mkSubVessel $ MMap.mapMaybeWithKey (\_ -> snd . unsaf -- | Construct an authorised 'AuthMapV' makeAuthMapV :: (Ord auth, View v) - => SubVessel auth v g - -> AuthMapV auth v g -makeAuthMapV = AuthMapV . mkSubVessel . MMap.mapMaybeWithKey (\_ -> Just . liftErrorV) . getSubVessel + => SubVessel auth v Identity + -> AuthMapV auth v Identity +makeAuthMapV = AuthMapV . mkSubVessel . MMap.mapMaybeWithKey (\_ -> Just . successErrorV) . getSubVessel deriving instance (Ord auth, Eq (view g), Eq (g (First (Maybe ())))) => Eq (AuthMapV auth view g) @@ -213,11 +214,8 @@ handlePersonalAuthMapQuery => (token -> Maybe user) -- ^ How to figure out the identity corresponding to a token. Note: this is pure because it absolutely must be cheap. See the corresponding comment on -- 'handleAuthMapQuery'. - -> (forall f g. - ViewQueryResult f ~ g - => (forall x. x -> f x -> g x) - -> v (Compose (MMap.MonoidalMap user) f) - -> m (v (Compose (MMap.MonoidalMap user) g)) + -> (v (Compose (MMap.MonoidalMap user) Proxy) + -> m (v (Compose (MMap.MonoidalMap user) Identity)) ) -- ^ Handle the query for each individual identity -> AuthMapV token v p @@ -241,9 +239,6 @@ handlePersonalAuthMapQuery readToken handler vt = do . (MMap.foldMapWithKey $ \t (Compose u) -> TaggedQuery (Set.singleton t) <$ u) . getCompose - injectResult :: forall x. x -> TaggedQuery (Set token) x -> ((Set token), x) - injectResult x (TaggedQuery xs) = (xs, x) - disperseTokens :: MMap.MonoidalMap user (Set token, a) -> Compose (MMap.MonoidalMap token) q a @@ -257,11 +252,29 @@ handlePersonalAuthMapQuery readToken handler vt = do $ getSubVessel $ getAuthMapV vt - vt' <- handler injectResult $ mapV condenseTokens $ condenseV $ getSubVessel vtReadToken - - -- TODO: warn about collisions in alignWithV - pure $ alignWithV (these id id const) (AuthMapV $ mkSubVessel $ MMap.MonoidalMap invalidTokens) - (makeAuthMapV (mkSubVessel $ disperseV $ mapV (disperseTokens . getCompose) vt')) + let condensedV = condenseV $ getSubVessel vtReadToken + condensedTokensV :: v (Compose (MonoidalMap user) (TaggedQuery (Set token))) + condensedTokensV = mapV condenseTokens condensedV + queryByUserV :: v (Compose (MonoidalMap user) Proxy) + queryByUserV = mapV (\(Compose m) -> Compose (fmap (const Proxy) m)) condensedTokensV + + views <- handler queryByUserV + + let reattachToken :: TaggedQuery (Set token) a -> Identity a -> (Set token, a) + reattachToken (TaggedQuery s) (Identity x) = (s,x) + + reattachTokenMap :: Compose (MonoidalMap user) (TaggedQuery (Set token)) a + -> Compose (MonoidalMap user) Identity a + -> Compose (MonoidalMap user) ((,) (Set token)) a + reattachTokenMap (Compose mtokens) (Compose mviews) = Compose (MMap.intersectionWith reattachToken mtokens mviews) + tokenedViewsM :: Maybe (v (Compose (MonoidalMap user) ((,) (Set token)))) + tokenedViewsM = cropV reattachTokenMap condensedTokensV views + case tokenedViewsM of + Nothing -> return emptyV + Just tokenedViews -> + -- TODO: warn about collisions in alignWithV + pure $ alignWithV (these id id const) (AuthMapV $ mkSubVessel $ MMap.MonoidalMap invalidTokens) + (makeAuthMapV (mkSubVessel $ disperseV $ mapV (disperseTokens . getCompose) tokenedViews)) -- | A query morphism that takes a view for a single identity and lifts it to -- a map of identities to views. diff --git a/common/Rhyolite/Vessel/AuthenticatedV.hs b/common/Rhyolite/Vessel/AuthenticatedV.hs index 11777055..177aa454 100644 --- a/common/Rhyolite/Vessel/AuthenticatedV.hs +++ b/common/Rhyolite/Vessel/AuthenticatedV.hs @@ -18,7 +18,6 @@ {-# LANGUAGE RankNTypes #-} module Rhyolite.Vessel.AuthenticatedV where -import Control.Applicative import Control.Monad import Data.Aeson import Data.Aeson.GADT.TH @@ -33,7 +32,6 @@ import Data.Vessel import Data.Vessel.Vessel import Data.Semigroup import Data.Semigroup.Commutative -import Data.Vessel.Path (Keyed(..)) import GHC.Generics import Reflex.Query.Class import Data.Map.Monoidal (MonoidalMap) @@ -54,7 +52,6 @@ import Control.Applicative (Alternative) import Prelude hiding ((.), id) import Control.Category import Data.Vessel.ViewMorphism (ViewQueryResult, ViewMorphism(..), ViewHalfMorphism(..)) -import Data.Vessel.Vessel (vessel) import Data.Bifoldable -- | An internal key type used to glue together parts of a view selector @@ -168,11 +165,8 @@ handleAuthenticatedQuery -> (forall p'. private p' -> m (private q)) -- ^ The result of private queries is only available to authenticated identities -- but the result is the same for all of them. - -> ( forall f g. - ViewQueryResult f ~ g - => (forall x. x -> f x -> g x) - -> personal (Compose (MonoidalMap user) f) - -> m (personal (Compose (MonoidalMap user) g))) + -> ( personal (Compose (MonoidalMap user) Proxy) + -> m (personal (Compose (MonoidalMap user) Identity))) -- ^ The result of personal queries depends on the identity making the query -> AuthenticatedV public (AuthMapV token private) (AuthMapV token personal) p -> m (AuthenticatedV public (AuthMapV token private) (AuthMapV token personal) q) @@ -378,6 +372,8 @@ disperseAuthenticatedErrorV :: ( View publicV , Semigroup (publicV Identity) , EmptyView privateV , Semigroup (privateV Identity) , EmptyView personalV , Semigroup (personalV Identity) + , Num x, Semigroup x, Semigroup (privateV (Const x)) + , Semigroup (personalV (Const x)) ) => QueryMorphism (ErrorV () (AuthenticatedV publicV privateV personalV) (Const x)) @@ -385,10 +381,10 @@ disperseAuthenticatedErrorV :: disperseAuthenticatedErrorV = QueryMorphism (maybe emptyV (runIdentity . traverseAuthenticatedV pure - (pure . liftErrorV) - (pure . liftErrorV)) + (pure . queryErrorVConst) + (pure . queryErrorVConst)) . snd . unsafeObserveErrorV) - (bifoldMap @(,) (maybe emptyV failureErrorV . (=<<) (getFirst . runIdentity)) liftErrorV + (bifoldMap @(,) (maybe emptyV failureErrorV . (=<<) (getFirst . runIdentity)) successErrorV . traverseAuthenticatedV ((,) Nothing) (fmap (maybe emptyV id) . unsafeObserveErrorV) diff --git a/common/Rhyolite/Vessel/ErrorV/Internal.hs b/common/Rhyolite/Vessel/ErrorV/Internal.hs index 1686f9cc..b03a40c9 100644 --- a/common/Rhyolite/Vessel/ErrorV/Internal.hs +++ b/common/Rhyolite/Vessel/ErrorV/Internal.hs @@ -8,6 +8,7 @@ {-# Language LambdaCase #-} {-# Language MultiParamTypeClasses #-} {-# Language PolyKinds #-} +{-# Language RankNTypes #-} {-# Language StandaloneDeriving #-} {-# Language TemplateHaskell #-} {-# Language TypeFamilies #-} @@ -122,9 +123,17 @@ instance type QueryResult (ErrorV err v (Compose c g)) = ErrorV err v (Compose c (VesselLeafWrapper (QueryResult (Vessel (ErrorVK err v) g)))) crop (ErrorV s) (ErrorV r) = ErrorV $ crop s r +-- | Construct a query that registers interest in both the success and error parts of an ErrorV. +queryErrorV :: (View v, Semigroup (v Proxy)) => v Proxy -> ErrorV e v Proxy +queryErrorV v = ErrorV (singletonV ErrorVK_View v <> singletonV ErrorVK_Error (SingleV Proxy)) + +-- | Construct a query that registers interest in both the success and error parts of an ErrorV. +queryErrorVConst :: (View v, Num x, Semigroup x, Semigroup (v (Const x))) => v (Const x) -> ErrorV e v (Const x) +queryErrorVConst v = ErrorV (singletonV ErrorVK_View v <> singletonV ErrorVK_Error (SingleV (Const 1))) + -- | The error part of the view will never be present -liftErrorV :: View v => v g -> ErrorV e v g -liftErrorV = ErrorV . singletonV ErrorVK_View +successErrorV :: View v => v Identity -> ErrorV e v Identity +successErrorV = ErrorV . singletonV ErrorVK_View -- | The successful part of the view will never be present failureErrorV :: Applicative f => e -> ErrorV e v f @@ -141,21 +150,23 @@ buildErrorV f (ErrorV v) = case lookupV ErrorVK_View v of Nothing -> pure (ErrorV emptyV) Just v' -> f v' >>= \case Left err -> pure $ failureErrorV err - Right val -> pure $ liftErrorV val + Right val -> pure $ successErrorV val -- | Given an 'ErrorV' result, observe whether it is an error result -- or a result of the underlying view type. observeErrorV - :: EmptyView v - => ErrorV e v Identity - -> Either e (v Identity) + :: ErrorV e v Identity + -> Maybe (Either e (v Identity)) observeErrorV (ErrorV v) = case lookupV ErrorVK_Error v of - Nothing -> Right $ case lookupV ErrorVK_View v of - Nothing -> emptyV - Just v' -> v' + Nothing -> Right <$> lookupV ErrorVK_View v Just err -> case lookupSingleV err of - Nothing -> Right emptyV - Just e -> Left e + Nothing -> Right <$> lookupV ErrorVK_View v + Just e -> Just (Left e) + +-- | A 'Path' which abstracts over constructing the query and observing the result. +errorV :: (Semigroup (v (Const x)), View v, Num x, Semigroup x) + => Path (v (Const x)) (ErrorV e v (Const x)) (ErrorV e v Identity) (Either e (v Identity)) +errorV = Path { _path_to = queryErrorVConst, _path_from = observeErrorV } -- | Given an 'ErrorV' result, observe both error and result -- of the underlying view type. @@ -172,28 +183,3 @@ noErrorP = Path liftErrorV (snd . unsafeObserveErrorV) errorP :: View v => Path (SingleV err g) (ErrorV err v g) (ErrorV err v g') (SingleV err g') errorP = Path (ErrorV . singletonV ErrorVK_Error) (lookupV ErrorVK_Error . unErrorV) - --- | A morphism that only cares about error results. -unsafeProjectE - :: ( EmptyView v - ) - => QueryMorphism - () - (ErrorV () v (Const SelectedCount)) -unsafeProjectE = QueryMorphism - { _queryMorphism_mapQuery = const (liftErrorV emptyV) - , _queryMorphism_mapQueryResult = const () - } - --- | A morphism that only cares about successful results. -unsafeProjectV - :: (EmptyView v, QueryResult (v (Const SelectedCount)) ~ v Identity) - => QueryMorphism - (v (Const SelectedCount)) - (ErrorV () v (Const SelectedCount)) -unsafeProjectV = QueryMorphism - { _queryMorphism_mapQuery = liftErrorV - , _queryMorphism_mapQueryResult = \r -> case observeErrorV r of - Left _ -> emptyV - Right r' -> r' - } diff --git a/default.nix b/default.nix index ae7253f0..943ec2ca 100644 --- a/default.nix +++ b/default.nix @@ -1,4 +1,4 @@ -{ obelisk ? import ./dep/obelisk (builtins.removeAttrs args ["pkgs" "inNixShell"] // { useGHC810 = true; }) +{ obelisk ? import ./dep/obelisk (builtins.removeAttrs args ["pkgs" "inNixShell"]) , pkgs ? obelisk.nixpkgs , ... } @ args: @@ -6,7 +6,7 @@ let reflex-platform = obelisk.reflex-platform; inherit (pkgs) lib; haskellLib = pkgs.haskell.lib; - repos = pkgs.thunkSet ./dep; + repos = pkgs.mapSubdirectories pkgs.thunkSource ./dep; # Some dependency thunks needed dep = import ./dep reflex-platform.hackGet; @@ -54,7 +54,6 @@ let beam-transformers-common = repos.beam-transformers + "/common"; bytestring-aeson-orphans = repos.bytestring-aeson-orphans; - bytestring-trie = repos.bytestring-trie; monoid-map = repos.monoid-map; postgresql-simple = repos.postgresql-simple; postgresql-simple-interpolate = repos.postgresql-simple-interpolate; @@ -65,11 +64,12 @@ let gargoyle-postgresql-connect = repos.gargoyle + "/gargoyle-postgresql-connect"; gargoyle-nix-postgres-monitor = repos.gargoyle + "/gargoyle-nix-postgres-monitor"; gargoyle-postgresql-nix = repos.gargoyle + "/gargoyle-postgresql-nix"; + vessel = repos.vessel; + postgresql-lo-stream = repos.postgresql-lo-stream; #TODO: Fix # push-notifications = repos.push-notifications; - vessel = repos.vessel; }; # You can use these manually if you don’t want to use rhyolite.project. @@ -80,33 +80,15 @@ let frontend = super.frontend.override { obelisk-executable-config-lookup = self.obelisk-executable-config-lookup; }; - beam-automigrate = haskellLib.doJailbreak super.beam-automigrate; beam-postgres = haskellLib.dontCheck super.beam-postgres; beam-migrate = haskellLib.dontCheck super.beam-migrate; - bytestring-trie = haskellLib.dontCheck super.bytestring-trie; gargoyle-postgresql-nix = haskellLib.overrideCabal super.gargoyle-postgresql-nix { librarySystemDepends = [ pkgs.postgresql ]; }; postgresql-simple = haskellLib.dontCheck super.postgresql-simple; validation = haskellLib.dontCheck super.validation; + postgresql-lo-stream = haskellLib.markUnbroken super.postgresql-lo-stream; - postgresql-lo-stream = haskellLib.doJailbreak (self.callHackageDirect { - pkg = "postgresql-lo-stream"; - ver = "0.1.1.1"; - sha256 = "0ifr6i6vygckj2nikv7k7yqia495gnn27pq6viasckmmh6zx6gwi"; - } {}); - - monad-logger-extras = self.callHackageDirect { - pkg = "monad-logger-extras"; - ver = "0.1.1.1"; - sha256 = "17dr2jwg1ig1gd4hw7160vf3l5jcx5p79b2lz7k17f6v4ygx3vbz"; - } {}; - monoid-subclasses = self.callHackageDirect { - pkg = "monoid-subclasses"; - ver = "1.1"; - sha256 = "02ggjcwjdjh6cmy7zaji5mcmnq140sp33cg9rvwjgply6hkddrvb"; - } {}; - HaskellNet = self.callHackage "HaskellNet" "0.6" {}; HaskellNet-SSL = self.callHackage "HaskellNet-SSL" "0.3.4.4" {}; base-orphans = self.callHackageDirect { @@ -115,6 +97,11 @@ let sha256 = "sha256:17hplm1mgw65jbszg5z4vqk4i24ilxv8mbszr3s8lhpll5naik26"; } {}; + aeson-qq = self.callHackage "aeson-qq" "0.8.4" {}; + postgresql-syntax = haskellLib.dontCheck super.postgresql-syntax; + vessel = haskellLib.doJailbreak super.vessel; + monoid-map = haskellLib.doJailbreak super.monoid-map; + # 'locale' is broken on nix darwin which is required by postgres 'initdb' rhyolite-beam-task-worker-backend = if pkgs.stdenv.hostPlatform.isDarwin then diff --git a/dep/beam-automigrate/github.json b/dep/beam-automigrate/github.json index f5e5cccb..2abb7c51 100644 --- a/dep/beam-automigrate/github.json +++ b/dep/beam-automigrate/github.json @@ -3,6 +3,6 @@ "repo": "beam-automigrate", "branch": "cg/pre-post-hooks", "private": false, - "rev": "82cec7787e090b3d71dcf80ca93fdc4987dcc50c", - "sha256": "0w5ncdxhzxmmfffa533n5zlyzmfp4vwb5j2jvi8xaz3wc1mbbxrr" + "rev": "614c0dcaabf111774a9054e48430c290ecf7b4c7", + "sha256": "0gwhqbr7s74pdkdk59vgp1q4441hl4w8a750asjjb2nzj97b7dgz" } diff --git a/dep/bytestring-aeson-orphans/github.json b/dep/bytestring-aeson-orphans/github.json index 9833606a..315a562f 100644 --- a/dep/bytestring-aeson-orphans/github.json +++ b/dep/bytestring-aeson-orphans/github.json @@ -1,8 +1,8 @@ { "owner": "obsidiansystems", "repo": "bytestring-aeson-orphans", - "branch": "release/0.1.0.0", + "branch": "release/0.1.0.1", "private": false, - "rev": "ca7818097360480b28745e56f7580ab0505a7c95", - "sha256": "1i4pdgv72x8idyq3limjvj25innw1pl4nd1m55ag29c5kcd9ap8q" + "rev": "4d3c8d2344af18a0e486b07d574e41ad7e24a10c", + "sha256": "17dhl97qsadn37pmvw5z9zjzwy750yis3wr88zqb1g4wfb95jv4h" } diff --git a/dep/bytestring-trie/default.nix b/dep/bytestring-trie/default.nix deleted file mode 100644 index 7a047786..00000000 --- a/dep/bytestring-trie/default.nix +++ /dev/null @@ -1,7 +0,0 @@ -# DO NOT HAND-EDIT THIS FILE -import ((import {}).fetchFromGitHub ( - let json = builtins.fromJSON (builtins.readFile ./github.json); - in { inherit (json) owner repo rev sha256; - private = json.private or false; - } -)) diff --git a/dep/bytestring-trie/github.json b/dep/bytestring-trie/github.json deleted file mode 100644 index 661b6d67..00000000 --- a/dep/bytestring-trie/github.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "owner": "obsidiansystems", - "repo": "bytestring-trie", - "branch": "ghc-8.4", - "rev": "27117ef4f9f01f70904f6e8007d33785c4fe300b", - "sha256": "103fqr710pddys3bqz4d17skgqmwiwrjksn2lbnc3w7s01kal98a" -} diff --git a/dep/gargoyle/github.json b/dep/gargoyle/github.json index 12f9a879..5d4f4900 100644 --- a/dep/gargoyle/github.json +++ b/dep/gargoyle/github.json @@ -3,6 +3,6 @@ "repo": "gargoyle", "branch": "dylang/haskell.nix", "private": false, - "rev": "57f859aa683fa80f9913708899498e943373ab76", - "sha256": "0z53c15c77d6ciq9ap4gf61n622md8cjm6m60i18zim6yv151xm2" + "rev": "9d9959d92c68f415fb6cff67bbdc8df64a04817b", + "sha256": "0dwf02dhc2ys2c16wg7qj6ly135nrdih0601alyvkmpjmghrx0zk" } diff --git a/dep/obelisk/github.json b/dep/obelisk/github.json index f32e7a27..f3787041 100644 --- a/dep/obelisk/github.json +++ b/dep/obelisk/github.json @@ -1,8 +1,8 @@ { "owner": "obsidiansystems", "repo": "obelisk", - "branch": "pzp", + "branch": "haskell.nix", "private": false, - "rev": "db78e4d4c20d9d51f90e00e5d9a0d5e77c0baf82", - "sha256": "1yxwvfdlv3ii17jrd3w2yiiza26d400b35x04gmpv209xk26yz3h" + "rev": "83ea329e5ab40ca91f0f5d0c993c5e5824b702b8", + "sha256": "1v6dard7w3c88szj7dbcqlhy29j88vfbia6vqibiawhjavgy93d9" } diff --git a/dep/postgresql-lo-stream/default.nix b/dep/postgresql-lo-stream/default.nix new file mode 100644 index 00000000..2b4d4ab1 --- /dev/null +++ b/dep/postgresql-lo-stream/default.nix @@ -0,0 +1,2 @@ +# DO NOT HAND-EDIT THIS FILE +import (import ./thunk.nix) \ No newline at end of file diff --git a/dep/postgresql-lo-stream/github.json b/dep/postgresql-lo-stream/github.json new file mode 100644 index 00000000..605ce837 --- /dev/null +++ b/dep/postgresql-lo-stream/github.json @@ -0,0 +1,8 @@ +{ + "owner": "obsidiansystems", + "repo": "postgresql-lo-stream", + "branch": "develop", + "private": false, + "rev": "ddf9778546ac90be5d74a8009d114c041f99cffb", + "sha256": "1hqrjmqmn45c7ghda7g2dqkndg7s2nsf0d9vpvhrmbim9aw9gvbr" +} diff --git a/dep/postgresql-lo-stream/thunk.nix b/dep/postgresql-lo-stream/thunk.nix new file mode 100644 index 00000000..20f2d28c --- /dev/null +++ b/dep/postgresql-lo-stream/thunk.nix @@ -0,0 +1,12 @@ +# DO NOT HAND-EDIT THIS FILE +let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: + if !fetchSubmodules && !private then builtins.fetchTarball { + url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; + } else (import (builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/3aad50c30c826430b0270fcf8264c8c41b005403.tar.gz"; + sha256 = "0xwqsf08sywd23x0xvw4c4ghq0l28w2ki22h0bdn766i16z9q2gr"; +}) {}).fetchFromGitHub { + inherit owner repo rev sha256 fetchSubmodules private; + }; + json = builtins.fromJSON (builtins.readFile ./github.json); +in fetch json \ No newline at end of file diff --git a/dep/postgresql-simple-interpolate/github.json b/dep/postgresql-simple-interpolate/github.json index 87379d48..08743be4 100644 --- a/dep/postgresql-simple-interpolate/github.json +++ b/dep/postgresql-simple-interpolate/github.json @@ -1,8 +1,8 @@ { "owner": "obsidiansystems", "repo": "postgresql-simple-interpolate", - "branch": "aa/iquery", + "branch": "master", "private": false, - "rev": "497583add970ee2d5da32b9620347de3eca6d42e", - "sha256": "09x1w8ffx5bq8llpvqiaxkm6psar8ir9aaskn3762qhy7rf5v0cq" + "rev": "fc34dd4d1e179b4dd8e2fa9419435d6f9bd29160", + "sha256": "0631fhycn8silm25rbajqc4fmhhh6n6kcnl6b0nddzx0bdmf1v0b" } diff --git a/dep/postgresql-simple-interpolate/thunk.nix b/dep/postgresql-simple-interpolate/thunk.nix index bbf2dc18..20f2d28c 100644 --- a/dep/postgresql-simple-interpolate/thunk.nix +++ b/dep/postgresql-simple-interpolate/thunk.nix @@ -2,7 +2,10 @@ let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: if !fetchSubmodules && !private then builtins.fetchTarball { url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; - } else (import {}).fetchFromGitHub { + } else (import (builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/3aad50c30c826430b0270fcf8264c8c41b005403.tar.gz"; + sha256 = "0xwqsf08sywd23x0xvw4c4ghq0l28w2ki22h0bdn766i16z9q2gr"; +}) {}).fetchFromGitHub { inherit owner repo rev sha256 fetchSubmodules private; }; json = builtins.fromJSON (builtins.readFile ./github.json); diff --git a/dep/vessel/github.json b/dep/vessel/github.json index cb7c066b..1c384736 100644 --- a/dep/vessel/github.json +++ b/dep/vessel/github.json @@ -3,6 +3,6 @@ "repo": "vessel", "branch": "pzp", "private": false, - "rev": "ac0969697ae12df6c66ebae9e7afd236cddaf344", - "sha256": "1g2nwpxs8n0x0l4qkr5n29ij1nvsz5q13kypmfrrplrpnviqrv1r" + "rev": "15334738886e8f2504363433e66f5c2ce0d8b0ab", + "sha256": "1mk5m47ssdvzqs4blbc5ykcm2jy25pa78hhp26hp3xbcqaa7wh0r" } diff --git a/widgets/rhyolite-widgets.cabal b/widgets/rhyolite-widgets.cabal index 5c9105f7..2ede1a2b 100644 --- a/widgets/rhyolite-widgets.cabal +++ b/widgets/rhyolite-widgets.cabal @@ -16,8 +16,6 @@ category: UI library exposed-modules: - Reflex.Dom.Modal.Base - Reflex.Dom.Modal.Class Reflex.Dom.Widget.ExtensibleList Reflex.Dom.Widget.Form diff --git a/widgets/src/Reflex/Dom/Modal/Base.hs b/widgets/src/Reflex/Dom/Modal/Base.hs deleted file mode 100644 index 51dee149..00000000 --- a/widgets/src/Reflex/Dom/Modal/Base.hs +++ /dev/null @@ -1,233 +0,0 @@ -{-| -Description: - Modal widgets - -The important definition here is 'ModalT', the related class is -in "Reflex.Dom.Modal.Class". --} - -{-# Language CPP #-} -{-# Language DataKinds #-} -{-# Language FlexibleContexts #-} -{-# Language FlexibleInstances #-} -{-# Language GeneralizedNewtypeDeriving #-} -{-# Language LambdaCase #-} -{-# Language MultiParamTypeClasses #-} -{-# Language OverloadedStrings #-} -{-# Language RankNTypes #-} -{-# Language RecursiveDo #-} -{-# Language ScopedTypeVariables #-} -{-# Language StandaloneDeriving #-} -{-# Language TypeFamilies #-} -{-# Language UndecidableInstances #-} - -module Reflex.Dom.Modal.Base where - -import Control.Applicative (liftA2) -import Control.Lens (Rewrapped, Wrapped (Unwrapped, _Wrapped'), iso) -import Control.Monad.Fix (MonadFix) -import Control.Monad.IO.Class (MonadIO) -import Control.Monad.Primitive (PrimMonad (PrimState, primitive)) -import Control.Monad.Reader (MonadReader) -import Control.Monad.Ref (MonadAtomicRef, MonadRef) -import Control.Monad.Trans (MonadTrans (lift)) -import Data.Coerce (coerce) -import Data.Either.Combinators (rightToMaybe) -import Data.Map (Map) -import qualified Data.Map as Map -import Data.Maybe (fromMaybe) -import Data.Semigroup (First(..)) -import Data.Text (Text) -import qualified GHCJS.DOM as DOM -import qualified GHCJS.DOM.EventM as EventM -import qualified GHCJS.DOM.GlobalEventHandlers as Events -import Language.Javascript.JSaddle (MonadJSM) -import Obelisk.Configs (HasConfigs) -import Obelisk.Route.Frontend -import Reflex.Dom.Core -import Reflex.Host.Class (MonadReflexCreateTrigger) - -import Reflex.Dom.Modal.Class (HasModal (ModalM, tellModal)) - -instance (Reflex t, Monad m) => HasModal t (ModalT t modalM m) where - type ModalM (ModalT t modalM m) = modalM - tellModal = ModalT . tellEvent . fmap First - --- | Modal monad transformer -newtype ModalT t modalM m a - = ModalT { unModalT :: EventWriterT t (First (Event t () -> modalM (Event t ()))) m a } - deriving - ( Functor, Applicative, Monad - , MonadFix, MonadIO, MonadRef, MonadAtomicRef, MonadReader r - , DomBuilder t, NotReady t, MonadHold t, MonadSample t - , PerformEvent t, TriggerEvent t, PostBuild t - , MonadReflexCreateTrigger t, MonadQuery t q, Requester t - ) - -instance PrimMonad m => PrimMonad (ModalT t modalM m) where - type PrimState (ModalT t modalM m) = PrimState m - primitive = lift . primitive - -instance Wrapped (ModalT t modalM m a) where - type Unwrapped (ModalT t modalM m a) = EventWriterT t (First (Event t () -> modalM (Event t ()))) m a - _Wrapped' = iso coerce coerce -instance ModalT t modalM m a ~ x => Rewrapped (ModalT t modalM m a) x - -instance HasDocument m => HasDocument (ModalT t modalM m) -#if !defined(ghcjs_HOST_OS) -instance MonadJSM m => MonadJSM (ModalT t modalM m) -#endif - -instance (Monad m, Routed t r m) => Routed t r (ModalT t modalM m) where - askRoute = lift askRoute - -instance (Monad m, RouteToUrl r m) => RouteToUrl r (ModalT t modalM m) where - askRouteToUrl = lift askRouteToUrl - -instance (Reflex t, Monad m, SetRoute t r m) => SetRoute t r (ModalT t modalM m) where - modifyRoute = lift . modifyRoute - -instance EventWriter t w m => EventWriter t w (ModalT t modalM m) where - tellEvent = lift . tellEvent - -instance MonadTrans (ModalT t modalM) where - lift = ModalT . lift - -instance (Adjustable t m, MonadHold t m, MonadFix m) => Adjustable t (ModalT t modalM m) where - runWithReplace a0 a' = ModalT $ runWithReplace (unModalT a0) (fmapCheap unModalT a') - traverseDMapWithKeyWithAdjust f dm0 dm' = ModalT $ traverseDMapWithKeyWithAdjust (coerce f) dm0 dm' - traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = ModalT $ traverseDMapWithKeyWithAdjustWithMove (coerce f) dm0 dm' - traverseIntMapWithKeyWithAdjust f im0 im' = ModalT $ traverseIntMapWithKeyWithAdjust (coerce f) im0 im' - -deriving instance DomRenderHook t m => DomRenderHook t (ModalT t modalM m) - -instance (Prerender t m, Monad m, Reflex t) => Prerender t (ModalT t modalM m) where - type Client (ModalT t modalM m) = ModalT t modalM (Client m) - prerender back front = do - (a, ev) <- fmap splitDynPure $ lift $ prerender - (runEventWriterT $ unModalT back) - (runEventWriterT $ unModalT front) - ModalT $ tellEvent $ switchDyn ev - pure a - -instance HasConfigs m => HasConfigs (ModalT t modalM m) - --- | Like 'withModals' but with the full convenience of 'ModalT', allowing 'tellModal' to open a modal anywhere. --- --- NB: This must wrap all other DOM building. This is because DOM for the modal --- must occur *after* all other DOM in order for the modal to appear on top of it. -runModalT - :: forall m t a. - ( MonadFix m - , DomBuilder t m, MonadHold t m, PostBuild t m, Prerender t m - ) - => ModalBackdropConfig -> ModalT t m m a -> m a -runModalT backdropCfg f = do - rec - ((a, open), _) <- withModals backdropCfg (getFirst <$> open) $ runEventWriterT (unModalT f) - pure a - --- | Change the underlying monad of `ModalT` and the monad the modals will be run in. --- --- For cases where those two monads differ, checkout `mapModalT` and `mapModalM`. -mapModalTM :: (Reflex t, MonadHold t m) => (forall x. m x -> n x) -> ModalT t m m a -> ModalT t n n a -mapModalTM f = mapModalT f . mapModalM f - --- | Change the underlying monad of `ModalT`. -mapModalT :: (Reflex t, MonadHold t m) => (forall x. m x -> n x) -> ModalT t modalM m a -> ModalT t modalM n a -mapModalT f = ModalT . mapEventWriterT f . unModalT - --- | Change the monad the modals will be run in. -mapModalM :: (Reflex t, MonadHold t m) => (forall x. modalM x -> modalN x) -> ModalT t modalM m a -> ModalT t modalN m a -mapModalM f = ModalT . withEventWriterT ((fmap . fmap) f) . unModalT - --- | You can adjust the attributes passed to the backdrop with this config. --- --- You can adjust the background and the size, for example. You cannot change --- the CSS `display` property, as it is controlled by this library. Also note --- that the dialog is not a child widget of the backdrop, but rendered within --- a separate `untouchable` top level div. For positioning of your dialog, --- you should not rely on backdrop, nor on this hidden div, instead we --- recommend fixed positioning for the dialog, as described in `tellModal`. -newtype ModalBackdropConfig = ModalBackdropConfig - { _modalBackdropConfig_attrs :: Map Text Text - } deriving (Monoid, Semigroup) - --- | Set up DOM to support modals. --- --- NB: This must wrap all other DOM building. This is because DOM for the modal --- must occur *after* all other DOM in order for the modal to appear on top of it. -withModals - :: forall m a b t. - ( MonadFix m - , DomBuilder t m, MonadHold t m, PostBuild t m, Prerender t m - ) - => ModalBackdropConfig - -> Event t (Event t () -> m (Event t a)) - -- ^ Event to trigger a modal to open. - -- The event carries a function that takes close events and builds a modal window - -- which returns a close event. - -> m b -- ^ Page body - -> m (b, Event t a) -- ^ Result of page body and an event firing whenever a modal closes -withModals backdropCfg open body = liftA2 (,) body (modalDom backdropCfg open) - --- | Builds modal-related DOM. Avoid using this and use 'withModals' instead. --- --- NB: This must run after all other DOM building. This is because DOM for the modal --- must occur *after* all other DOM in order for the modal to appear on top of it. -modalDom - :: forall a m t. (DomBuilder t m, MonadFix m, MonadHold t m, PostBuild t m, Prerender t m) - => ModalBackdropConfig - -> Event t (Event t () -> m (Event t a)) - -- ^ Event to trigger a modal to open. - -- The event carries a function that takes close events and builds a modal window - -- which returns a close event. - -> m (Event t a) -- ^ An event firing whenever the modal closes -modalDom backdropCfg open = do - escPressed :: Event t () <- fmap switchDyn $ prerender (pure never) $ do - document <- DOM.currentDocumentUnchecked - wrapDomEventMaybe document (`EventM.on` Events.keyDown) $ do - key <- getKeyEvent - pure $ if keyCodeLookup (fromIntegral key) == Escape then Just () else Nothing - rec - isVisible <- holdDyn False $ leftmost [True <$ open, False <$ close] - (backdropEl, _) <- elDynAttr' "div" - (ffor isVisible $ \isVis -> - ("style" =: (isVisibleStyle isVis <> ";" <> existingBackdropStyle)) <> _modalBackdropConfig_attrs backdropCfg - ) - blank - close <- elDynAttr "div" (ffor isVisible $ \isVis -> "style" =: isVisibleStyle isVis) $ - fmap switchDyn $ widgetHold (pure never) $ leftmost - [ ($ leftmost [escPressed, domEvent Click backdropEl]) <$> open - , pure never <$ close - ] - pure close - where - existingBackdropStyle = fromMaybe "" $ Map.lookup "style" $ _modalBackdropConfig_attrs backdropCfg - isVisibleStyle isVis = "display:" <> (if isVis then "block" else "none") - --- | Widget used as a modal div for widgets that want to take some action when --- clicked anywhere but itself, such as dropdown widgets or the like. The --- first argument is a CSS class name, the suggested CSS class styling for use --- of this widget is as follows: --- --- > position: fixed; --- > top: 0; --- > bottom: 0; --- > right: 0; --- > left: 0; --- > z-index: 100; --- -{-# Deprecated withBackdrop "Use ModalT instead" #-} -withBackdrop :: forall m t a. (DomBuilder t m, MonadFix m, MonadHold t m) => Text -> Event t (m (Event t a)) -> m (Event t a) -withBackdrop cls openBackdropWithChild = mdo - sth <- widgetHold (return never) $ ffor (leftmost [close, open]) $ \case - Nothing -> return never - Just child -> do - (backgroundEl, _) <- elClass' "div" cls blank - childResult <- child - let backgroundEvent = domEvent Click backgroundEl - return $ leftmost [Left <$> backgroundEvent, Right <$> childResult] - let close :: Event t (Maybe (m (Event t a))) = Nothing <$ (switch . current $ sth) - open :: Event t (Maybe (m (Event t a))) = Just <$> openBackdropWithChild - return $ fmapMaybe rightToMaybe $ switch . current $ sth diff --git a/widgets/src/Reflex/Dom/Modal/Class.hs b/widgets/src/Reflex/Dom/Modal/Class.hs deleted file mode 100644 index 702ed07c..00000000 --- a/widgets/src/Reflex/Dom/Modal/Class.hs +++ /dev/null @@ -1,60 +0,0 @@ -{-| Description: Class for modal creators -A class for widget that can have modals. A concrete implementation is in -"Reflex.Dom.Modal.Base". --} - -{-# Language DefaultSignatures #-} -{-# Language FlexibleInstances #-} -{-# Language MultiParamTypeClasses #-} -{-# Language TypeFamilies #-} - -module Reflex.Dom.Modal.Class where - -import Control.Monad.Reader (MonadReader (ask), ReaderT (..)) -import Control.Monad.Trans (MonadTrans (lift)) -import Obelisk.Route.Frontend (RoutedT, askRoute, runRoutedT) -import Reflex (Event, EventWriterT, Reflex) - --- | The class of monads supporting a 'tellModal' operation which will open a modal --- that stays on top of all other content. -class HasModal t m where - -- If 'm' is the monad that supports 'tellModal' then 'ModalM m' is the monad that the modal itself is in, - -- which, notably, probably doesn't support 'tellModal'. - type ModalM m :: * -> * - - -- | Opens a modal when the given event fires. The event carries a function which: - -- * takes a "close" event triggered when the user signifies that they want to close the modal, - -- * builds content in 'ModalM m', - -- * returns a "close" event which will be used to actually close the modal. - -- - -- For example, a modal may choose not to be closable by simply ignoring its input and returning 'never'. - -- - -- Note on positioning: We control the containing div of your dialog for - -- handling of the CSS display property. We recommend that you position - -- your dialog `fixed` with some Clay like the following: - -- - -- @ - -- position fixed - -- top (pct 50) - -- left (pct 50) - -- transform (translate (pct $ negate 50) (pct $ negate 50)) - -- @ - tellModal :: Event t (Event t () -> ModalM m (Event t ())) -> m () - - default tellModal :: (MonadTrans f, m ~ f m', HasModal t m', Monad m', ModalM (f m') ~ ModalM m') => Event t (Event t () -> ModalM m (Event t ())) -> m () - tellModal = lift . tellModal - -instance (Monad m, Reflex t, HasModal t m) => HasModal t (EventWriterT t w m) where - type ModalM (EventWriterT t w m) = ModalM m - -instance (Monad m, Reflex t, HasModal t m) => HasModal t (ReaderT r m) where - type ModalM (ReaderT r m) = ReaderT r (ModalM m) -- Transform the modal's monad - tellModal ev = do - r <- ask - lift $ tellModal $ (fmap . fmap) (`runReaderT` r) ev - -instance (Monad m, Reflex t, HasModal t m) => HasModal t (RoutedT t r m) where - type ModalM (RoutedT t r m) = RoutedT t r (ModalM m) -- Transform the modal's monad - tellModal ev = do - r <- askRoute - lift $ tellModal $ (fmap . fmap) (`runRoutedT` r) ev