1pub mod channel_view;
2pub mod chat_panel;
3pub mod collab_panel;
4mod collab_titlebar_item;
5mod face_pile;
6pub mod notification_panel;
7pub mod notifications;
8mod panel_settings;
9mod sharing_status_indicator;
10
11use call::{report_call_event_for_room, ActiveCall, Room};
12use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
13use gpui::{
14 actions,
15 elements::{Empty, Image},
16 geometry::{
17 rect::RectF,
18 vector::{vec2f, Vector2F},
19 },
20 platform::{Screen, WindowBounds, WindowKind, WindowOptions},
21 AnyElement, AppContext, Element, ImageData, Task,
22};
23use std::{rc::Rc, sync::Arc};
24use theme::Theme;
25use time::{OffsetDateTime, UtcOffset};
26use util::ResultExt;
27use workspace::AppState;
28
29pub use collab_titlebar_item::CollabTitlebarItem;
30pub use panel_settings::{
31 ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
32};
33
34actions!(
35 collab,
36 [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
37);
38
39pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
40 settings::register::<CollaborationPanelSettings>(cx);
41 settings::register::<ChatPanelSettings>(cx);
42 settings::register::<NotificationPanelSettings>(cx);
43
44 vcs_menu::init(cx);
45 collab_titlebar_item::init(cx);
46 collab_panel::init(cx);
47 chat_panel::init(cx);
48 notifications::init(&app_state, cx);
49 sharing_status_indicator::init(cx);
50
51 cx.add_global_action(toggle_screen_sharing);
52 cx.add_global_action(toggle_mute);
53 cx.add_global_action(toggle_deafen);
54}
55
56pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
57 let call = ActiveCall::global(cx).read(cx);
58 if let Some(room) = call.room().cloned() {
59 let client = call.client();
60 let toggle_screen_sharing = room.update(cx, |room, cx| {
61 if room.is_screen_sharing() {
62 report_call_event_for_room(
63 "disable screen share",
64 room.id(),
65 room.channel_id(),
66 &client,
67 cx,
68 );
69 Task::ready(room.unshare_screen(cx))
70 } else {
71 report_call_event_for_room(
72 "enable screen share",
73 room.id(),
74 room.channel_id(),
75 &client,
76 cx,
77 );
78 room.share_screen(cx)
79 }
80 });
81 toggle_screen_sharing.detach_and_log_err(cx);
82 }
83}
84
85pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
86 let call = ActiveCall::global(cx).read(cx);
87 if let Some(room) = call.room().cloned() {
88 let client = call.client();
89 room.update(cx, |room, cx| {
90 let operation = if room.is_muted(cx) {
91 "enable microphone"
92 } else {
93 "disable microphone"
94 };
95 report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
96
97 room.toggle_mute(cx)
98 })
99 .map(|task| task.detach_and_log_err(cx))
100 .log_err();
101 }
102}
103
104pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
105 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
106 room.update(cx, Room::toggle_deafen)
107 .map(|task| task.detach_and_log_err(cx))
108 .log_err();
109 }
110}
111
112fn notification_window_options(
113 screen: Rc<dyn Screen>,
114 window_size: Vector2F,
115) -> WindowOptions<'static> {
116 const NOTIFICATION_PADDING: f32 = 16.;
117
118 let screen_bounds = screen.content_bounds();
119 WindowOptions {
120 bounds: WindowBounds::Fixed(RectF::new(
121 screen_bounds.upper_right()
122 + vec2f(
123 -NOTIFICATION_PADDING - window_size.x(),
124 NOTIFICATION_PADDING,
125 ),
126 window_size,
127 )),
128 titlebar: None,
129 center: false,
130 focus: false,
131 show: true,
132 kind: WindowKind::PopUp,
133 is_movable: false,
134 screen: Some(screen),
135 }
136}
137
138fn render_avatar<T: 'static>(avatar: Option<Arc<ImageData>>, theme: &Arc<Theme>) -> AnyElement<T> {
139 let avatar_style = theme.chat_panel.avatar;
140 avatar
141 .map(|avatar| {
142 Image::from_data(avatar)
143 .with_style(avatar_style.image)
144 .aligned()
145 .contained()
146 .with_corner_radius(avatar_style.outer_corner_radius)
147 .constrained()
148 .with_width(avatar_style.outer_width)
149 .with_height(avatar_style.outer_width)
150 .into_any()
151 })
152 .unwrap_or_else(|| {
153 Empty::new()
154 .constrained()
155 .with_width(avatar_style.outer_width)
156 .into_any()
157 })
158 .contained()
159 .with_style(theme.chat_panel.avatar_container)
160 .into_any()
161}
162
163fn format_timestamp(
164 mut timestamp: OffsetDateTime,
165 mut now: OffsetDateTime,
166 local_timezone: UtcOffset,
167) -> String {
168 timestamp = timestamp.to_offset(local_timezone);
169 now = now.to_offset(local_timezone);
170
171 let today = now.date();
172 let date = timestamp.date();
173 let mut hour = timestamp.hour();
174 let mut part = "am";
175 if hour > 12 {
176 hour -= 12;
177 part = "pm";
178 }
179 if date == today {
180 format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
181 } else if date.next_day() == Some(today) {
182 format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
183 } else {
184 format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
185 }
186}
187
188fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
189 cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
190}