concord 2.5.10

A terminal user interface client for Discord
use std::collections::BTreeMap;

use fixtures::*;
use ratatui::text::Line;

use crate::{
    config::{DisplayOptions, NotificationOptions, UiStateOptions, VoiceOptions},
    discord::ids::{
        Id,
        marker::{ChannelMarker, ForumTagMarker, GuildMarker, MessageMarker, UserMarker},
    },
};
use unicode_width::UnicodeWidthStr;

use super::model::{ChannelBranch, GuildBranch};
use super::{
    ActiveGuildScope, AttachmentViewerItem, ChannelActionKind, ChannelPaneEntry, ComposerLock,
    DashboardState, FocusPane, GuildActionKind, GuildPaneEntry, MessageActionItem,
    MessageActionKind, SearchResultItem,
};
use crate::discord::test_builders::{MessageAckFixture, message_ack_event};
use crate::discord::{
    ActivityInfo, ActivityKind, AppCommand, AppEvent, AttachmentInfo, ChannelInfo,
    ChannelNotificationOverrideInfo, ChannelRecipientInfo, ChannelUnreadState,
    ChannelVisibilityStats, CustomEmojiInfo, DiscordState, DownloadAttachmentSource,
    EmbedFieldInfo, EmbedInfo, ForumTagInfo, GuildFolder, GuildMemberListItem,
    GuildMemberListOperation, GuildMemberListUpdateInfo, GuildNotificationSettingsInfo,
    MessageInfo, MessageKind, MessageReferenceInfo, MessageSearchPage, MessageSnapshotInfo,
    MessageState, MessageUpdateDispatchInfo, MessageUpdateEventFields, NotificationLevel,
    PermissionOverwriteInfo, PermissionOverwriteKind, PremiumTier, PresenceEventFields,
    PresenceStatus, ReactionEmoji, ReactionInfo, ReactionUserInfo, ReplyInfo, RoleInfo,
    SnapshotRevision, UserGuildSettingsInfo, UserProfileInfo, UserSettingsInfo,
    VoiceConnectionStatus, VoiceStateInfo,
};

macro_rules! assert_send_message_eq {
    ($actual:expr, $expected:expr $(, $($message:tt)+)?) => {{
        let mut actual = $actual;
        match &mut actual {
            Some(AppCommand::SendMessage { nonce, .. })
            | Some(AppCommand::SendTtsMessage { nonce, .. }) => *nonce = Id::new(1),
            _ => {}
        }
        assert_eq!(actual, $expected $(, $($message)+)?);
    }};
}

mod channel_switcher;
mod composer;
mod direct_messages;
mod emoji_reactions;
mod fixtures;
mod leader_actions;
mod members;
mod message_actions;
mod message_layout;
mod message_viewport;
mod notifications;
mod options_voice;
mod panes;
mod pinned_threads;
mod profiles;
mod read_state;
mod search;
mod threads;

fn message_rendered_height(
    message: &MessageState,
    content_width: usize,
    preview_width: u16,
    max_preview_height: u16,
) -> usize {
    DashboardState::new().message_rendered_height(
        message,
        content_width,
        preview_width,
        max_preview_height,
    )
}

fn profile_info(user_id: u64, guild_nick: Option<&str>) -> UserProfileInfo {
    UserProfileInfo {
        guild_nick: guild_nick.map(str::to_owned),
        ..UserProfileInfo::test(Id::new(user_id), format!("user-{user_id}"))
    }
}

fn notification_message_event(channel_id: Id<ChannelMarker>, content: &str) -> AppEvent {
    message_create_event(MessageCreateFixture {
        guild_id: Some(Id::new(1)),
        channel_id,
        message_id: Id::new(50),
        author_id: Id::new(99),
        content: Some(content.to_owned()),
        ..guild_message_create_fixture()
    })
}

fn direct_message_create_event(channel_id: Id<ChannelMarker>, message_id: u64) -> AppEvent {
    message_create_event(MessageCreateFixture {
        guild_id: None,
        channel_id,
        message_id: Id::new(message_id),
        author_id: Id::new(99),
        content: Some("hello from dm".to_owned()),
        ..guild_message_create_fixture()
    })
}

fn user_settings_update(folders: Vec<GuildFolder>) -> AppEvent {
    AppEvent::UserSettingsUpdate {
        settings: UserSettingsInfo {
            guild_folders: Some(folders),
            ..UserSettingsInfo::default()
        },
    }
}

