concord 2.0.2

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
406
407
408
409
410
411
412
413
414
415
416
417
418
419
use std::{
    collections::hash_map::DefaultHasher,
    fmt::{self, Write as _},
    hash::Hasher,
};

use crate::{
    config,
    discord::{ChannelUnreadState, PresenceStatus},
    tui::state,
};

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

use super::state::DashboardState;

#[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<ChannelSwitcherItemSignature>,
    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>,
    composer_mention_query: Option<String>,
    composer_mention_selected: usize,
    composer_mention_candidates: Vec<MemberEntrySignature>,
    popups: VisiblePopupSignature,
    visible_guilds: Vec<GuildEntrySignature>,
    pub(super) visible_channels: Vec<ChannelEntrySignature>,
    pub(super) visible_messages: Vec<DebugSignature>,
    visible_forum_posts: Vec<DebugSignature>,
    visible_members: Vec<MemberEntrySignature>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct VisiblePopupSignature {
    message_action_open: bool,
    image_viewer_open: bool,
    image_viewer_download_message: Option<String>,
    guild_leader_action_open: bool,
    channel_leader_action_open: bool,
    member_leader_action_open: bool,
    voice_leader_action_open: bool,
    options_open: bool,
    options_title: &'static str,
    selected_option: Option<usize>,
    display_options: config::DisplayOptions,
    notification_options: config::NotificationOptions,
    voice_options: config::VoiceOptions,
    emoji_picker_open: bool,
    reaction_users_open: bool,
    poll_vote_picker_open: bool,
    user_profile_open: bool,
    debug_log_open: bool,
    user_profile_data: DebugSignature,
    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,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct DebugSignature(u64);

#[derive(Clone, Debug, Eq, PartialEq)]
struct ChannelSwitcherItemSignature {
    channel_id: Id<ChannelMarker>,
    group_label: String,
    parent_label: Option<String>,
    channel_label: String,
    depth: usize,
    unread: ChannelUnreadState,
    unread_message_count: usize,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct GuildEntrySignature {
    row: DebugSignature,
    unread_count: Option<usize>,
    unread_state: Option<ChannelUnreadState>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct ChannelEntrySignature {
    row: DebugSignature,
    unread: Option<ChannelUnreadState>,
    unread_message_count: Option<usize>,
}

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());
    let channel_switcher_items = if state.is_channel_switcher_open() {
        state.channel_switcher_items()
    } else {
        Vec::new()
    };
    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_result_count: channel_switcher_items.len(),
        channel_switcher_items: channel_switcher_item_signature(&channel_switcher_items),
        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(),
        composer_mention_query: state.composer_mention_query().map(str::to_owned),
        composer_mention_selected: state.composer_mention_selected(),
        composer_mention_candidates: state
            .composer_mention_candidates()
            .into_iter()
            .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(),
        popups: VisiblePopupSignature {
            message_action_open: state.is_message_action_menu_open(),
            image_viewer_open: state.is_image_viewer_open(),
            image_viewer_download_message: state.image_viewer_download_message().map(str::to_owned),
            guild_leader_action_open: state.is_guild_leader_action_active(),
            channel_leader_action_open: state.is_channel_leader_action_active(),
            member_leader_action_open: state.is_member_leader_action_active(),
            voice_leader_action_open: state.is_voice_leader_action_active(),
            options_open: state.is_options_popup_open(),
            options_title: state.options_popup_title(),
            selected_option: state.selected_option_index(),
            display_options: state.display_options(),
            notification_options: state.notification_options(),
            voice_options: state.voice_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: debug_signature(&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 => GuildEntrySignature {
                    row: debug_signature(&entry),
                    unread_count: Some(state.direct_message_unread_count()),
                    unread_state: None,
                },
                state::GuildPaneEntry::Guild { state: guild, .. } => GuildEntrySignature {
                    row: debug_signature(&entry),
                    unread_count: None,
                    unread_state: Some(state.sidebar_guild_unread(guild.id)),
                },
                state::GuildPaneEntry::FolderHeader { .. } => GuildEntrySignature {
                    row: debug_signature(&entry),
                    unread_count: None,
                    unread_state: None,
                },
            })
            .collect(),
        visible_channels: state
            .visible_channel_pane_entries()
            .into_iter()
            .map(|entry| match entry {
                state::ChannelPaneEntry::Channel { state: channel, .. } => ChannelEntrySignature {
                    row: debug_signature(&entry),
                    unread: Some(state.channel_unread(channel.id)),
                    unread_message_count: Some(state.channel_unread_message_count(channel.id)),
                },
                state::ChannelPaneEntry::VoiceParticipant { .. }
                | state::ChannelPaneEntry::CategoryHeader { .. } => ChannelEntrySignature {
                    row: debug_signature(&entry),
                    unread: None,
                    unread_message_count: None,
                },
            })
            .collect(),
        visible_messages: state
            .visible_messages()
            .into_iter()
            .map(debug_signature)
            .collect(),
        visible_forum_posts: state
            .visible_forum_post_items()
            .into_iter()
            .map(|post| debug_signature(&post))
            .collect(),
        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(),
    }
}

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.composer_mention_query == after.composer_mention_query
        && before.composer_mention_selected == after.composer_mention_selected
        && before.composer_mention_candidates == after.composer_mention_candidates
        && 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.composer_mention_query == after.composer_mention_query
        && before.composer_mention_selected == after.composer_mention_selected
        && before.composer_mention_candidates == after.composer_mention_candidates
        && 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(
    items: &[state::ChannelSwitcherItem],
) -> Vec<ChannelSwitcherItemSignature> {
    items
        .iter()
        .map(|item| ChannelSwitcherItemSignature {
            channel_id: item.channel_id,
            group_label: item.group_label.clone(),
            parent_label: item.parent_label.clone(),
            channel_label: item.channel_label.clone(),
            depth: item.depth,
            unread: item.unread,
            unread_message_count: item.unread_message_count,
        })
        .collect()
}

struct DebugSignatureWriter {
    hasher: DefaultHasher,
}

impl fmt::Write for DebugSignatureWriter {
    fn write_str(&mut self, value: &str) -> fmt::Result {
        self.hasher.write(value.as_bytes());
        Ok(())
    }
}

fn debug_signature<T: fmt::Debug>(value: &T) -> DebugSignature {
    let mut writer = DebugSignatureWriter {
        hasher: DefaultHasher::new(),
    };
    write!(&mut writer, "{value:?}").expect("writing into signature hasher cannot fail");
    DebugSignature(writer.hasher.finish())
}

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())
}