Bladeren bron

Merge pull request #826 from markbaumgarten/main

Adding quasar chat message and improving chat example
Rodja Trappe 2 jaren geleden
bovenliggende
commit
bbe6529720
5 gewijzigde bestanden met toevoegingen van 110 en 22 verwijderingen
  1. 15 14
      examples/chat_app/main.py
  2. 20 0
      nicegui/elements/chat_message.js
  3. 40 0
      nicegui/elements/chat_message.py
  4. 34 8
      nicegui/functions/refreshable.py
  5. 1 0
      nicegui/ui.py

+ 15 - 14
examples/chat_app/main.py

@@ -1,27 +1,29 @@
 #!/usr/bin/env python3
-import asyncio
+from datetime import datetime
 from typing import List, Tuple
 
 from nicegui import Client, ui
 
 messages: List[Tuple[str, str]] = []
-contents: List[ui.column] = []
 
 
-async def update(content: ui.column) -> None:
-    content.clear()
-    with content:  # use the context of each client to update their ui
-        for name, text in messages:
-            ui.markdown(f'**{name or "someone"}:** {text}').classes('text-lg m-2')
-        await ui.run_javascript(f'window.scrollTo(0, document.body.scrollHeight)', respond=False)
+@ui.refreshable
+async def chat_messages(name_input: ui.input) -> None:
+    for name, text in messages:
+        ui.chat_message(text=text,
+                        name=name,
+                        stamp=datetime.utcnow().strftime('%X'),
+                        avatar=f'https://robohash.org/{name or "anonymous"}?bgset=bg2',
+                        sent=name == name_input.value)
+    await ui.run_javascript('window.scrollTo(0, document.body.scrollHeight)', respond=False)
 
 
 @ui.page('/')
 async def main(client: Client):
-    async def send() -> None:
+    def send() -> None:
         messages.append((name.value, text.value))
         text.value = ''
-        await asyncio.gather(*[update(content) for content in contents])  # run updates concurrently
+        chat_messages.refresh()
 
     anchor_style = r'a:link, a:visited {color: inherit !important; text-decoration: none; font-weight: 500}'
     ui.add_head_html(f'<style>{anchor_style}</style>')
@@ -33,9 +35,8 @@ async def main(client: Client):
         ui.markdown('simple chat app built with [NiceGUI](https://nicegui.io)') \
             .classes('text-xs self-end mr-8 m-[-1em] text-primary')
 
-    await client.connected()  # update(...) uses run_javascript which is only possible after connecting
-    contents.append(ui.column().classes('w-full max-w-2xl mx-auto'))  # save ui context for updates
-    await update(contents[-1])  # ensure all messages are shown after connecting
-
+    await client.connected()  # chat_messages(...) uses run_javascript which is only possible after connecting
+    with ui.column().classes('w-full max-w-2xl mx-auto items-stretch'):
+        await chat_messages(name_input=name)
 
 ui.run()

+ 20 - 0
nicegui/elements/chat_message.js

@@ -0,0 +1,20 @@
+export default {
+  template: `
+    <q-chat-message
+      :text="[text]"
+      :name="name"
+      :label="label"
+      :stamp="stamp"
+      :avatar="avatar"
+      :sent=sent
+    />
+  `,
+  props: {
+    text: String,
+    name: String,
+    label: String,
+    stamp: String,
+    avatar: String,
+    sent: Boolean,
+  },
+};

+ 40 - 0
nicegui/elements/chat_message.py

@@ -0,0 +1,40 @@
+from typing import Optional
+
+from ..dependencies import register_component
+from ..element import Element
+
+register_component('chat_message', __file__, 'chat_message.js')
+
+
+class ChatMessage(Element):
+
+    def __init__(self,
+                 text: str, *,
+                 name: Optional[str] = None,
+                 label: Optional[str] = None,
+                 stamp: Optional[str] = None,
+                 avatar: Optional[str] = None,
+                 sent: bool = False,
+                 ) -> None:
+        """Chat Message
+
+        Based on Quasar's `Chat Message <https://quasar.dev/vue-components/chat/`_ component.
+
+        :param text: the message body
+        :param name: the name of the message author
+        :param label: renders a label header/section only
+        :param stamp: timestamp of the message
+        :param avatar: URL to an avatar
+        :param sent: render as a sent message (so from current user) (default: False)
+        """
+        super().__init__('chat_message')
+        self._props['text'] = text
+        if name is not None:
+            self._props['name'] = name
+        if label is not None:
+            self._props['label'] = label
+        if stamp is not None:
+            self._props['stamp'] = stamp
+        if avatar is not None:
+            self._props['avatar'] = avatar
+        self._props['sent'] = sent

+ 34 - 8
nicegui/functions/refreshable.py

@@ -1,9 +1,11 @@
-from typing import Callable, List
+from typing import Any, Callable, Dict, List, Tuple
 
 from typing_extensions import Self
 
+from .. import background_tasks, globals
 from ..dependencies import register_component
 from ..element import Element
+from ..helpers import is_coroutine
 
 register_component('refreshable', __file__, 'refreshable.js')
 
@@ -18,19 +20,43 @@ class refreshable:
         """
         self.func = func
         self.instance = None
-        self.containers: List[Element] = []
+        self.containers: List[Tuple[Element, List[Any], Dict[str, Any]]] = []
 
     def __get__(self, instance, _) -> Self:
         self.instance = instance
         return self
 
-    def __call__(self) -> None:
+    def __call__(self, *args, **kwargs) -> None:
+        self.prune()
         with Element('refreshable') as container:
-            self.func() if self.instance is None else self.func(self.instance)
-        self.containers.append(container)
+            self.containers.append((container, args, kwargs))
+            return self.func(*args, **kwargs) if self.instance is None else self.func(self.instance, *args, **kwargs)
 
     def refresh(self) -> None:
-        for container in self.containers:
+        self.prune()
+        for container, args, kwargs in self.containers:
             container.clear()
-            with container:
-                self.func() if self.instance is None else self.func(self.instance)
+            if is_coroutine(self.func):
+                async def wait_for_result(container: Element, args, kwargs):
+                    with container:
+                        if self.instance is None:
+                            await self.func(*args, **kwargs)
+                        else:
+                            await self.func(self.instance, *args, **kwargs)
+                if globals.loop and globals.loop.is_running():
+                    background_tasks.create(wait_for_result(container=container, args=args, kwargs=kwargs))
+                else:
+                    globals.app.on_startup(wait_for_result(container=container, args=args, kwargs=kwargs))
+            else:
+                with container:
+                    if self.instance is None:
+                        self.func(*args, **kwargs)
+                    else:
+                        self.func(self.instance, *args, **kwargs)
+
+    def prune(self) -> None:
+        self.containers = [
+            (container, args, kwargs)
+            for container, args, kwargs in self.containers
+            if container.client.id in globals.clients
+        ]

+ 1 - 0
nicegui/ui.py

@@ -11,6 +11,7 @@ from .elements.card import Card as card
 from .elements.card import CardActions as card_actions
 from .elements.card import CardSection as card_section
 from .elements.chart import Chart as chart
+from .elements.chat_message import ChatMessage as chat_message
 from .elements.checkbox import Checkbox as checkbox
 from .elements.color_input import ColorInput as color_input
 from .elements.color_picker import ColorPicker as color_picker