Chatroom Example¶
Using dazzler session system and redis, create a pubsub based chatroom.
Setup¶
Install supported aioredis
client with pip install dazzler[redis]
.
Set the REDIS_URL
environ variable if necessary.
(Default: redis://localhost:6379
)
Code¶
from datetime import datetime
from dazzler import Dazzler
from dazzler.components import core, calendar, extra
from dazzler.system import Page, BindingContext
app = Dazzler(__name__)
app.config.session.backend = 'Redis'
page = Page(__name__, core.Container([
core.Container([], identity='app', class_name='app')
]), url='/')
app.add_page(page)
chat_layout = core.Container([
core.ListBox(identity='messages'),
core.Container(
[
core.TextArea(identity='msg-area'),
core.Button(
'Send',
identity='send-btn',
preset='primary',
disabled=True,
rounded=True,
class_name='send-btn'
)
],
class_name='message-form row'
)
])
def redis():
# Redis is currently integrated with the session middleware
# which is always the first middleware in the list for now.
# Will be changed to a proper middleware & added to aiohttp app context
return app.middlewares[0]._backend.redis
def create_message(msg):
return extra.PopUp(
[
core.Html('span', msg['name'], class_name='msg-user'),
core.Html('span', msg['body'], class_name='msg-body'),
],
content=calendar.Timestamp(
msg['ts'], format='[Sent at] DD/MM/YYYY HH[:]mm'
),
class_name='msg'
)
async def handle_messages(ctx: BindingContext):
channel, = await redis().subscribe(
f'chatter-{ctx.session.session_id}'
)
while await channel.wait_message():
message = await channel.get_json()
await ctx.set_aspect('messages', prepend=create_message(message))
@page.bind('class_name@app')
async def page_load(ctx: BindingContext):
# Define if already supplied a username
name = await ctx.session.get('name')
if name:
await ctx.set_aspect(
'app', children=core.Container([
core.Html('h2', f'Welcome back {name}'),
chat_layout,
])
)
# Run the task in the background.
# Task created with the context will be automatically cancelled
# when the websocket connection dies.
ctx.create_task(handle_messages(ctx))
else:
await ctx.set_aspect(
'app', children=core.Container([
core.Html('h2', 'Please enter a name to join the chat'),
core.Input(identity='name-input', placeholder='Name'),
core.Button('Join', identity='join-btn')
])
)
@page.bind('value@msg-area', 'disabled@send-btn')
async def text_input(ctx: BindingContext):
text_len = len(ctx.trigger.value)
if ctx.states['send-btn']['disabled'] and text_len > 1:
await ctx.set_aspect('send-btn', disabled=False)
elif text_len < 1:
await ctx.set_aspect('send-btn', disabled=True)
@page.bind('clicks@join-btn', 'value@name-input')
async def join_room(ctx: BindingContext):
name = ctx.states['name-input']['value']
await ctx.session.set('name', name)
await ctx.set_aspect('app', children=chat_layout)
ctx.create_task(
handle_messages(ctx)
)
@page.bind('clicks@send-btn', 'value@msg-area')
async def send_messages(ctx: BindingContext):
channels = await redis().pubsub_channels(
pattern='chatter-*'
)
name = await ctx.session.get('name')
for channel in channels:
await redis().publish_json(
channel, {
'body': ctx.states['msg-area']['value'],
'name': name,
'ts': datetime.utcnow().isoformat(),
}
)
await ctx.set_aspect('msg-area', value='')
if __name__ == '__main__':
app.start('--debug --reload --port 8189')
Some styles to go along:
.app {
display: flex;
justify-content: center;
align-items: center;
}
.msg {
padding: 1rem;
background-color: #0e84b5;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 5px;
margin: 1rem 0;
color: white;
width: 100%;
}
.msg:nth-child(2n) {
background-color: #40a070;
}
.msg-user {
font-weight: bold;
}
.msg-user::before {
content: '@'
}
.msg-user::after {
content: ' '
}