Compare commits

..

2 Commits

8 changed files with 226 additions and 339 deletions

View File

@ -2,6 +2,14 @@
# Их использование крайне нежелательно!
# Получите свои API-ключи приложения на https://my.telegram.org/apps и вставьте их ниже.
# Спасибо за понимание!
#
# In EN: WARNING: it's official TG API-keys, please, don't use them, get yours via link above. Thanks!
API_ID=2040
API_HASH=b18441a1ff607e10a989891a5462e627
# Конфиг
CURRENT_USER = "user"
DO_NOTIFY = False
UTC_OFFSET = 0
LANGUAGE = "en"

View File

@ -1,8 +1,8 @@
#!/usr/bin/python
"""Файл инициализации приложения"""
from src.app import TelegramTUI
from src.app import Talc
if __name__ == "__main__":
tg = TelegramTUI()
tg.run()
talc = Talc()
talc.run()

View File

@ -1,6 +1,3 @@
textual
telethon
python-dotenv
emoji
Pillow
pywhatkit

View File

@ -1,44 +1,33 @@
"""Главный файл приложения"""
import os
import sys
from os import getenv
from dotenv import load_dotenv
from telethon import TelegramClient, events
from telethon import TelegramClient
from textual.app import App
from rich.console import Console
from src.screens import AuthScreen, ChatScreen
# Настройка консоли для корректной работы с Unicode
"""
console = Console(force_terminal=True, color_system="auto")
sys.stdout = console
"""
# спойлер: не помогло
import src.locales
load_dotenv()
API_ID = getenv("API_ID")
API_HASH = getenv("API_HASH")
api_id = os.getenv("API_ID")
api_hash = os.getenv("API_HASH")
if not api_id or not api_hash:
if not API_ID or not API_HASH:
raise ValueError(
"API_ID и API_HASH не найдены в .env файле. "
"Пожалуйста, скопируйте .env.example в .env и заполните свои ключи."
)
api_id = int(api_id)
API_ID = int(API_ID)
class TelegramTUI(App):
#locale = locales.
class Talc(App):
"""Класс приложения"""
CSS_PATH = "style.tcss"
TITLE = "Telegram TUI"
def __init__(self):
super().__init__()
async def on_mount(self) -> None:
self.telegram_client = TelegramClient("user", api_id, api_hash)
self.telegram_client = TelegramClient(getenv("CURRENT_USER"), API_ID, API_HASH)
await self.telegram_client.connect()
chat_screen = ChatScreen(telegram_client=self.telegram_client)
@ -51,9 +40,8 @@ class TelegramTUI(App):
else:
self.push_screen("chats")
self.scroll_sensitivity_y = 1.0
async def on_exit_app(self):
await self.telegram_client.disconnect()
return super()._on_exit_app()
if __name__ == "__main__":
raise Exception("Запущен не тот файл. Запустите main.py.")

2
src/locales.py Normal file
View File

@ -0,0 +1,2 @@
ru = {"greeting_auth": "Добро пожаловать в Тальк", "phone_number": "Номер телефона", "code": "Код", "password": "Пароль", "you": "Вы", "mention": "Вас упомянули"}
en = {"greeting_auth": "Welcome to Talc", "phone_number": "Phone number", "code": "Code", "password": "Password", "you": "You", "mention": "You got mentioned"}

View File

