Skip to content

Commit

Permalink
fix(chatbar): don't close emoji picker on scroll (#1435)
Browse files Browse the repository at this point in the history
Co-authored-by: Phill Wisniewski <93608357+phillsatellite@users.noreply.github.com>
  • Loading branch information
sdwoodbury and phillsatellite authored Nov 2, 2023
1 parent dd39023 commit 1551c30
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 228 deletions.
8 changes: 8 additions & 0 deletions ui/src/layouts/chats/data/chatbar/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub type ChatInput = (Vec<String>, Uuid, Option<Uuid>, Option<Uuid>);

mod typing_indicator;
mod typing_info;

pub use typing_indicator::*;
pub use typing_info::*;
use uuid::Uuid;
10 changes: 10 additions & 0 deletions ui/src/layouts/chats/data/chatbar/typing_indicator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#[derive(Eq, PartialEq)]
pub enum TypingIndicator {
// reset the typing indicator timer
Typing(uuid::Uuid),
// clears the typing indicator, ensuring the indicator
// will not be refreshed
NotTyping,
// resend the typing indicator
Refresh(uuid::Uuid),
}
5 changes: 5 additions & 0 deletions ui/src/layouts/chats/data/chatbar/typing_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[derive(Clone)]
pub struct TypingInfo {
pub chat_id: uuid::Uuid,
pub last_update: std::time::Instant,
}
2 changes: 2 additions & 0 deletions ui/src/layouts/chats/data/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
mod chat_data;
mod chat_props;
mod chatbar;
mod js_msg;
mod misc;
mod msg_group;
mod scroll_btn;

