add reveal text

This commit is contained in:
2026-10-30 14:03:14 +07:00
parent 905a339e3e
commit 41837e487b
5 changed files with 189 additions and 48 deletions
+43 -10
View File
@@ -36,8 +36,8 @@ use state::{
};
use ui::{
Avatar, COMPACT_REACTION_EMOJIS, IconName, MessageActions, MessageMedia, MessageRow,
REACTION_EMOJIS, ReactionBar, RenderedText, ReplyPreview, WelcomeMessage, group_reactions,
opens_run, rendered_text,
REACTION_EMOJIS, ReactionBar, RenderedText, ReplyPreview, RevealText, WelcomeMessage,
group_reactions, opens_run, rendered_text,
};
use crate::file::*;
@@ -90,6 +90,9 @@ pub struct ChatPanel {
speech_started: Option<instant::Instant>,
/// Reads messages aloud
speaker: Speaker,
/// The message being revealed in step with speech, with its estimated duration
speech_reveal: Option<(EventId, Duration)>,
/// Subject input state
subject_input: Entity<InputState>,
/// Subject bar visibility
@@ -222,6 +225,20 @@ impl ChatPanel {
this.get_messages(window, cx);
});
// End the speech reveal when the utterance finishes ahead of its estimate
let speaker = Speaker::new();
let end_events = speaker.end_events();
let reveal_end_task = cx.spawn(async move |this, cx| {
while end_events.recv_async().await.is_ok() {
this.update(cx, |this, cx| {
this.speech_reveal = None;
cx.notify();
})?;
}
Ok(())
});
Self {
focus_handle: cx.focus_handle(),
id,
@@ -234,7 +251,8 @@ impl ChatPanel {
input,
speech,
speech_started: None,
speaker: Speaker::new(),
speaker,
speech_reveal: None,
subject_input,
subject_bar,
replies_to,
@@ -245,7 +263,7 @@ impl ChatPanel {
reports_by_id,
uploading: false,
subscriptions,
tasks: vec![],
tasks: vec![reveal_end_task],
}
}
@@ -1174,6 +1192,15 @@ impl ChatPanel {
nostr.read(cx).current_user(),
));
let body = match self
.speech_reveal
.as_ref()
.filter(|(reveal_id, _)| *reveal_id == id)
{
Some((_, duration)) => RevealText::new(message.preview(), *duration).into_any_element(),
None => rendered_text,
};
MessageRow::new(ix)
.show_author(show_author)
.hide_avatar(hide_avatar)
@@ -1198,7 +1225,7 @@ impl ChatPanel {
.when(has_replies, |this| {
this.children(self.render_message_replies(replies, cx))
})
.when(message.file.is_none(), |this| this.child(rendered_text))
.when(message.file.is_none(), |this| this.child(body))
.child(MessageMedia::new(&message.media))
.when_some(message.file.as_ref(), |this, file| {
this.child(self.render_message_file(&id, file, cx))
@@ -1431,7 +1458,6 @@ impl ChatPanel {
let reply_id = *id;
let dropdown_id = *id;
let speech_id = *id;
let public_key = *public_key;
let actions = actions.reply(cx.listener(move |this, _event, _window, cx| {
@@ -1447,13 +1473,20 @@ impl ChatPanel {
actions
};
let speech_id = *id;
let actions = actions.speech(cx.listener(move |this, _event, window, cx| {
let text = this.message(&speech_id).map(|message| message.preview());
if let Some(text) = text
&& let Err(error) = this.speaker.speak(&text)
{
window.push_notification(Notification::error(error.to_string()), cx);
if let Some(text) = text {
match this.speaker.speak(&text) {
Ok(duration) => {
this.speech_reveal = Some((speech_id, duration));
cx.notify();
}
Err(error) => {
window.push_notification(Notification::error(error.to_string()), cx)
}
}
}
}));