concord 1.3.1

A terminal user interface client for Discord
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
use crate::{
    config,
    discord::{MessageState, PresenceStatus},
    tui::state,
};

use crate::discord::ids::{
    Id,
    marker::{ChannelMarker, GuildMarker, UserMarker},
};

use super::state::DashboardState;

#[derive(Default)]
pub(super) struct RedrawDiagnostics {
    pub(super) key_presses: u32,
    pub(super) mouse_events: u32,
    pub(super) resizes: u32,
    pub(super) terminal_closed: u32,
    pub(super) preview_decodes: u32,
    pub(super) snapshot_events: u32,
    pub(super) effect_events: u32,
    pub(super) redraw_timer_fires: u32,
    pub(super) media_requests: u32,
    pub(super) request_failures: u32,
    pub(super) visible_image_previews_max: usize,
    pub(super) snapshot_message_changes: u32,
    pub(super) snapshot_member_changes: u32,
    pub(super) snapshot_channel_changes: u32,
    pub(super) snapshot_guild_changes: u32,
    pub(super) snapshot_popup_changes: u32,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct VisibleDashboardSignature {
    focus: state::FocusPane,
    leader_active: bool,
    leader_action_mode: bool,
    channel_switcher_open: bool,
    channel_switcher_query: Option<String>,
    channel_switcher_query_cursor: Option<usize>,
    channel_switcher_selected: Option<usize>,
    channel_switcher_result_count: usize,
    channel_switcher_items: Vec<String>,
    guild_pane_visible: bool,
    channel_pane_visible: bool,
    member_pane_visible: bool,
    current_user: Option<String>,
    update_available_version: Option<String>,
    selected_guild_id: Option<Id<GuildMarker>>,
    selected_channel_id: Option<Id<ChannelMarker>>,
    guild_horizontal_scroll: usize,
    channel_horizontal_scroll: usize,
    selected_message: usize,
    message_scroll: usize,
    message_line_scroll: usize,
    pub(super) new_messages_count: usize,
    selected_member: usize,
    member_scroll: usize,
    member_horizontal_scroll: usize,
    channel_action_threads_phase: bool,
    message_pane_title: String,
    typing_footer: Option<String>,
    status_message: Option<String>,
    popups: VisiblePopupSignature,
    visible_guilds: Vec<String>,
    pub(super) visible_channels: Vec<String>,
    pub(super) visible_messages: Vec<MessageState>,
    visible_forum_posts: Vec<state::ChannelThreadItem>,
    visible_members: Vec<MemberEntrySignature>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct VisiblePopupSignature {
    message_action_open: bool,
    image_viewer_open: bool,
    image_viewer_action_open: bool,
    guild_action_open: bool,
    channel_action_open: bool,
    member_action_open: bool,
    options_open: bool,
    selected_option: Option<usize>,
    display_options: config::DisplayOptions,
    emoji_picker_open: bool,
    reaction_users_open: bool,
    poll_vote_picker_open: bool,
    user_profile_open: bool,
    debug_log_open: bool,
    user_profile_data: String,
    user_profile_error: Option<String>,
    user_profile_status: PresenceStatus,
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct MemberEntrySignature {
    user_id: Id<UserMarker>,
    display_name: String,
    username: Option<String>,
    is_bot: bool,
    status: PresenceStatus,
}

pub(super) fn visible_dashboard_signature(state: &DashboardState) -> VisibleDashboardSignature {
    let member_start = state.member_scroll();
    let member_end = member_start.saturating_add(state.member_content_height());
    VisibleDashboardSignature {
        focus: state.focus(),
        leader_active: state.is_leader_active(),
        leader_action_mode: state.is_leader_action_mode(),
        channel_switcher_open: state.is_channel_switcher_open(),
        channel_switcher_query: state.channel_switcher_query().map(str::to_owned),
        channel_switcher_query_cursor: state.channel_switcher_query_cursor_byte_index(),
        channel_switcher_selected: state.selected_channel_switcher_index(),
        channel_switcher_items: channel_switcher_item_signature(state),
        channel_switcher_result_count: state.channel_switcher_items().len(),
        guild_pane_visible: state.is_pane_visible(state::FocusPane::Guilds),
        channel_pane_visible: state.is_pane_visible(state::FocusPane::Channels),
        member_pane_visible: state.is_pane_visible(state::FocusPane::Members),
        current_user: state.current_user().map(str::to_owned),
        update_available_version: state.update_available_version().map(str::to_owned),
        selected_guild_id: state.selected_guild_id(),
        selected_channel_id: state.selected_channel_id(),
        guild_horizontal_scroll: state.guild_horizontal_scroll(),
        channel_horizontal_scroll: state.channel_horizontal_scroll(),
        selected_message: state.selected_message(),
        message_scroll: state.message_scroll(),
        message_line_scroll: state.message_line_scroll(),
        new_messages_count: state.new_messages_count(),
        selected_member: state.selected_member(),
        member_scroll: state.member_scroll(),
        member_horizontal_scroll: state.member_horizontal_scroll(),
        channel_action_threads_phase: state.is_channel_action_threads_phase(),
        message_pane_title: state.message_pane_title(),
        typing_footer: state.typing_footer_for_selected_channel(),
        status_message: state.last_status().map(str::to_owned),
        popups: VisiblePopupSignature {
            message_action_open: state.is_message_action_menu_open(),
            image_viewer_open: state.is_image_viewer_open(),
            image_viewer_action_open: state.is_image_viewer_action_menu_open(),
            guild_action_open: state.is_guild_action_menu_open(),
            channel_action_open: state.is_channel_action_menu_open(),
            member_action_open: state.is_member_action_menu_open(),
            options_open: state.is_options_popup_open(),
            selected_option: state.selected_option_index(),
            display_options: state.display_options(),
            emoji_picker_open: state.is_emoji_reaction_picker_open(),
            reaction_users_open: state.is_reaction_users_popup_open(),
            poll_vote_picker_open: state.is_poll_vote_picker_open(),
            user_profile_open: state.is_user_profile_popup_open(),
            debug_log_open: state.is_debug_log_popup_open(),
            user_profile_data: format!("{:?}", state.user_profile_popup_data()),
            user_profile_error: state.user_profile_popup_load_error().map(str::to_owned),
            user_profile_status: state.user_profile_popup_status(),
        },
        visible_guilds: state
            .visible_guild_pane_entries()
            .into_iter()
            .map(|entry| match entry {
                state::GuildPaneEntry::DirectMessages => {
                    format!("{entry:?} unread={}", state.direct_message_unread_count())
                }
                state::GuildPaneEntry::Guild { state: guild, .. } => {
                    format!(
                        "{entry:?} unread={:?}",
                        state.sidebar_guild_unread(guild.id)
                    )
                }
                state::GuildPaneEntry::FolderHeader { .. } => format!("{entry:?}"),
            })
            .collect(),
        visible_channels: state
            .visible_channel_pane_entries()
            .into_iter()
            .map(|entry| match entry {
                state::ChannelPaneEntry::Channel { state: channel, .. } => format!(
                    "{entry:?} unread={:?} unread_messages={}",
                    state.channel_unread(channel.id),
                    state.channel_unread_message_count(channel.id)
                ),
                state::ChannelPaneEntry::CategoryHeader { .. } => format!("{entry:?}"),
            })
            .collect(),
        visible_messages: state.visible_messages().into_iter().cloned().collect(),
        visible_forum_posts: state.visible_forum_post_items(),
        visible_members: state
            .flattened_members()
            .into_iter()
            .skip(member_start)
            .take(member_end.saturating_sub(member_start))
            .map(|entry| MemberEntrySignature {
                user_id: entry.user_id(),
                display_name: entry.display_name(),
                username: entry.username(),
                is_bot: entry.is_bot(),
                status: entry.status(),
            })
            .collect(),
    }
}

pub(super) fn record_visible_signature_change(
    diagnostics: &mut RedrawDiagnostics,
    before: &VisibleDashboardSignature,
    after: &VisibleDashboardSignature,
) {
    if before.selected_channel_id != after.selected_channel_id
        || before.message_pane_title != after.message_pane_title
        || before.selected_message != after.selected_message
        || before.message_scroll != after.message_scroll
        || before.message_line_scroll != after.message_line_scroll
        || before.new_messages_count != after.new_messages_count
        || before.typing_footer != after.typing_footer
        || before.visible_messages != after.visible_messages
        || before.visible_forum_posts != after.visible_forum_posts
    {
        diagnostics.snapshot_message_changes =
            diagnostics.snapshot_message_changes.saturating_add(1);
    }

    if before.selected_member != after.selected_member
        || before.member_scroll != after.member_scroll
        || before.member_horizontal_scroll != after.member_horizontal_scroll
        || before.visible_members != after.visible_members
    {
        diagnostics.snapshot_member_changes = diagnostics.snapshot_member_changes.saturating_add(1);
    }

    if before.selected_channel_id != after.selected_channel_id
        || before.channel_horizontal_scroll != after.channel_horizontal_scroll
        || before.visible_channels != after.visible_channels
    {
        diagnostics.snapshot_channel_changes =
            diagnostics.snapshot_channel_changes.saturating_add(1);
    }

    if before.selected_guild_id != after.selected_guild_id
        || before.guild_horizontal_scroll != after.guild_horizontal_scroll
        || before.visible_guilds != after.visible_guilds
    {
        diagnostics.snapshot_guild_changes = diagnostics.snapshot_guild_changes.saturating_add(1);
    }

    if before.focus != after.focus
        || before.leader_active != after.leader_active
        || before.leader_action_mode != after.leader_action_mode
        || before.channel_switcher_open != after.channel_switcher_open
        || before.channel_switcher_query != after.channel_switcher_query
        || before.channel_switcher_query_cursor != after.channel_switcher_query_cursor
        || before.channel_switcher_selected != after.channel_switcher_selected
        || before.channel_switcher_result_count != after.channel_switcher_result_count
        || before.channel_switcher_items != after.channel_switcher_items
        || before.guild_pane_visible != after.guild_pane_visible
        || before.channel_pane_visible != after.channel_pane_visible
        || before.member_pane_visible != after.member_pane_visible
        || before.current_user != after.current_user
        || before.status_message != after.status_message
        || before.popups != after.popups
        || before.channel_action_threads_phase != after.channel_action_threads_phase
    {
        diagnostics.snapshot_popup_changes = diagnostics.snapshot_popup_changes.saturating_add(1);
    }
}

fn only_visible_member_signature_changed(
    before: &VisibleDashboardSignature,
    after: &VisibleDashboardSignature,
) -> bool {
    before.focus == after.focus
        && before.leader_active == after.leader_active
        && before.leader_action_mode == after.leader_action_mode
        && before.channel_switcher_open == after.channel_switcher_open
        && before.channel_switcher_query == after.channel_switcher_query
        && before.channel_switcher_query_cursor == after.channel_switcher_query_cursor
        && before.channel_switcher_selected == after.channel_switcher_selected
        && before.channel_switcher_result_count == after.channel_switcher_result_count
        && before.channel_switcher_items == after.channel_switcher_items
        && before.guild_pane_visible == after.guild_pane_visible
        && before.channel_pane_visible == after.channel_pane_visible
        && before.member_pane_visible == after.member_pane_visible
        && before.current_user == after.current_user
        && before.update_available_version == after.update_available_version
        && before.selected_guild_id == after.selected_guild_id
        && before.selected_channel_id == after.selected_channel_id
        && before.guild_horizontal_scroll == after.guild_horizontal_scroll
        && before.channel_horizontal_scroll == after.channel_horizontal_scroll
        && before.selected_message == after.selected_message
        && before.message_scroll == after.message_scroll
        && before.message_line_scroll == after.message_line_scroll
        && before.new_messages_count == after.new_messages_count
        && before.message_pane_title == after.message_pane_title
        && before.typing_footer == after.typing_footer
        && before.status_message == after.status_message
        && before.popups == after.popups
        && before.channel_action_threads_phase == after.channel_action_threads_phase
        && before.visible_guilds == after.visible_guilds
        && before.visible_channels == after.visible_channels
        && before.visible_messages == after.visible_messages
        && before.visible_forum_posts == after.visible_forum_posts
        && (before.selected_member != after.selected_member
            || before.member_scroll != after.member_scroll
            || before.member_horizontal_scroll != after.member_horizontal_scroll
            || before.visible_members != after.visible_members)
}

fn only_new_message_notice_changed(
    before: &VisibleDashboardSignature,
    after: &VisibleDashboardSignature,
) -> bool {
    before.focus == after.focus
        && before.leader_active == after.leader_active
        && before.leader_action_mode == after.leader_action_mode
        && before.channel_switcher_open == after.channel_switcher_open
        && before.channel_switcher_query == after.channel_switcher_query
        && before.channel_switcher_query_cursor == after.channel_switcher_query_cursor
        && before.channel_switcher_selected == after.channel_switcher_selected
        && before.channel_switcher_result_count == after.channel_switcher_result_count
        && before.channel_switcher_items == after.channel_switcher_items
        && before.guild_pane_visible == after.guild_pane_visible
        && before.channel_pane_visible == after.channel_pane_visible
        && before.member_pane_visible == after.member_pane_visible
        && before.current_user == after.current_user
        && before.update_available_version == after.update_available_version
        && before.selected_guild_id == after.selected_guild_id
        && before.selected_channel_id == after.selected_channel_id
        && before.guild_horizontal_scroll == after.guild_horizontal_scroll
        && before.channel_horizontal_scroll == after.channel_horizontal_scroll
        && before.selected_message == after.selected_message
        && before.message_scroll == after.message_scroll
        && before.message_line_scroll == after.message_line_scroll
        && before.selected_member == after.selected_member
        && before.member_scroll == after.member_scroll
        && before.member_horizontal_scroll == after.member_horizontal_scroll
        && before.message_pane_title == after.message_pane_title
        && before.typing_footer == after.typing_footer
        && before.status_message == after.status_message
        && before.popups == after.popups
        && before.channel_action_threads_phase == after.channel_action_threads_phase
        && before.visible_guilds == after.visible_guilds
        && before.visible_channels == after.visible_channels
        && before.visible_messages == after.visible_messages
        && before.visible_forum_posts == after.visible_forum_posts
        && before.visible_members == after.visible_members
        && before.new_messages_count != after.new_messages_count
}

fn channel_switcher_item_signature(state: &DashboardState) -> Vec<String> {
    if !state.is_channel_switcher_open() {
        return Vec::new();
    }
    state
        .channel_switcher_items()
        .into_iter()
        .map(|item| {
            format!(
                "{}:{}:{:?}:{}:{}:{:?}:{}",
                item.channel_id.get(),
                item.group_label,
                item.parent_label,
                item.channel_label,
                item.depth,
                item.unread,
                item.unread_message_count,
            )
        })
        .collect()
}

pub(super) fn should_suppress_image_redraw_for_signature_change(
    before: &VisibleDashboardSignature,
    after: &VisibleDashboardSignature,
    image_surfaces_visible: bool,
) -> bool {
    image_surfaces_visible
        && ((after.focus != state::FocusPane::Members
            && only_visible_member_signature_changed(before, after))
            || (after.focus != state::FocusPane::Channels
                && only_new_message_notice_changed(before, after)))
}

pub(super) fn should_redraw_after_visible_signature_change(
    before: &VisibleDashboardSignature,
    after: &VisibleDashboardSignature,
    image_surfaces_visible: bool,
    force_redraw: bool,
) -> bool {
    force_redraw
        || (before != after
            && !should_suppress_image_redraw_for_signature_change(
                before,
                after,
                image_surfaces_visible,
            ))
}

pub(super) fn image_surfaces_visible(
    state: &DashboardState,
    image_targets_visible: bool,
    avatar_targets_visible: bool,
    emoji_targets_visible: bool,
) -> bool {
    image_targets_visible
        || avatar_targets_visible
        || emoji_targets_visible
        || (state.show_avatars() && state.user_profile_popup_avatar_url().is_some())
}