@ -3,12 +3,12 @@
from textual.screen import Screen
from textual.widgets import Label, Input, Footer, Static, ContentSwitcher
from textual.containers import Vertical, Horizontal, VerticalScroll
from textual.events import Key
from textual.app import ComposeResult
from telethon.errors import SessionPasswordNeededError
from telethon import TelegramClient, events
from src.widgets import Dialog, Chat, normalize_text
from textual import log
from textual.keys import Keys, _character_to_key
from src.widgets import Chat
from os import system, getenv
from telethon.utils import get_display_name
class AuthScreen(Screen):
"""Класс экрана логина в аккаунт"""
@ -19,83 +19,75 @@ class AuthScreen(Screen):
id = None,
classes = None,
telegram_client: TelegramClient | None = None
):
) -> None:
super().__init__(name, id, classes)
self.client = telegram_client
def on_mount(self):
def on_mount(self) -> None:
self.ac = self.query_one("#auth_container")
def compose(self):
def compose(self) -> ComposeResult:
with Vertical(id="auth_container"):
yield Label(normalize_text("Добро пожаловать в Telegram TUI"))
yield Input(placeholder=normalize_text("Номер телефона"), id="phone")
yield Input(placeholder=normalize_text("Код"), id="code", disabled=True)
yield Label("Добро пожаловать в Talc")
yield Input(placeholder="Номер телефона", id="phone")
yield Input(placeholder="Код", id="code", disabled=True)
yield Input(
placeholder=normalize_text("Пароль"),
placeholder="Пароль",
id="password",
password=True,
disabled=True
)
async def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "phone":
self.phone = normalize_text(event.value)
self.ac.query_one("#phone").disabled = True
self.ac.query_one("#code").disabled = False
await self.client.send_code_request(phone=self.phone)
elif event.input.id == "code":
try:
self.code = normalize_text(event.value)
self.ac.query_one("#code").disabled = True
await self.client.sign_in(phone=self.phone, code=self.code)
match event.input.id:
case "phone":
self.phone = event.value
self.ac.query_one("#phone").disabled = True
self.ac.query_one("#code").disabled = False
await self.client.send_code_request(phone=self.phone)
case "code":
try:
self.code = event.value
self.ac.query_one("#code").disabled = True
await self.client.sign_in(phone=self.phone, code=self.code)
self.app.pop_screen()
self.app.push_screen("chats")
except SessionPasswordNeededError:
self.ac.query_one("#code").disabled = True
self.ac.query_one("#password").disabled = False
case "password":
self.password = event.value
await self.client.sign_in(password=self.password)
await self.client.start()
self.app.pop_screen()
self.app.push_screen("chats")
except SessionPasswordNeededError:
self.ac.query_one("#code").disabled = True
self.ac.query_one("#password").disabled = False
elif event.input.id == "password":
self.password = normalize_text(event.value)
await self.client.sign_in(password=self.password)
await self.client.start()
self.app.pop_screen()
self.app.push_screen("chats")
class ChatScreen(Screen):
"""Класс экрана чатов, он же основной экран приложения"""
BINDINGS = [
(Keys.Tab, "log(\"Нажат таб\")", "Переключение фокуса"),
(Keys.Enter, "log(\"Нажат энтер\")", "Открыть"),
(Keys.Escape, "log(\"Нажат эскейп\")", "Назад"),
(_character_to_key("/"), "log(\"Нажат слэш\")", "Поиск")
]
def __init__(
self,
name = None,
id = None,
classes = None,
telegram_client: TelegramClient | None = None
):
) -> None:
super().__init__(name, id, classes)
self.telegram_client = telegram_client
self.search_query = ""
self.selected_chat_index = 0
self.chats = []
self.focused_element = "search" # search, chat_list, dialog
self.DO_NOTIFY = getenv("DO_NOTIFY")
async def on_mount(self):
async def on_mount(self) -> None:
self.limit = 100
#Получение ID пользователя (себя)
self.me_id = await self.telegram_client.get_peer_id("me")
# Получение объекта контейнера чатов
self.chat_container = self\
.query_one("#main_container")\
.query_one("#chats")\
.query_one("#chat_container")
self.search_input = self.query_one("#search_input")
log("Первоначальная загрузка виджетов чатов...")
print("Первоначальная загрузка виджетов чатов...")
self.mount_chats(
len(
await self.telegram_client.get_dialogs(
@ -103,37 +95,44 @@ class ChatScreen(Screen):
)
)
)
log("Первоначальная загрузка виджетов чата завершена")
print("Первоначальная загрузка виджетов чата завершена")
self.is_chat_update_blocked = False
await self.update_chat_list()
print("Первоначальная загрузка чатов завершена")
log("Первоначальная загрузка чатов завершена")
# Автообновление чатов при следующих событиях
for event in (
events.NewMessage,
events.MessageDeleted,
events.MessageEdited
):
self.telegram_client.on(event())(self.update_chat_list)
self.telegram_client.on(events.NewMessage)(self.notify_send)
def mount_chats(self, limit: int):
log("Загрузка виджетов чатов...")
def mount_chats(self, limit: int) -> None:
"""Функция маунта чатов"""
print("Загрузка виджетов чатов...")
# Счёт текущего количества примонтированных чатов
chats_amount = len(self.chat_container.query(Chat))
if limit > chats_amount:
# Маунт недостоющих, если чатов меньше, чем нужно
for i in range(limit - chats_amount):
chat = Chat(id=f"chat-{i + chats_amount + 1}")
self.chat_container.mount(chat)
elif limit < chats_amount:
# Удаление лишних, если чатов больше, чем нужно
for i in range(chats_amount - limit):
self.chat_container.query(Chat).last().remove()
# Ничего, если их ровно столько же
log("Виджеты чатов загружены")
print("Виджеты чатов загружены")
async def update_chat_list(self, event = None):
log("Запрос обновления чатов")
async def update_chat_list(self, event = None) -> None:
"""Функция обновления чатов (и уведомления)"""
print("Запрос обновления чатов")
if not self.is_chat_update_blocked:
self.is_chat_update_blocked = True
@ -141,96 +140,62 @@ class ChatScreen(Screen):
dialogs = await self.telegram_client.get_dialogs(
limit=self.limit, archived=False
)
log("Получены диалоги")
# Фильтруем диалоги по поисковому запросу
if self.search_query:
dialogs = [
d for d in dialogs
if self.search_query.lower() in \
normalize_text(d.name).lower()
]
print("Получены диалоги")
# Маунт виджетов чатов в панели чатов по лимиту
limit = len(dialogs)
self.mount_chats(limit)
# Изменение надписей в виджетах чатов
for i in range(limit):
chat = self.chat_container.query_one(f"#chat-{i + 1}")
chat.username = normalize_text(str(dialogs[i].name))
chat.msg = normalize_text(str(dialogs[i].message.message))
chat.peername = str(dialogs[i].name)
chat.is_group = dialogs[i].is_group
chat.peer_id = dialogs[i].id
chat.is_selected = (i == self.selected_chat_index)
chat.is_focused = (self.focused_element == "chat_list" and \
i == self.selected_chat_index)
try:
is_my_msg = \
dialogs[i].message.from_id.user_id == self.me_id
except:
is_my_msg = dialogs[i].id == self.me_id
if dialogs[i].is_group and is_my_msg:
chat.username = "Вы"
chat.msg = str(dialogs[i].message.message)
elif dialogs[i].is_group:
chat.username = str(
get_display_name(dialogs[i].message.sender)
)
chat.msg = str(dialogs[i].message.message)
elif is_my_msg:
chat.msg = "Вы: " * is_my_msg + str(
dialogs[i].message.message
)
else:
chat.msg = str(dialogs[i].message.message)
self.is_chat_update_blocked = False
log("Чаты обновлены")
print("Чаты обновлены")
else:
log("Обновление чатов невозможно: уже выполняется")
print("Обновление чатов невозможно: уже выполняется")
def on_input_changed(self, event: Input.Changed) -> None:
if event.input.id == "search_input":
self.search_query = normalize_text(event.value)
self.selected_chat_index = 0
self.update_chat_list()
def notify_send(self, event) -> None:
if not event:
return None
if bool(self.DO_NOTIFY) and event.mentioned and not self.app.focused:
system(f"notify-send \"Вас упомянули\" Talc")
def on_key(self, event: Key) -> None:
if event.key == Keys.Tab:
# Переключаем фокус между элементами
if self.focused_element == "search":
self.focused_element = "chat_list"
self.search_input.blur()
self.update_chat_list()
elif self.focused_element == "chat_list":
self.focused_element = "search"
self.search_input.focus()
self.update_chat_list()
return
if self.focused_element == "search":
return
chats = self.chat_container.query(Chat)
if not chats:
return
match event.key:
case Keys.Up:
self.selected_chat_index = max(0, self.selected_chat_index - 1)
for i, chat in enumerate(chats):
chat.is_selected = (i == self.selected_chat_index)
chat.is_focused = (i == self.selected_chat_index)
# Прокручиваем к выбранному чату
selected_chat = chats[self.selected_chat_index]
self.chat_container.scroll_to(selected_chat, animate=False)
case Keys.Down:
self.selected_chat_index = min(len(chats) - 1, self.selected_chat_index + 1)
for i, chat in enumerate(chats):
chat.is_selected = (i == self.selected_chat_index)
chat.is_focused = (i == self.selected_chat_index)
# Прокручиваем к выбранному чату
selected_chat = chats[self.selected_chat_index]
self.chat_container.scroll_to(selected_chat, animate=False)
case Keys.Enter:
chats[self.selected_chat_index].on_click()
case Keys.Escape:
# Возвращаемся к списку чатов
self.app.pop_screen()
self.app.push_screen("chats")
case "/": #Не работает: нужен кейкод слэша
# Фокус на поиск
self.focused_element = "search"
self.search_input.focus()
self.update_chat_list()
def compose(self):
yield Footer()
with Horizontal(id="main_container"):
with Vertical(id="chats"):
yield Input(placeholder=normalize_text("Поиск чатов..."), id="search_input")
def compose(self) -> ComposeResult:
yield Footer() # Нижняя панель с подсказками
with Horizontal(id="main_container"): # Основной контейнер
with Horizontal(id="chats"):
yield VerticalScroll(id="chat_container")
yield ContentSwitcher(id="dialog_switcher")
#yield Dialog(telegram_client=self.telegram_client)
if __name__ == "__main__":
raise Exception("Запущен не тот файл. Запустите main.py.")
#TODO: сделать кнопку, чтобы прогрузить больше чатов,
# или ленивую прокрутку
with ContentSwitcher(id="dialog_switcher"):
# ↑ Внутри него как раз крутятся диалоги
yield Label(
"Нажмите на чат в панели слева, чтобы начать общаться",
id="begin_talk_label"
) #TODO: не показывается надпись, надо будет исправить

