Skip to content

Commit 9e1e6b6

Browse files
committed
new chat with dupTChan
fixes
1 parent 6512e43 commit 9e1e6b6

3 files changed

Lines changed: 91 additions & 51 deletions

File tree

demo/App.hs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,25 +105,25 @@ run = do
105105
putStrLn $ "Starting Examples on http://localhost:" <> show port
106106

107107
users <- Users.initUsers
108-
(count, chats, config) <- runEff $ runEnvironment $ do
108+
(count, room, config) <- runEff $ runEnvironment $ do
109109
c <- runConcurrent Effects.initCounter
110-
ct <- runConcurrent Chat.initChats
110+
room <- runConcurrent Chat.initChatRoom
111111
a <- getAppConfigEnv
112-
pure (c, ct, a)
112+
pure (c, room, a)
113113

114114
cache <- clientCache
115115

116116
Warp.run port $
117117
Static.staticPolicyWithOptions cache (addBase "client/dist") $
118118
Static.staticPolicy (addBase "demo/static") $ do
119-
devReload config $ exampleApp config users count chats
119+
devReload config $ exampleApp config users count room
120120
where
121121
devReload :: AppConfig -> Application -> Application
122122
devReload config
123123
| config.devMode = Wai.modifyResponse $ Wai.mapResponseHeaders $ \hs -> ("Connection", "Close") : hs
124124
| otherwise = id
125125

126-
exampleApp :: AppConfig -> UserStore -> TVar Int -> TVar [(Text, Text)] -> Application
126+
exampleApp :: AppConfig -> UserStore -> TVar Int -> Chat.Room -> Application
127127
exampleApp config users count chats = do
128128
liveAppWith
129129
(ServerOptions (document documentHead) serverError)

demo/App/Page/Examples.hs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ page = do
1313
section' "UI Demos" $ do
1414
col ~ gap 10 $ do
1515
card (Examples Tags) "Add and remove \"tags\" from via an input"
16-
card (Examples Chat) "Demonstrates server pushes"
16+
card (Examples Chat) "Demonstrates server pushes and concurrency. Open in multiple tabs"
1717
card (Examples Scrollbars) "Layouts with internal scrollbars"
18-
-- card (Contacts ContactsAll) "Random "
1918

2019
section' "Data Lists" $ do
2120
col ~ gap 10 $ do

demo/Example/Chat.hs

Lines changed: 85 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,25 @@ module Example.Chat where
44

55
import App.Route
66
import Control.Monad (forM_, forever)
7-
import Data.Text (Text, unpack)
7+
import Data.Text (Text)
88
import Effectful
9-
import Effectful.Concurrent (threadDelay)
9+
import Effectful.Concurrent
1010
import Effectful.Concurrent.STM
1111
import Effectful.Reader.Dynamic
12+
import Effectful.State.Dynamic (get, modify)
1213
import Example.Colors
1314
import Example.Style qualified as Style
1415
import Example.Style.Cyber (embed)
1516
import Example.Style.Cyber as Cyber (btn, font)
1617
import Example.View.Layout (layout)
1718
import Web.Atomic.CSS
1819
import 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]
2323
page = 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

83104
data 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
100137
chatsLoad :: Username -> View Chats ()
101138
chatsLoad 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

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

Comments
 (0)