@@ -4,25 +4,25 @@ module Example.Chat where
44
55import App.Route
66import Control.Monad (forM_ , forever )
7- import Data.Text (Text , unpack )
7+ import Data.Text (Text )
88import Effectful
9- import Effectful.Concurrent ( threadDelay )
9+ import Effectful.Concurrent
1010import Effectful.Concurrent.STM
1111import Effectful.Reader.Dynamic
12+ import Effectful.State.Dynamic (get , modify )
1213import Example.Colors
1314import Example.Style qualified as Style
1415import Example.Style.Cyber (embed )
1516import Example.Style.Cyber as Cyber (btn , font )
1617import Example.View.Layout (layout )
1718import Web.Atomic.CSS
1819import Web.Hyperbole
20+ import Web.Hyperbole.Data.Encoded (Encoded (.. ), FromEncoded (.. ), ToEncoded (.. ))
1921
20- -- import Example.View.Layout
21-
22- page :: (Hyperbole :> es , Concurrent :> es , Reader (TVar [(Username , Text )]) :> es ) => Page es '[Content , Chats , Message ]
22+ page :: (Hyperbole :> es , Concurrent :> es , Reader Room :> es ) => Page es '[Content , Chats , NewMessage ]
2323page = do
2424 pure $ layout (Examples Chat ) $ do
25- el " Demonstrates server pushes"
25+ el " Demonstrates server pushes and concurrency. Open in two tabs with different usernames to test. "
2626 col ~ embed . Cyber. font $ do
2727 hyper Content $ contentView Nothing
2828
@@ -35,7 +35,7 @@ instance HyperView Content es where
3535 data Action Content = Login | Logout
3636 deriving (Generic , ViewAction )
3737
38- type Require Content = '[Chats , Message ]
38+ type Require Content = '[Chats , NewMessage ]
3939
4040 update Login = do
4141 LoginForm u <- formData
@@ -63,78 +63,119 @@ contentView mu = do
6363 el ~ bold $ text u
6464 space
6565 button Logout ~ btn $ " logout"
66- hyper Chats $ chatsLoad u
67- hyper Message $ messageView u
66+ hyperState Chats mempty $ chatsLoad u
67+ hyperState NewMessage u messageView
68+
69+ -- Chat Room -------------------------------------
70+
71+ data Message = Message
72+ { sender :: Username
73+ , body :: Text
74+ }
75+ deriving (Generic , ToParam , FromParam )
76+
77+ newtype Room = Room (TChan Message )
78+ newtype Subscription = Subscription (TChan Message )
79+
80+ initChatRoom :: (Concurrent :> es ) => Eff es Room
81+ initChatRoom = Room <$> newBroadcastTChanIO
6882
69- -- Chats State Effect -------------------------------------
83+ subscribeChatRoom :: (Concurrent :> es ) => Room -> Eff es Subscription
84+ subscribeChatRoom (Room chan) = fmap Subscription <$> atomically $ dupTChan chan
7085
71- -- it's just not a very good use-case for it...
86+ waitMessage :: (Concurrent :> es ) => Subscription -> Eff es Message
87+ waitMessage (Subscription chan) = atomically $ readTChan chan
7288
73- initChats :: (Concurrent :> es ) => Eff es (TVar [( Username , Text )] )
74- initChats = newTVarIO []
89+ sendMessage :: (Concurrent :> es ) => Room -> Message -> Eff es ()
90+ sendMessage ( Room chan) msg = atomically $ writeTChan chan msg
7591
76- getChats :: (Concurrent :> es , Reader (TVar [(Username , Text )]) :> es ) => Eff es [(Username , Text )]
77- getChats = do
78- var <- ask
79- readTVarIO var
92+ -- Encoding for message history since starting
93+ newtype AllMessages = AllMessages [Message ]
94+ deriving newtype (Semigroup , Monoid )
8095
81- --- Show Chat Updates -------------------------------
96+ instance ToEncoded AllMessages where
97+ toEncoded (AllMessages ms) = Encoded " " (fmap toParam ms)
98+ instance FromEncoded AllMessages where
99+ parseEncoded (Encoded _ ps) =
100+ AllMessages <$> mapM parseParam ps
101+
102+ --- Chat Updates ---------------------------------------------
82103
83104data Chats = Chats
84- deriving (ViewId , Generic )
105+ deriving (Generic )
106+ instance ViewId Chats where
107+ type ViewState Chats = AllMessages
85108
86- instance (Concurrent :> es , Reader ( TVar [( Username , Text )]) :> es , IOE :> es ) => HyperView Chats es where
109+ instance (Concurrent :> es , Reader Room :> es , IOE :> es ) => HyperView Chats es where
87110 data Action Chats = Stream Username
88111 deriving (Generic , ViewAction )
89112
113+ -- we need to build up our own list of messages...
90114 update (Stream u) = do
91- forever streamChats
115+ room <- ask
116+ sub <- subscribeChatRoom room
117+
118+ sendMessage room $ Message u " I have arrived!"
119+
120+ forever (streamChats sub)
92121 where
93- streamChats = do
122+ streamChats room = do
94123 -- this will get cancelled when the user leaves the page, on calling pushUpdate
95- chats <- getChats
96- liftIO $ putStrLn $ " CHATS => " <> unpack u <> " " <> show (length chats)
97- pushUpdate $ chatsView u chats
98- threadDelay 1000000
124+ msg <- waitMessage room
125+ modify $ addMessage msg
126+ pushUpdate $ chatsView u
127+
128+ allMessages :: View Chats AllMessages
129+ allMessages = do
130+ AllMessages ms <- viewState
131+ pure $ AllMessages $ reverse ms
99132
133+ addMessage :: Message -> AllMessages -> AllMessages
134+ addMessage msg (AllMessages ms) = AllMessages $ msg : ms
135+
136+ -- TODO: initial message or view that shows better, since we aren't loading history any more
100137chatsLoad :: Username -> View Chats ()
101138chatsLoad user = el @ onLoad (Stream user) 100 $ " ..."
102139
103- chatsView :: Username -> [(Username , Text )] -> View Chats ()
104- chatsView _user chats = do
140+ chatsView :: Username -> View Chats ()
141+ chatsView _user = do
142+ AllMessages chats <- allMessages
105143 col ~ gap 5 . pad 5 . minHeight 400 . border 1 . bg GrayLight $ do
106- forM_ chats $ \ (u, msg) -> do
144+ forM_ chats $ \ chat -> do
107145 el $ do
108- text u
146+ text chat . sender
109147 text " : "
110- text msg
148+ text chat . body
111149
112150--- New Chat Messages ------------------------------
113151
114- data Message = Message
115- deriving (ViewId , Generic )
152+ data NewMessage = NewMessage
153+ deriving (Generic )
154+ instance ViewId NewMessage where
155+ type ViewState NewMessage = Username
116156
117- instance (Concurrent :> es , Reader ( TVar [( Username , Text )]) :> es , IOE :> es ) => HyperView Message es where
118- data Action Message = NewMessage Username
157+ instance (Concurrent :> es , Reader Room :> es , IOE :> es ) => HyperView NewMessage es where
158+ data Action NewMessage = SendMessage
119159 deriving (Generic , ViewAction )
120160
121- type Require Message = '[Chats ]
161+ -- type Require NewMessage = '[Chats]
122162
123- update (NewMessage u) = do
163+ update SendMessage = do
164+ user <- get @ Username
165+ room <- ask
124166 MessageForm msg <- formData
125- cvar <- ask
126- atomically $ modifyTVar cvar $ \ cs -> (u, msg) : cs
127-
128- pure $ messageView u
167+ sendMessage room $ Message user msg
168+ -- NOTE: this doesn't show an update at all, but we are subscribed to the channel and will get a push like everyone else
169+ pure messageView
129170
130171data MessageForm = MessageForm
131172 { message :: Text
132173 }
133174 deriving (Generic , FromForm )
134175
135- messageView :: Username -> View Message ()
136- messageView u = do
137- form ( NewMessage u) ~ flexRow . gap 10 $ do
176+ messageView :: View NewMessage ()
177+ messageView = do
178+ form SendMessage ~ flexRow . gap 10 $ do
138179 field " message" $ do
139180 input TextInput @ placeholder " type your message here" . value " " . autofocus ~ Style. input . grow
140181 submit " Send" ~ btn
0 commit comments