fn user_guild_settings_init(settings: Vec<GuildNotificationSettingsInfo>) -> AppEvent {
    AppEvent::UserGuildSettingsInit {
        settings: settings
            .into_iter()
            .map(|notification_settings| UserGuildSettingsInfo {
                notification_settings,
                extra_fields: BTreeMap::new(),
            })
            .collect(),
    }
}

fn message_update_event(
    channel_id: Id<ChannelMarker>,
    message_id: Id<MessageMarker>,
    fields: MessageUpdateEventFields,
) -> AppEvent {
    AppEvent::MessageUpdateDispatch {
        update: MessageUpdateDispatchInfo {
            guild_id: None,
            channel_id,
            message_id,
            fields,
            extra_fields: BTreeMap::new(),
        },
    }
}

fn guild_member_list_counts_event(guild_id: Id<GuildMarker>, online: u32) -> AppEvent {
    AppEvent::GuildMemberListUpdate {
        update: GuildMemberListUpdateInfo {
            guild_id,
            list_id: None,
            member_count: None,
            online_count: Some(online),
            groups: Vec::new(),
            ops: Vec::new(),
            extra_fields: BTreeMap::new(),
        },
    }
}

fn drain_debounced_read_ack(state: &mut DashboardState) -> Vec<AppCommand> {
    state.drain_pending_commands()
}

fn apply_optimistic_ack_commands<C>(state: &mut DashboardState, commands: &[C])
where
    C: Clone,
    AppCommand: From<C>,
{
    for command in commands {
        match AppCommand::from(command.clone()) {
            AppCommand::AckChannel {
                channel_id,
                message_id,
            }
            | AppCommand::ScheduleAckChannel {
                channel_id,
                message_id,
            } => state.push_event(message_ack_event(MessageAckFixture {
                channel_id,
                message_id,
                ..MessageAckFixture::new()
            })),
            AppCommand::AckChannels { targets } => {
                for (channel_id, message_id) in targets {
                    state.push_event(message_ack_event(MessageAckFixture {
                        channel_id,
                        message_id,
                        ..MessageAckFixture::new()
                    }));
                }
            }
            _ => {}
        }
    }
}

fn clear_scheduled_read_ack(state: &mut DashboardState) {
    state.drain_pending_commands();
}

fn push_reply_message_with_attachments(
    state: &mut DashboardState,
    message_id: u64,
    author_id: u64,
    content: Option<&str>,
    attachments: Vec<AttachmentInfo>,
) {
    state.push_event(message_create_event(MessageCreateFixture {
        guild_id: Some(Id::new(1)),
        channel_id: Id::new(2),
        message_id: Id::new(message_id),
        author_id: Id::new(author_id),
        author: format!("user-{author_id}"),
        message_kind: MessageKind::new(19),
        reference: Some(MessageReferenceInfo {
            guild_id: Some(Id::new(1)),
            channel_id: Some(Id::new(2)),
            ..MessageReferenceInfo::test(Id::new(42))
        }),
        reply: Some(ReplyInfo {
            content: Some("original message".to_owned()),
            ..ReplyInfo::test("original")
        }),
        content: content.map(str::to_owned),
        attachments,
        ..guild_message_create_fixture()
    }));
}

fn channel_entry_names(state: &DashboardState) -> Vec<&str> {
    state
        .channel_pane_entries()
        .into_iter()
        .filter_map(|entry| match entry {
            ChannelPaneEntry::Channel { state, .. } | ChannelPaneEntry::Thread { state, .. } => {
                Some(state.name.as_str())
            }
            ChannelPaneEntry::CategoryHeader { .. } | ChannelPaneEntry::VoiceParticipant { .. } => {
                None
            }
        })
        .collect()
}

fn state_with_voice_channel_participant() -> DashboardState {
    let guild_id = Id::new(1);
    let category_id = Id::new(10);
    let voice_id = Id::new(11);
    let text_id = Id::new(12);
    let alice = Id::new(20);
    let mut state = DashboardState::new();

    state.push_event(crate::discord::test_builders::guild_create_event(
        GuildCreateFixture {
            channels: vec![
                category_channel_info(guild_id, category_id, "Channels", 0),
                ChannelInfo {
                    parent_id: Some(category_id),
                    owner_id: None,
                    ..voice_channel_info(guild_id, voice_id, "Lobby")
                },
                child_text_channel_info(guild_id, text_id, category_id, "general", 1),
            ],
            members: vec![member_with_username(alice, "Alice", "alice")],
            ..GuildCreateFixture::new(guild_id)
        },
    ));
    state.push_event(AppEvent::VoiceStateUpdate {
        state: voice_state(guild_id, Some(voice_id), alice),
    });
    state.confirm_selected_guild();
    state
}