pub use chat_data::*;
pub use chat_props::*;
pub use chatbar::*;
pub use js_msg::*;
pub use misc::*;
pub use msg_group::*;
Expand Down
3 changes: 2 additions & 1 deletion ui/src/layouts/chats/presentation/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ pub fn Compose(cx: Scope) -> Element {
let show_edit_group: &UseState<Option<Uuid>> = use_state(cx, || None);
let show_group_users: &UseState<Option<Uuid>> = use_state(cx, || None);

let should_ignore_focus = state.read().ui.ignore_focus;
// if the emoji picker is visible, autofocusing on the chatbar will close the emoji picker.
let should_ignore_focus = state.read().ui.ignore_focus || state.read().ui.emoji_picker_visible;
let creator = chat_data.read().active_chat.creator();

let chat_id = chat_data.read().active_chat.id();
Expand Down
210 changes: 210 additions & 0 deletions ui/src/layouts/chats/presentation/chatbar/coroutines.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
use std::{
path::PathBuf,
time::{Duration, Instant},
};

use common::{
state::{Action, State},
warp_runner::{RayGunCmd, WarpCmd},
STATIC_ARGS, WARP_CMD_CH,
};
use dioxus::prelude::*;
use futures::{channel::oneshot, StreamExt};
use uuid::Uuid;
use warp::raygun::{self, Location};

use crate::layouts::chats::data::{
self, ChatInput, ChatProps, TypingInfo, DEFAULT_MESSAGES_TO_TAKE,
};

use super::TypingIndicator;

pub type MsgChInput = (Vec<String>, Uuid, Option<Uuid>, Option<Uuid>);
pub fn get_msg_ch(
cx: &Scoped<'_, ChatProps>,
state: &UseSharedState<State>,
) -> Coroutine<MsgChInput> {
use_coroutine(cx, |mut rx: UnboundedReceiver<ChatInput>| {
to_owned![state];
async move {
let warp_cmd_tx = WARP_CMD_CH.tx.clone();
while let Some((msg, conv_id, ui_msg_id, reply)) = rx.next().await {
let (tx, rx) = oneshot::channel::<Result<(), warp::error::Error>>();
let attachments = state
.read()
.get_active_chat()
.map(|f| f.files_attached_to_send)
.unwrap_or_default();
let msg_clone = msg.clone();
let cmd = match reply {
Some(reply_to) => RayGunCmd::Reply {
conv_id,
reply_to,
msg,
attachments,
rsp: tx,
},
None => RayGunCmd::SendMessage {
conv_id,
msg,
attachments,
ui_msg_id,
rsp: tx,
},
};
let attachments = state
.read()
.get_active_chat()
.map(|f| f.files_attached_to_send)
.unwrap_or_default();
state
.write_silent()
.mutate(Action::ClearChatAttachments(conv_id));
let attachment_files: Vec<String> = attachments
.iter()
.map(|p| {
let pathbuf = match p {
Location::Disk { path } => path.clone(),
Location::Constellation { path } => PathBuf::from(path),
};
pathbuf
.file_name()
.map_or_else(String::new, |ostr| ostr.to_string_lossy().to_string())
})
.collect();
if let Err(e) = warp_cmd_tx.send(WarpCmd::RayGun(cmd)) {
log::error!("failed to send warp command: {}", e);
state.write().decrement_outgoing_messages(
conv_id,
msg_clone,
attachment_files,
ui_msg_id,
);
continue;
}

let rsp = rx.await.expect("command canceled");
if let Err(e) = rsp {
log::error!("failed to send message: {}", e);
state.write().decrement_outgoing_messages(
conv_id,
msg_clone,
attachment_files,
ui_msg_id,
);
}
}
}
})
.clone()
}

pub fn get_scroll_ch(
cx: &Scoped<'_, ChatProps>,
chat_data: &UseSharedState<data::ChatData>,
state: &UseSharedState<State>,
) -> Coroutine<Uuid> {
use_coroutine(cx, |mut rx: UnboundedReceiver<Uuid>| {
to_owned![chat_data, state];
async move {
while let Some(conv_id) = rx.next().await {
match crate::layouts::chats::presentation::chat::coroutines::fetch_most_recent(
conv_id,
DEFAULT_MESSAGES_TO_TAKE,
)
.await
{
Ok((messages, behavior)) => {
log::debug!("re-init messages with most recent");
chat_data.write().set_active_chat(
&state.read(),
&conv_id,
behavior,
messages,
);
}
Err(e) => log::error!("{e}"),
}
}
}
})
.clone()
}

// typing indicator notes
// consider side A, the local side, and side B, the remote side
// side A -> (typing indicator) -> side B
// side B removes the typing indicator after a timeout
// side A doesn't want to send too many typing indicators, say once every 4-5 seconds
// should we consider matching the timeout with the send frequency so we can closely match if a person is straight up typing for 5 mins straight.

// tracks if the local participant is typing
// re-sends typing indicator in response to the Refresh command
pub fn get_typing_ch(cx: &Scoped<'_, ChatProps>) -> Coroutine<TypingIndicator> {
use_coroutine(cx, |mut rx: UnboundedReceiver<TypingIndicator>| {
// to_owned![];
async move {
let mut typing_info: Option<TypingInfo> = None;
let warp_cmd_tx = WARP_CMD_CH.tx.clone();

let send_typing_indicator = |conv_id| async move {
let (tx, rx) = oneshot::channel::<Result<(), warp::error::Error>>();
let event = raygun::MessageEvent::Typing;
if let Err(e) = warp_cmd_tx.send(WarpCmd::RayGun(RayGunCmd::SendEvent {
conv_id,
event,
rsp: tx,
})) {
log::error!("failed to send warp command: {}", e);
// return from the closure
return;
}
let rsp = rx.await.expect("command canceled");
if let Err(e) = rsp {
log::error!("failed to send typing indicator: {}", e);
}
};

while let Some(indicator) = rx.next().await {
match indicator {
TypingIndicator::Typing(chat_id) => {
// if typing_info was none or the chat id changed, send the indicator immediately
let should_send_indicator = match typing_info {
None => true,
Some(info) => info.chat_id != chat_id,
};
if should_send_indicator {
send_typing_indicator.clone()(chat_id).await;
}
typing_info = Some(TypingInfo {
chat_id,
last_update: Instant::now(),
});
}
TypingIndicator::NotTyping => {
typing_info = None;
}
TypingIndicator::Refresh(conv_id) => {
let info = match &typing_info {
Some(i) => i.clone(),
None => continue,
};
if info.chat_id != conv_id {
typing_info = None;
continue;
}
// todo: verify duration for timeout
let now = Instant::now();
if now - info.last_update
<= (Duration::from_secs(STATIC_ARGS.typing_indicator_timeout)
- Duration::from_millis(500))
{
send_typing_indicator.clone()(conv_id).await;
}
}
}
}
}
})
.clone()
}
Loading

0 comments on commit 1551c30

Please sign in to comment.