Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reply preview improvements and reply preview click smooth scroll #151

Merged
merged 7 commits into from
Sep 22, 2024
157 changes: 130 additions & 27 deletions src/home/room_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ live_design! {
width: Fill
height: Fit
flow: Down

padding: {top: 0.0, right: 12.0, bottom: 0.0, left: 12.0}

// A reply preview with a vertical bar drawn in the background.
Expand Down Expand Up @@ -302,6 +303,54 @@ live_design! {
padding: 0.0,
spacing: 0.0

show_bg: true
draw_bg: {
instance highlight: 0.0
instance hover: 0.0
fn pixel(self) -> vec4 {
return mix(
mix(
#ffffff,
#fafafa,
self.hover
),
#fafafa,
self.highlight
)
}
}

animator: {
highlight = {
default: off
off = {
redraw: true,
from: { all: Forward {duration: 2.0} }
ease: ExpDecay {d1: 0.80, d2: 0.97}
apply: { draw_bg: {highlight: 0.0} }
}
on = {
redraw: true,
from: { all: Forward {duration: 0.5} }
ease: ExpDecay {d1: 0.80, d2: 0.97}
apply: { draw_bg: {highlight: 1.0} }
}
}
hover = {
default: off
off = {
redraw: true,
from: { all: Snap }
apply: { draw_bg: {hover: 0.0} }
}
on = {
redraw: true,
from: { all: Snap }
apply: { draw_bg: {hover: 1.0} }
}
}
}

// A preview of the earlier message that this message was in reply to.
replied_to_message = <RepliedToMessage> {
flow: Right
Expand Down Expand Up @@ -1002,7 +1051,7 @@ impl Widget for RoomScreen {
}
}
MessageAction::ReplyPreviewClicked(item_id) => {
let portal_list = self.portal_list(id!(list));
let mut portal_list = self.portal_list(id!(list));
let Some(tl) = self.tl_state.as_mut() else {
continue;
};
Expand All @@ -1022,15 +1071,42 @@ impl Widget for RoomScreen {
})
});
if let Some(index) = message_replied_to_tl_index {
portal_list.set_first_id(index);
let distance = (index as isize - portal_list.first_id() as isize).abs() as f64;
let base_speed = 10.0;
// apply a scaling based on the distance
let scaled_speed = base_speed * (distance * distance);

// substract to leave some space.
portal_list.smooth_scroll_to(cx, index - 2, scaled_speed);
kevinaboos marked this conversation as resolved.
Show resolved Hide resolved
// start highlight animation.
tl.message_highlight_animation_state = MessageHighlightAnimationState::Pending {
item_id: index
};

self.redraw(cx);
}
}
}
}
}
}
MessageAction::None => {}
_ => {}
}

// Handle the highlight animation.
let portal_list = self.portal_list(id!(list));
let Some(tl) = self.tl_state.as_mut() else {
return;
};
if let MessageHighlightAnimationState::Pending { item_id } = tl.message_highlight_animation_state {
if portal_list.smooth_scroll_reached(actions) {
cx.widget_action(
widget_uid,
&scope.path,
MessageAction::MessageHighlight(item_id),
);
tl.message_highlight_animation_state = MessageHighlightAnimationState::Off;
}
}

// Handle message reply action
Expand Down Expand Up @@ -1215,7 +1291,6 @@ impl Widget for RoomScreen {

}


fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
while let Some(subview) = self.view.draw_walk(cx, scope, walk).step() {
// We only care about drawing the portal list.
Expand Down Expand Up @@ -1437,6 +1512,7 @@ impl RoomScreen {
media_cache: MediaCache::new(MediaFormatConst::File, Some(update_sender)),
replying_to: None,
saved_state: SavedState::default(),
message_highlight_animation_state: MessageHighlightAnimationState::default(),
};
(new_tl_state, true)
};
Expand Down Expand Up @@ -1653,6 +1729,15 @@ struct TimelineUiState {
/// The states relevant to the UI display of this timeline that are saved upon
/// a `Hide` action and restored upon a `Show` action.
saved_state: SavedState,

/// The state of the message highlight animation.
///
/// We need to run the animation once the scrolling, triggered by the click of of a
/// a reply preview, ends. so we keep a small state for it.
/// By default, it starts in Off.
/// Once the scrolling is started, the state becomes Pending.
/// If the animation was trigged, the state goes back to Off.
message_highlight_animation_state: MessageHighlightAnimationState,
}

/// The item index, scroll position, and optional unique IDs of the first `N` events
Expand All @@ -1678,6 +1763,14 @@ struct ItemIndexScroll {
scroll: f64,
}


#[derive(Default, Debug)]
enum MessageHighlightAnimationState {
Pending { item_id: usize, },
#[default]
Off,
}

/// States that are necessary to save in order to maintain a consistent UI display for a timeline.
///
/// These are saved when navigating away from a timeline (upon `Hide`)
Expand Down Expand Up @@ -2482,6 +2575,7 @@ fn get_profile_display_name(event_tl_item: &EventTimelineItem) -> Option<String>
pub enum MessageAction {
MessageReply(usize),
ReplyPreviewClicked(usize),
MessageHighlight(usize),
None,
}

Expand All @@ -2495,10 +2589,22 @@ pub struct Message {
can_be_replied_to: bool,
#[rust]
item_id: usize,
#[animator]
animator: Animator,
}

impl Widget for Message {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
if self.animator_handle_event(cx, event).must_redraw() {
self.redraw(cx);
}

if !self.animator.is_track_animating(cx, id!(highlight))
&& self.animator_in_state(cx, id!(highlight.on))
{
self.animator_play(cx, id!(highlight.off));
}

let widget_uid = self.widget_uid();

if let Event::Actions(actions) = event {
Expand All @@ -2511,7 +2617,7 @@ impl Widget for Message {
}
}

if let Hit::FingerUp(fe) = event.hits(cx, self.view(id!(reply_preview)).area()) {
if let Hit::FingerUp(fe) = event.hits(cx, self.view(id!(replied_to_message)).area()) {
if fe.was_tap() {
cx.widget_action(
widget_uid,
Expand All @@ -2521,41 +2627,38 @@ impl Widget for Message {
}
}

if let Event::Actions(actions) = event {
for action in actions {
match action.as_widget_action().cast() {
MessageAction::MessageHighlight(id) if id == self.item_id => {
self.animator_play(cx, id!(highlight.on));
self.redraw(cx);
}
_ => {}
}
}
}

if let Event::MouseMove(e) = event {
let hovered = self.view.area().rect(cx).contains(e.abs);
if self.hovered != hovered {
if (self.hovered != hovered) || (!hovered && self.animator_in_state(cx, id!(hover.on))){
self.hovered = hovered;

// TODO: Once we have a context menu, the messageMenu can be displayed on hover or push only
// self.view.view(id!(message_menu)).set_visible(hovered);

self.redraw(cx);
let hover_animator = if self.hovered {
id!(hover.on)
} else {
id!(hover.off)
};
self.animator_play(cx, hover_animator);
}
}

self.view.handle_event(cx, event, scope);
}

fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
// TODO: need vecs for apply_over(), maybe use an animator so we just set the state here
// and the animator handles the color changes from inside the dsl.
let default_color = vec3(1.0, 1.0, 1.0); // #ffffff
let hover_color = vec3(0.98, 0.98, 0.98); // #fafafa (very light gray)

let bg_color = if self.hovered {
hover_color
} else {
default_color
};

self.view.apply_over(
cx,
live! {
show_bg: true,
draw_bg: {color: (bg_color)}
},
);

self.view
.button(id!(reply_button))
.set_visible(self.can_be_replied_to);
Expand Down