View File

@ -10,6 +10,10 @@ Chat {
height: 3;
}
Chat Horizontal Vertical Label{
height: 1;
}
Rule {
color: #FFFFFF;
}
@ -42,12 +46,13 @@ Message Container {
align: right middle;
}
.is_me_true Static {
.is_me_true Container Label {
border: solid $primary;
width: auto;
height: auto;
text-align: right;
text-align: left;
min-width: 11;
max-width: 100%;
}
.is_me_false Container {
@ -55,10 +60,24 @@ Message Container {
align: left middle;
}
.is_me_false Static {
.is_me_false Container Label {
border: solid $foreground;
width: auto;
height: auto;
text-align: left;
min-width: 11;
max-width: 100%;
}
ContentSwitcher {
width: 100%;
height: 100%;
}
.begin_talk_label {
width: 100%;
height: 100%;
text-align: center;
content-align: center middle;
color: $panel;
}

View File

@ -5,85 +5,19 @@ from textual.widget import Widget
from textual.reactive import Reactive
from textual.widgets import Input, Button, Label, Static, ContentSwitcher
from textual.app import ComposeResult, RenderResult
from telethon import TelegramClient, events, utils
from textual.content import Content
from telethon import TelegramClient, events, utils, types
import datetime
import unicodedata
import re
import emoji
import os
import tempfile
from PIL import Image
#import pywhatkit as kit
from textual import log
from warnings import deprecated
def remove_emoji(text: str) -> str:
"""Удаляет эмодзи из текста"""
if not text:
return ""
return emoji.replace_emoji(text, '')
def normalize_text(text: str) -> str:
"""Нормализует текст для корректного отображения"""
if not text:
return ""
# Удаляем эмодзи
text = remove_emoji(text)
# Удаляем все управляющие символы
text = ''\
.join(char for char in text if unicodedata.category(char)[0] != 'C')
# Нормализуем Unicode
text = unicodedata.normalize('NFKC', text)
# Заменяем специальные символы на их ASCII-эквиваленты
text = text.replace('', '-').replace('', '-')
# Удаляем все непечатаемые символы
text = ''.join(char for char in text if char.isprintable())
return text
def safe_ascii(text: str) -> str:
"""Преобразует текст в безопасный ASCII-формат"""
if not text:
return ""
# Удаляем эмодзи
text = remove_emoji(text)
# Оставляем только ASCII символы и пробелы
return ''.join(char for char in text if ord(char) < 128 or char.isspace())
@deprecated("Не работает на моём компьютере.")
def convert_image_to_ascii(image_path: str, width: int = 50) -> str:
"""Конвертирует изображение в ASCII-арт"""
try:
# Открываем изображение
img = Image.open(image_path)
# Конвертируем в RGB если нужно
if img.mode != 'RGB':
img = img.convert('RGB')
# Изменяем размер, сохраняя пропорции
aspect_ratio = img.height / img.width
height = int(width * aspect_ratio * 0.5) # * 0.5 потому что символы выше чем шире
img = img.resize((width, height))
# Конвертируем в ASCII
ascii_str = kit.image_to_ascii_art(image_path, output_file=None)
# Очищаем временный файл
os.remove(image_path)
return ascii_str
except Exception as e:
log(f"Ошибка конвертации изображения: {e}")
return "Ошибка загрузки изображения"
from os import getenv
class Chat(Widget):
"""Класс виджета чата для панели чатов"""
username: Reactive[str] = Reactive(" ", recompose=True)
peername: Reactive[str] = Reactive(" ", recompose=True)
msg: Reactive[str] = Reactive(" ", recompose=True)
is_group: Reactive[bool] = Reactive(False, recompose=True)
peer_id: Reactive[int] = Reactive(0)
is_selected: Reactive[bool] = Reactive(False)
is_focused: Reactive[bool] = Reactive(False)
def __init__(
self,
@ -100,19 +34,14 @@ class Chat(Widget):
)
def on_mount(self) -> None:
self.switcher = self.screen.query_one(Horizontal).query_one("#dialog_switcher", ContentSwitcher)
self.switcher = self.screen.query_one(Horizontal)\
.query_one("#dialog_switcher", ContentSwitcher)
def on_click(self) -> None:
# Снимаем выделение со всех чатов
for chat in self.screen.query(Chat):
chat.is_selected = False
chat.is_focused = False
# Выделяем текущий чат
self.is_selected = True
self.is_focused = True
# Получение ID диалога и создание DOM-ID на его основе
dialog_id = f"dialog-{str(self.peer_id)}"
# Маунт диалога
try:
self.switcher.mount(Dialog(
telegram_client=self.app.telegram_client,
@ -120,34 +49,21 @@ class Chat(Widget):
id=dialog_id
))
except:
# Диалог уже есть: ничего не делаем
pass
self.switcher.current = dialog_id
self.switcher.recompose()
def compose(self) -> ComposeResult:
with Horizontal(classes="chat-item"):
"""
# Используем ASCII-символы для рамки
yield Label(
f"┌───┐\n{normalize_text(
self.username[:1].upper()
):1} \n"
)
with Horizontal():
yield Label(f"┌───┐\n{self.peername[:1]:1}\n└───┘")
with Vertical():
yield Label(normalize_text(self.username), id="name")
yield Label(normalize_text(self.msg), id="last_msg")
"""
yield Label(f"┌───┐\n{self.username[:1].upper():1}\n└───┘")
with Vertical():
yield Label(self.username, id="name")
yield Label(self.peername, id="peername")
if self.is_group:
yield Label(self.username, id="name")
yield Label(self.msg, id="last_msg")
def on_mouse_enter(self) -> None:
self.add_class("hover")
def on_mouse_leave(self) -> None:
self.remove_class("hover")
class Dialog(Widget):
"""Класс окна диалога"""
@ -158,25 +74,23 @@ class Dialog(Widget):
disabled=None,
telegram_client: TelegramClient | None = None,
chat_id = None
) -> None:
) -> None:
super().__init__(id=id, classes=classes, disabled=disabled)
self.telegram_client = telegram_client
self.chat_id = chat_id
self.is_msg_update_blocked = False
self.messages_loaded = 0
self.is_loading = False
self.timezone = datetime.timezone(
datetime.timedelta(hours=int(getenv("UTC_OFFSET")))
)
async def on_mount(self) -> None:
self.limit = 50 # Увеличиваем начальное количество сообщений
self.messages_loaded = self.limit
self.limit = 10
self.msg_input = self.query_one("#msg_input")
self.dialog = self.query_one(Vertical).query_one("#dialog")
self.load_more_btn = self.query_one("#load_more")
self.me = await self.telegram_client.get_me()
self.dialog.scroll_end(animate=False)
await self.update_dialog()
for event in (
@ -188,6 +102,8 @@ class Dialog(Widget):
event(chats=(self.chat_id))
)(self.update_dialog)
self.dialog.scroll_down(animate=False, immediate=True)
def mount_messages(self, limit: int) -> None:
print("Загрузка виджетов сообщений...")
@ -204,7 +120,7 @@ class Dialog(Widget):
self.dialog.query(Message).last().remove()
async def update_dialog(self, event = None) -> None:
log("Запрос обновления сообщений")
print("Запрос обновления сообщений")
if not self.is_msg_update_blocked:
self.is_msg_update_blocked = True
@ -212,31 +128,52 @@ class Dialog(Widget):
messages = await self.telegram_client.get_messages(
entity=self.chat_id, limit=self.limit
)
log("Получены сообщения")
print("Получены сообщения")
limit = len(messages)
self.mount_messages(limit)
for i in range(limit):
msg = self.dialog.query_one(f"#msg-{i + 1}")
msg.message = ""
# Обрабатываем изображения
if messages[i].media:
try:
# Скачиваем изображение
image_data = await self.telegram_client.download_media(
messages[i].media,
bytes
)
if image_data:
await msg.set_image(image_data)
except Exception as e:
log(f"Ошибка загрузки изображения: {e}")
# Обрабатываем текст
if str(messages[i].message):
msg.message = normalize_text(str(messages[i].message))
message = Content(
"[Медиа] " * \
bool(messages[i].media) + \
str(messages[i].message)
)
else:
message = Content("[Медиа]" * bool(messages[i].media) + "")
entities = messages[i].entities
if entities:
for entity in entities:
match type(entity):
case types.MessageEntityBold:
message = message.stylize(
"bold",
entity.offset,
entity.offset + entity.length
)
case types.MessageEntityUnderline:
message = message.stylize(
"underline",
entity.offset,
entity.offset + entity.length
)
case types.MessageEntityItalic:
message = message.stylize(
"italic",
entity.offset,
entity.offset + entity.length
)
case types.MessageEntityStrike:
message = message.stylize(
"strike",
entity.offset,
entity.offset + entity.length
)
msg.message = message
try:
is_me = messages[i].from_id.user_id == self.me.id
@ -244,52 +181,26 @@ class Dialog(Widget):
is_me = False
msg.is_me = is_me
msg.username = normalize_text(utils.get_display_name(messages[i].sender))
msg.username = utils.get_display_name(messages[i].sender)
msg.send_time = messages[i]\
.date\
.astimezone(datetime.timezone.utc)\
.astimezone(self.timezone)\
.strftime("%H:%M")
self.is_msg_update_blocked = False
log("Сообщения обновлены")
print("Сообщения обновлены")
else:
log("Обновление сообщений невозможно: уже выполняется")
async def load_more_messages(self) -> None:
if not self.is_loading:
self.is_loading = True
self.load_more_btn.disabled = True
self.load_more_btn.label = "Загрузка..."
try:
messages = await self.telegram_client.get_messages(
entity=self.chat_id,
limit=self.limit,
offset_id=self.messages_loaded
)
if messages:
self.messages_loaded += len(messages)
self.mount_messages(self.messages_loaded)
await self.update_dialog()
finally:
self.is_loading = False
self.load_more_btn.disabled = False
self.load_more_btn.label = "Загрузить еще"
print("Обновление сообщений невозможно: уже выполняется")
def compose(self) -> ComposeResult:
with Vertical():
yield Button("Загрузить еще", id="load_more", variant="default")
yield VerticalScroll(id="dialog")
with Horizontal(id="input_place"):
yield Input(placeholder="Сообщение", id="msg_input")
yield Button(label=">", id="send", variant="primary")
yield Button(label="", id="send", variant="primary")
async def on_button_pressed(self, event) -> None:
if event.button.id == "load_more":
await self.load_more_messages()
else:
await self.send_message()
async def on_button_pressed(self, event = None) -> None:
await self.send_message()
async def on_input_submitted(self, event = None) -> None:
await self.send_message()
@ -298,7 +209,7 @@ class Dialog(Widget):
try:
await self.telegram_client.send_message(
self.chat_id,
normalize_text(str(self.msg_input.value))
str(self.msg_input.value)
)
except ValueError:
self.app.notify("Ошибка отправки")
@ -308,7 +219,7 @@ class Dialog(Widget):
class Message(Widget):
"""Класс виджета сообщений для окна диалога"""
message: Reactive[str] = Reactive("", recompose=True)
message: Reactive[Content] = Reactive("", recompose=True)
is_me: Reactive[bool] = Reactive(False, recompose=True)
username: Reactive[str] = Reactive("", recompose=True)
send_time: Reactive[str] = Reactive("", recompose=True)
@ -326,17 +237,14 @@ class Message(Widget):
pass
def compose(self) -> ComposeResult:
static = Static(normalize_text(self.message))
static.border_title = normalize_text(self.username) * (not self.is_me)
static.border_subtitle = self.send_time
label = Label(self.message, markup=False)
label.border_title = self.username * (not self.is_me)
label.border_subtitle = self.send_time
with Container():
yield static
yield label
if self.is_me:
self.classes = "is_me_true"
else:
self.classes = "is_me_false"
if __name__ == "__main__":
raise Exception("Запущен не тот файл. Запустите main.py.")