Search code examples
pythontuitextual

Problem with recomposing a VerticalScroll


Recently I've been working on a project to make a TUI for telegram with python and Textual. But when I'm trying to add new messages to msg_list the recomposing the Chat class. messages just go away and only the last message is show up. I've checked the msg_list and yes messages are getting added to the list. and notice that composing the first time doesn't make this problem. This is my code, It's a little mess sorry.

from textual.app import App, ComposeResult
from textual.widgets import Footer, Header, Digits, Button, Label, Input
from textual.containers import Horizontal, HorizontalGroup, VerticalScroll, VerticalGroup
from textual import on
from uuid import uuid4
from typing import Union
import asyncio
msgs = {
    'uuid':'class'
}
class Message(VerticalGroup):
    uuid:Union[str, None] = None
    reply_to:Union[str, None] = None
    fullname:str = ''
    text:str = ''
    date:str = ''
    def compose(self) -> ComposeResult:
        fullname = Label(self.fullname)
        message_text = Label(self.text,expand=True, shrink=True)
        message_text.add_class('txt')
        send_date = Label(self.date)
        send_date.add_class('date')
        if isinstance(self.reply_to, str):
            reply_to = ' '+self.reply_to[:50]
            reply_to = Label(reply_to)
            yield reply_to
        yield fullname
        yield message_text
        yield send_date

def pack_a_message(sender, text, date, reply_to=None,cls='me'):
    msg = Message()
    msg.uuid = uuid4().hex
    msg.reply_to = reply_to
    msg.date = date
    msg.text = text
    msg.fullname = sender
    msg.add_class(cls)
    reply = Button(' ', variant='primary', id=f'i_{msg.uuid}')
    reply.add_class('reply')
    
    return HorizontalGroup(msg, reply)
     


d_id = {}
def dialog(fullname, online=True):
    online = '󰀅 ' if online else ' '
    f = uuid4().hex
    d_id['i'+f] = fullname
    
    l = Button(f"{online} {fullname}", id='i'+f, variant='success')
    l.add_class('dialog')
    
    return l


dialogs = [
    dialog("Alice Smith"), dialog("Bob Jones"), dialog("Charlie Taylor"), dialog("David Brown"), 
    dialog("Eva Williams"), dialog("Frank Davis"), dialog("Grace Miller"), dialog("Hank Wilson"), 
    dialog("Ivy Moore"), dialog("Jack Anderson"), dialog("Kathy Thomas"), dialog("Louis Jackson"), 
    dialog("Mona White"), dialog("Nathan Harris"), dialog("Olivia Martin"), dialog("Paul Thompson"), 
    dialog("Quinn Garcia"), dialog("Rachel Martinez"), dialog("Sam Roberts"), dialog("Tom Walker"), 
    dialog("Uma Smith", False), dialog("Vera Jones", False), dialog("Walter Taylor", False), 
    dialog("Xander Brown", False), dialog("Yara Williams", False), dialog("Zack Davis", False), 
    dialog("Alice Miller", False), dialog("Bob Wilson", False), dialog("Charlie Moore", False), 
    dialog("David Anderson", False), dialog("Eva Thomas", False), dialog("Frank Jackson", False), 
    dialog("Grace White", False), dialog("Hank Harris", False), dialog("Ivy Martin", False), 
    dialog("Jack Thompson", False), dialog("Kathy Garcia", False), dialog("Louis Martinez", False), 
    dialog("Mona Roberts", False), dialog("Nathan Walker", False), dialog("Olivia Smith", False), 
    dialog("Paul Jones", False), dialog("Quinn Taylor", False), dialog("Rachel Brown", False), 
    dialog("Sam Williams", False), dialog("Tom Davis", False)
]
msg_list = []

class Chats(VerticalScroll):
    def compose(self) -> ComposeResult:
        self.inp = Input(placeholder='Type something...', id='send')
        message_input = HorizontalGroup(self.inp, Button(' ', id='send_msg'), id='send_box')

        # Yielding each message from msg_list ensures that they get laid out in the correct order
        for msg in msg_list:
            yield msg
        yield message_input
    
        
    def clear(self):
        self.inp.value = ''

class ChocolateGram(App):
    """A Textual app to manage stopwatches."""
    CSS_PATH = 'stop.tcss'
    BINDINGS = [("d", "toggle_dark", "Toggle dark mode"), ("ctrl+s", "send_message", "Send Message")]

    def compose(self) -> ComposeResult:
        """Create child widgets for the app."""
        self.search = Input(
            placeholder="Search...",
            id='search'
        )
        self.dialogs = VerticalScroll(self.search, *dialogs)
        self.dialogs.add_class('cont-di')
        yield Header()

        
        self.chat = Chats()
        yield HorizontalGroup(
            self.dialogs,
            self.chat
            )
        
        yield Footer()

    @on(Input.Changed)
    def search_in_dialogs(self, event: Input.Changed) -> None:
        """Update the UI to show or hide dialogs based on search."""
        if event.input.id == 'search':
            for dialog in dialogs:
                if event.input.value not in d_id[dialog.id].lower() :
                    # Dynamically remove the dialog if it doesn't match the search query.
                    dialog.add_class('hidden')
                else:
                    try:
                        dialog.remove_class('hidden')
                    except:
                        pass

    def action_send_message(self):
        msg_list.append(pack_a_message('me', self.chat.inp.value, '12:12:12'))
        self.chat.clear()
        self.chat.refresh(recompose=True)
        
    
    

    def action_toggle_dark(self) -> None:
        """An action to toggle dark mode."""
        self.theme = (
            "textual-dark" if self.theme == "textual-light" else "textual-light"
        )
        
        


if __name__ == "__main__":
    msg_list.append(pack_a_message('me', 'self.inp.value', '12:12:12'))
    msg_list.append(pack_a_message('me', 'self.inp.value', '12:12:12'))
    app = ChocolateGram()
    app.run()

I need a way to update my VirticalScroll. so It can show new messages as well as the old ones.


Solution

  • When you recompose, Textual will remove the widgets, and call compose() again to create new widgets. This can be useful, but it isn't what you want with a long stream of chat messages.

    It's way more efficient to add new chat messages as required, which you can do with the mount() method. See the tutorial on dynamic widgets