feat: activity status persistence, voice-node leave on disconnect, guild bar, chat context menu, settings/identity UI
This commit is contained in:
parent
03d94e0465
commit
49e1f38793
9 changed files with 862 additions and 136 deletions
190
src/main.rs
190
src/main.rs
|
|
@ -1,6 +1,5 @@
|
|||
mod ui;
|
||||
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
|
@ -68,7 +67,9 @@ fn main() -> Result<()> {
|
|||
let mut s = state.borrow_mut();
|
||||
crate::ui::update::process_net_events(&mut s);
|
||||
let own_id = s.identity.short_id().to_string();
|
||||
crate::ui::bridge::sync_ui(&win, &s.ui, &own_id);
|
||||
let own_nick = s.identity.nickname.clone();
|
||||
let own_pubkey = s.identity.pubkey_hex.clone();
|
||||
crate::ui::bridge::sync_ui(&win, &s.ui, &own_id, &own_nick, &own_pubkey);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -163,6 +164,25 @@ fn wire_signals(window: &MainWindow, state: &Rc<RefCell<AppState>>) {
|
|||
return;
|
||||
}
|
||||
let mut s = state.borrow_mut();
|
||||
|
||||
if let Some(edit_id) = s.ui.editing_message_id.take() {
|
||||
let net = s.net.clone();
|
||||
s.ui.chat_input = String::new();
|
||||
drop(s);
|
||||
tokio::spawn(async move {
|
||||
net.send(NetCommand::MessageEdit {
|
||||
channel_id: ch_id,
|
||||
message_id: edit_id,
|
||||
content: text,
|
||||
})
|
||||
.await;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let reply_to = s.ui.replying_to_message.take();
|
||||
s.ui.replying_to_sender.clear();
|
||||
s.ui.replying_to_content.clear();
|
||||
s.ui.chat_input = String::new();
|
||||
let net = s.net.clone();
|
||||
drop(s);
|
||||
|
|
@ -172,7 +192,7 @@ fn wire_signals(window: &MainWindow, state: &Rc<RefCell<AppState>>) {
|
|||
net.send(NetCommand::SendChat {
|
||||
channel_id: ch_id,
|
||||
content: text,
|
||||
reply_to: None,
|
||||
reply_to,
|
||||
})
|
||||
.await;
|
||||
});
|
||||
|
|
@ -332,17 +352,169 @@ fn wire_signals(window: &MainWindow, state: &Rc<RefCell<AppState>>) {
|
|||
}
|
||||
|
||||
{
|
||||
let _state = state.clone();
|
||||
window.on_set_passphrase(move |_pass| {});
|
||||
let state = state.clone();
|
||||
window.on_set_passphrase(move |pass, confirm| {
|
||||
let mut s = state.borrow_mut();
|
||||
if pass != confirm {
|
||||
s.ui.settings_passphrase_error =
|
||||
Some("Passphrases do not match".to_string());
|
||||
return;
|
||||
}
|
||||
if pass.len() < 8 {
|
||||
s.ui.settings_passphrase_error =
|
||||
Some("Passphrase must be at least 8 characters".to_string());
|
||||
return;
|
||||
}
|
||||
s.ui.settings_passphrase_error = None;
|
||||
let identity = s.identity.clone();
|
||||
drop(s);
|
||||
if let Err(e) = identity::save(&identity, Some(pass.as_str())) {
|
||||
let mut s = state.borrow_mut();
|
||||
s.ui.settings_passphrase_error = Some(format!("{e}"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let _state = state.clone();
|
||||
window.on_export_keyfile(move || {});
|
||||
let state = state.clone();
|
||||
window.on_remove_passphrase(move || {
|
||||
let s = state.borrow();
|
||||
let identity = s.identity.clone();
|
||||
drop(s);
|
||||
if let Err(e) = identity::save(&identity, None) {
|
||||
let mut s = state.borrow_mut();
|
||||
s.ui.settings_passphrase_error = Some(format!("{e}"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let _state = state.clone();
|
||||
window.on_import_keyfile(move || {});
|
||||
let state = state.clone();
|
||||
window.on_export_keyfile(move |pass, confirm| {
|
||||
let mut s = state.borrow_mut();
|
||||
if pass != confirm {
|
||||
s.ui.settings_export_error = Some("Passphrases do not match".to_string());
|
||||
return;
|
||||
}
|
||||
let passphrase = if pass.is_empty() { None } else { Some(pass.as_str()) };
|
||||
let identity = s.identity.clone();
|
||||
drop(s);
|
||||
match identity::export_keyfile(&identity, passphrase) {
|
||||
Ok(json) => {
|
||||
let mut s = state.borrow_mut();
|
||||
s.ui.settings_export_output = Some(json);
|
||||
s.ui.settings_export_error = None;
|
||||
}
|
||||
Err(e) => {
|
||||
let mut s = state.borrow_mut();
|
||||
s.ui.settings_export_error = Some(format!("{e}"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let state = state.clone();
|
||||
window.on_import_keyfile(move |input, pass| {
|
||||
let input = input.to_string();
|
||||
let passphrase = if pass.is_empty() { None } else { Some(pass.as_str()) };
|
||||
let mut s = state.borrow_mut();
|
||||
match identity::import_keyfile(&input, passphrase) {
|
||||
Ok(identity) => {
|
||||
if let Err(e) = identity::save(&identity, None) {
|
||||
s.ui.settings_import_error = Some(format!("{e}"));
|
||||
} else {
|
||||
s.ui.settings_import_error = None;
|
||||
// Will need restart to take full effect
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
s.ui.settings_import_error = Some(format!("{e}"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let state = state.clone();
|
||||
window.on_react(move |msg_id, emoji| {
|
||||
let msg_id = msg_id.to_string();
|
||||
let emoji = emoji.to_string();
|
||||
let s = state.borrow();
|
||||
let ch_id = s.ui.active_channel.clone().unwrap_or_default();
|
||||
let net = s.net.clone();
|
||||
drop(s);
|
||||
tokio::spawn(async move {
|
||||
net.send(NetCommand::ReactionAdd {
|
||||
channel_id: ch_id,
|
||||
message_id: msg_id,
|
||||
emoji,
|
||||
})
|
||||
.await;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let state = state.clone();
|
||||
window.on_reply_to_msg(move |msg_id| {
|
||||
let msg_id = msg_id.to_string();
|
||||
let mut s = state.borrow_mut();
|
||||
s.ui.replying_to_message = Some(msg_id.clone());
|
||||
if let Some(ch_id) = s.ui.active_channel.clone() {
|
||||
if let Some(msgs) = s.ui.messages.get(&ch_id) {
|
||||
if let Some(m) = msgs.iter().find(|m| m.message_id == msg_id) {
|
||||
let sender = m.sender_id.clone();
|
||||
let content = m.content.clone();
|
||||
s.ui.replying_to_sender = sender;
|
||||
s.ui.replying_to_content = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let state = state.clone();
|
||||
window.on_cancel_reply(move || {
|
||||
let mut s = state.borrow_mut();
|
||||
s.ui.replying_to_message = None;
|
||||
s.ui.replying_to_sender.clear();
|
||||
s.ui.replying_to_content.clear();
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let state = state.clone();
|
||||
window.on_edit_msg(move |msg_id| {
|
||||
let msg_id = msg_id.to_string();
|
||||
let mut s = state.borrow_mut();
|
||||
if let Some(ch_id) = s.ui.active_channel.clone() {
|
||||
if let Some(msgs) = s.ui.messages.get(&ch_id) {
|
||||
if let Some(m) = msgs.iter().find(|m| m.message_id == msg_id) {
|
||||
s.ui.chat_input = m.content.clone();
|
||||
s.ui.editing_message_id = Some(msg_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let state = state.clone();
|
||||
window.on_delete_msg(move |msg_id| {
|
||||
let msg_id = msg_id.to_string();
|
||||
let s = state.borrow();
|
||||
let ch_id = s.ui.active_channel.clone().unwrap_or_default();
|
||||
let net = s.net.clone();
|
||||
drop(s);
|
||||
tokio::spawn(async move {
|
||||
net.send(NetCommand::MessageDelete {
|
||||
channel_id: ch_id,
|
||||
message_id: msg_id,
|
||||
})
|
||||
.await;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,9 @@ fn set_members_state(window: &MainWindow, state: &UiState, ch_id: &str) {
|
|||
}
|
||||
}
|
||||
|
||||
fn sync_sidebar(window: &MainWindow, state: &UiState) {
|
||||
fn sync_sidebar(window: &MainWindow, state: &UiState, own_nickname: &str) {
|
||||
let guild_id = state.active_guild_id.as_deref();
|
||||
|
||||
let channels: Vec<ChannelItem> = state
|
||||
.channels
|
||||
.iter()
|
||||
|
|
@ -78,9 +80,18 @@ fn sync_sidebar(window: &MainWindow, state: &UiState) {
|
|||
let guilds: Vec<GuildItem> = state
|
||||
.guilds
|
||||
.iter()
|
||||
.map(|g| GuildItem {
|
||||
guild_id: SharedString::from(g.guild_id.as_str()),
|
||||
name: SharedString::from(g.name.as_str()),
|
||||
.map(|g| {
|
||||
let short = if g.name.len() >= 2 {
|
||||
g.name[..2].to_string()
|
||||
} else {
|
||||
g.name.clone()
|
||||
};
|
||||
GuildItem {
|
||||
guild_id: SharedString::from(g.guild_id.as_str()),
|
||||
name: SharedString::from(g.name.as_str()),
|
||||
short_name: SharedString::from(short),
|
||||
selected: Some(g.guild_id.as_str()) == guild_id,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
window.set_guilds(Rc::new(VecModel::from(guilds)).into());
|
||||
|
|
@ -92,8 +103,14 @@ fn sync_sidebar(window: &MainWindow, state: &UiState) {
|
|||
.collect();
|
||||
window.set_dm_conversations(Rc::new(VecModel::from(dms)).into());
|
||||
|
||||
window.set_username(SharedString::from("user"));
|
||||
window.set_username(SharedString::from(own_nickname));
|
||||
window.set_user_status(SharedString::from("online"));
|
||||
window.set_show_dms(guild_id.is_none() && !state.dm_conversations.is_empty());
|
||||
window.set_replying_to_message(SharedString::from(
|
||||
state.replying_to_message.as_deref().unwrap_or(""),
|
||||
));
|
||||
window.set_replying_to_sender(SharedString::from(state.replying_to_sender.as_str()));
|
||||
window.set_replying_to_content(SharedString::from(state.replying_to_content.as_str()));
|
||||
window.set_active_channel_id(SharedString::from(
|
||||
state.active_channel.as_deref().unwrap_or(""),
|
||||
));
|
||||
|
|
@ -246,8 +263,8 @@ pub fn sync_connect(window: &MainWindow, state: &UiState) {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn sync_settings(window: &MainWindow, state: &UiState) {
|
||||
window.set_identity_pubkey(SharedString::from(""));
|
||||
pub fn sync_settings(window: &MainWindow, state: &UiState, own_pubkey: &str) {
|
||||
window.set_identity_pubkey(SharedString::from(own_pubkey));
|
||||
window.set_vault_enabled(false);
|
||||
window.set_voice_input_device(SharedString::from(""));
|
||||
window.set_voice_output_device(SharedString::from(""));
|
||||
|
|
@ -255,13 +272,46 @@ pub fn sync_settings(window: &MainWindow, state: &UiState) {
|
|||
window.set_vad_mode(state.vad_mode as i32);
|
||||
window.set_volume(state.output_volume);
|
||||
window.set_active_tab(SharedString::from("audio"));
|
||||
window.set_settings_open(state.settings_open);
|
||||
|
||||
window.set_passphrase_modal_open(state.settings_passphrase_open);
|
||||
window.set_passphrase_input(SharedString::from(state.settings_passphrase_input.as_str()));
|
||||
window.set_passphrase_confirm(SharedString::from(
|
||||
state.settings_passphrase_confirm.as_str(),
|
||||
));
|
||||
window.set_passphrase_error(SharedString::from(
|
||||
state.settings_passphrase_error.as_deref().unwrap_or(""),
|
||||
));
|
||||
|
||||
window.set_export_modal_open(state.settings_export_open);
|
||||
window.set_export_pass_input(SharedString::from(state.settings_export_pass.as_str()));
|
||||
window.set_export_pass_confirm(SharedString::from(state.settings_export_confirm.as_str()));
|
||||
window.set_export_output(SharedString::from(
|
||||
state.settings_export_output.as_deref().unwrap_or(""),
|
||||
));
|
||||
window.set_export_error(SharedString::from(
|
||||
state.settings_export_error.as_deref().unwrap_or(""),
|
||||
));
|
||||
|
||||
window.set_import_modal_open(state.settings_import_open);
|
||||
window.set_import_keyfile_input(SharedString::from(state.settings_import_input.as_str()));
|
||||
window.set_import_pass_input(SharedString::from(state.settings_import_pass.as_str()));
|
||||
window.set_import_error(SharedString::from(
|
||||
state.settings_import_error.as_deref().unwrap_or(""),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn sync_ui(window: &MainWindow, state: &UiState, own_user_id: &str) {
|
||||
sync_sidebar(window, state);
|
||||
pub fn sync_ui(
|
||||
window: &MainWindow,
|
||||
state: &UiState,
|
||||
own_user_id: &str,
|
||||
own_nickname: &str,
|
||||
own_pubkey: &str,
|
||||
) {
|
||||
sync_sidebar(window, state, own_nickname);
|
||||
sync_chat(window, state, own_user_id);
|
||||
sync_friends(window, state);
|
||||
sync_voice(window, state);
|
||||
sync_connect(window, state);
|
||||
sync_settings(window, state);
|
||||
sync_settings(window, state, own_pubkey);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,56 @@ export struct MemberItem {
|
|||
status: string,
|
||||
}
|
||||
|
||||
component MessageActions {
|
||||
in-out property <bool> is-own;
|
||||
in-out property <string> msg-id;
|
||||
|
||||
callback react(string, string);
|
||||
callback reply-to-msg(string);
|
||||
callback edit-msg(string);
|
||||
callback delete-msg(string);
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: 4px;
|
||||
Rectangle {
|
||||
width: 24px; height: 24px; border-radius: 4px;
|
||||
background: Theme.bg-interactive;
|
||||
TouchArea { clicked => { react(msg-id, "👍"); } }
|
||||
Text { text: "👍"; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
Rectangle {
|
||||
width: 24px; height: 24px; border-radius: 4px;
|
||||
background: Theme.bg-interactive;
|
||||
TouchArea { clicked => { react(msg-id, "❤️"); } }
|
||||
Text { text: "❤️"; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
Rectangle {
|
||||
width: 24px; height: 24px; border-radius: 4px;
|
||||
background: Theme.bg-interactive;
|
||||
TouchArea { clicked => { react(msg-id, "😂"); } }
|
||||
Text { text: "😂"; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
Rectangle {
|
||||
width: 24px; height: 24px; border-radius: 4px;
|
||||
background: Theme.bg-interactive;
|
||||
TouchArea { clicked => { reply-to-msg(msg-id); } }
|
||||
Text { text: "↩"; color: Theme.text-secondary; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
if is-own : Rectangle {
|
||||
width: 24px; height: 24px; border-radius: 4px;
|
||||
background: Theme.bg-interactive;
|
||||
TouchArea { clicked => { edit-msg(msg-id); } }
|
||||
Text { text: "✏"; color: Theme.text-secondary; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
if is-own : Rectangle {
|
||||
width: 24px; height: 24px; border-radius: 4px;
|
||||
background: Theme.danger;
|
||||
TouchArea { clicked => { delete-msg(msg-id); } }
|
||||
Text { text: "✕"; color: Theme.text-primary; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component ChatPanel {
|
||||
in-out property <string> active-channel-id;
|
||||
in-out property <string> active-channel-name;
|
||||
|
|
@ -34,9 +84,17 @@ export component ChatPanel {
|
|||
in-out property <bool> is-dm;
|
||||
in-out property <bool> e2ee-enabled;
|
||||
in-out property <string> dm-partner-name;
|
||||
in-out property <string> replying-to-message;
|
||||
in-out property <string> replying-to-sender;
|
||||
in-out property <string> replying-to-content;
|
||||
|
||||
callback send-message(string, string);
|
||||
callback load-more(string);
|
||||
callback react(string, string);
|
||||
callback reply-to-msg(string);
|
||||
callback edit-msg(string);
|
||||
callback delete-msg(string);
|
||||
callback cancel-reply();
|
||||
|
||||
VerticalLayout {
|
||||
padding: 0;
|
||||
|
|
@ -63,16 +121,25 @@ export component ChatPanel {
|
|||
Flickable {
|
||||
VerticalLayout {
|
||||
for msg in messages: Rectangle {
|
||||
height: 60px;
|
||||
height: 72px;
|
||||
VerticalLayout {
|
||||
padding: 8px;
|
||||
spacing: 4px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
spacing: 2px;
|
||||
if msg.reply_to != "" : Text {
|
||||
text: "↳ " + msg.reply_to;
|
||||
color: Theme.text-muted;
|
||||
font-size: 11px;
|
||||
}
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
Text {
|
||||
text: msg.sender_name;
|
||||
color: Theme.accent;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
Text {
|
||||
text: msg.timestamp;
|
||||
|
|
@ -83,12 +150,59 @@ export component ChatPanel {
|
|||
Text {
|
||||
text: msg.content;
|
||||
wrap: word-wrap;
|
||||
color: Theme.text-primary;
|
||||
font-size: 14px;
|
||||
}
|
||||
HorizontalLayout {
|
||||
height: 22px;
|
||||
MessageActions {
|
||||
is-own: msg.is_own;
|
||||
msg-id: msg.message_id;
|
||||
react(id, emoji) => { root.react(id, emoji); }
|
||||
reply-to-msg(id) => { root.reply-to-msg(id); }
|
||||
edit-msg(id) => { root.edit-msg(id); }
|
||||
delete-msg(id) => { root.delete-msg(id); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if replying-to-message != "" : Rectangle {
|
||||
height: 32px;
|
||||
background: Theme.bg-elevated;
|
||||
HorizontalLayout {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
spacing: 8px;
|
||||
Rectangle {
|
||||
width: 3px;
|
||||
height: 20px;
|
||||
background: Theme.accent;
|
||||
border-radius: 2px;
|
||||
}
|
||||
Text {
|
||||
text: "Replying to " + replying-to-sender;
|
||||
color: Theme.text-secondary;
|
||||
font-size: 12px;
|
||||
}
|
||||
Text {
|
||||
text: replying-to-content;
|
||||
color: Theme.text-muted;
|
||||
font-size: 12px;
|
||||
horizontal-stretch: 1;
|
||||
overflow: elide;
|
||||
}
|
||||
Rectangle {
|
||||
width: 20px; height: 20px; border-radius: 4px;
|
||||
background: Theme.bg-interactive;
|
||||
TouchArea { clicked => { cancel-reply(); } }
|
||||
Text { text: "✕"; color: Theme.text-muted; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 48px;
|
||||
background: Theme.bg-surface;
|
||||
|
|
@ -98,6 +212,7 @@ export component ChatPanel {
|
|||
TextInput {
|
||||
text <=> input-text;
|
||||
horizontal-stretch: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
Rectangle {
|
||||
width: 64px;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ChatPanel, MessageItem, MemberItem } from "chat.slint";
|
|||
import { FriendsPanel, FriendItem } from "friends.slint";
|
||||
import { VoicePanel, VoiceMemberItem } from "voice.slint";
|
||||
import { BookmarkItem, ConnectScreen } from "connect.slint";
|
||||
import { SettingsWindow } from "settings.slint";
|
||||
|
||||
export component MainWindow inherits Window {
|
||||
in-out property <[ChannelItem]> channels;
|
||||
|
|
@ -13,6 +14,7 @@ export component MainWindow inherits Window {
|
|||
in-out property <string> user-status;
|
||||
in-out property <string> active-channel-id;
|
||||
in-out property <int> active-guild-idx;
|
||||
in-out property <bool> show-dms;
|
||||
|
||||
in-out property <string> active-channel-name;
|
||||
in-out property <[MessageItem]> messages;
|
||||
|
|
@ -24,6 +26,9 @@ export component MainWindow inherits Window {
|
|||
in-out property <bool> show-chat;
|
||||
in-out property <bool> show-friends;
|
||||
in-out property <bool> show-voice;
|
||||
in-out property <string> replying-to-message;
|
||||
in-out property <string> replying-to-sender;
|
||||
in-out property <string> replying-to-content;
|
||||
|
||||
in-out property <[FriendItem]> friends;
|
||||
in-out property <[FriendItem]> pending-requests;
|
||||
|
|
@ -52,6 +57,23 @@ export component MainWindow inherits Window {
|
|||
in-out property <int> vad-mode: 0;
|
||||
in-out property <float> volume: 100.0;
|
||||
in-out property <string> active-tab: "audio";
|
||||
in-out property <bool> settings-open;
|
||||
|
||||
in-out property <bool> passphrase-modal-open;
|
||||
in-out property <string> passphrase-input;
|
||||
in-out property <string> passphrase-confirm;
|
||||
in-out property <string> passphrase-error;
|
||||
|
||||
in-out property <bool> export-modal-open;
|
||||
in-out property <string> export-pass-input;
|
||||
in-out property <string> export-pass-confirm;
|
||||
in-out property <string> export-output;
|
||||
in-out property <string> export-error;
|
||||
|
||||
in-out property <bool> import-modal-open;
|
||||
in-out property <string> import-keyfile-input;
|
||||
in-out property <string> import-pass-input;
|
||||
in-out property <string> import-error;
|
||||
|
||||
callback channel-selected(string);
|
||||
callback guild-selected(int);
|
||||
|
|
@ -59,6 +81,11 @@ export component MainWindow inherits Window {
|
|||
callback disconnect();
|
||||
callback send-message(string, string);
|
||||
callback load-more(string);
|
||||
callback react(string, string);
|
||||
callback reply-to-msg(string);
|
||||
callback edit-msg(string);
|
||||
callback delete-msg(string);
|
||||
callback cancel-reply();
|
||||
callback add-friend(string);
|
||||
callback accept-friend(string);
|
||||
callback decline-friend(string);
|
||||
|
|
@ -71,9 +98,10 @@ export component MainWindow inherits Window {
|
|||
callback remove-bookmark(string);
|
||||
callback add-bookmark(string);
|
||||
callback close-settings();
|
||||
callback set-passphrase(string);
|
||||
callback export-keyfile();
|
||||
callback import-keyfile();
|
||||
callback set-passphrase(string, string);
|
||||
callback remove-passphrase();
|
||||
callback export-keyfile(string, string);
|
||||
callback import-keyfile(string, string);
|
||||
|
||||
min-width: 900px;
|
||||
min-height: 560px;
|
||||
|
|
@ -101,6 +129,7 @@ export component MainWindow inherits Window {
|
|||
user-status: root.user-status;
|
||||
active-channel-id: root.active-channel-id;
|
||||
active-guild-idx: root.active-guild-idx;
|
||||
show-dms: root.show-dms;
|
||||
channel-selected(id) => { root.channel-selected(id); }
|
||||
guild-selected(idx) => { root.guild-selected(idx); }
|
||||
dm-selected(id) => { root.dm-selected(id); }
|
||||
|
|
@ -116,8 +145,16 @@ export component MainWindow inherits Window {
|
|||
is-dm: root.is-dm;
|
||||
e2ee-enabled: root.e2ee-enabled;
|
||||
dm-partner-name: root.dm-partner-name;
|
||||
replying-to-message: root.replying-to-message;
|
||||
replying-to-sender: root.replying-to-sender;
|
||||
replying-to-content: root.replying-to-content;
|
||||
send-message(id, text) => { root.send-message(id, text); }
|
||||
load-more(id) => { root.load-more(id); }
|
||||
react(id, emoji) => { root.react(id, emoji); }
|
||||
reply-to-msg(id) => { root.reply-to-msg(id); }
|
||||
edit-msg(id) => { root.edit-msg(id); }
|
||||
delete-msg(id) => { root.delete-msg(id); }
|
||||
cancel-reply => { root.cancel-reply(); }
|
||||
}
|
||||
friends-panel := FriendsPanel {
|
||||
visible: show-friends;
|
||||
|
|
@ -143,5 +180,44 @@ export component MainWindow inherits Window {
|
|||
toggle-deafen => { root.toggle-deafen(); }
|
||||
leave-voice => { root.leave-voice(); }
|
||||
}
|
||||
|
||||
if root.settings-open : Rectangle {
|
||||
background: #00000080;
|
||||
TouchArea { }
|
||||
SettingsWindow {
|
||||
y: (parent.height - 480px) / 2;
|
||||
x: (parent.width - 640px) / 2;
|
||||
identity-pubkey: root.identity-pubkey;
|
||||
vault-enabled: root.vault-enabled;
|
||||
voice-input-device: root.voice-input-device;
|
||||
voice-output-device: root.voice-output-device;
|
||||
voice-bitrate: root.voice-bitrate;
|
||||
vad-mode: root.vad-mode;
|
||||
volume: root.volume;
|
||||
active-tab: root.active-tab;
|
||||
|
||||
passphrase-modal-open: root.passphrase-modal-open;
|
||||
passphrase-input: root.passphrase-input;
|
||||
passphrase-confirm: root.passphrase-confirm;
|
||||
passphrase-error: root.passphrase-error;
|
||||
|
||||
export-modal-open: root.export-modal-open;
|
||||
export-pass-input: root.export-pass-input;
|
||||
export-pass-confirm: root.export-pass-confirm;
|
||||
export-output: root.export-output;
|
||||
export-error: root.export-error;
|
||||
|
||||
import-modal-open: root.import-modal-open;
|
||||
import-keyfile-input: root.import-keyfile-input;
|
||||
import-pass-input: root.import-pass-input;
|
||||
import-error: root.import-error;
|
||||
|
||||
close => { root.close-settings(); }
|
||||
set-passphrase(pass, confirm) => { root.set-passphrase(pass, confirm); }
|
||||
remove-passphrase => { root.remove-passphrase(); }
|
||||
export-keyfile(pass, confirm) => { root.export-keyfile(pass, confirm); }
|
||||
import-keyfile(input, pass) => { root.import-keyfile(input, pass); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { Theme } from "theme.slint";
|
||||
import { Button } from "std-widgets.slint";
|
||||
import { Button, LineEdit } from "std-widgets.slint";
|
||||
|
||||
export component SettingsWindow inherits Window {
|
||||
title: "Settings";
|
||||
width: 720px;
|
||||
height: 520px;
|
||||
background: Theme.bg-base;
|
||||
export component SettingsWindow {
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
|
||||
in-out property <string> identity-pubkey;
|
||||
in-out property <bool> vault-enabled;
|
||||
|
|
@ -16,53 +14,181 @@ export component SettingsWindow inherits Window {
|
|||
in-out property <float> volume;
|
||||
in-out property <string> active-tab;
|
||||
|
||||
in-out property <bool> passphrase-modal-open;
|
||||
in-out property <string> passphrase-input;
|
||||
in-out property <string> passphrase-confirm;
|
||||
in-out property <string> passphrase-error;
|
||||
|
||||
in-out property <bool> export-modal-open;
|
||||
in-out property <string> export-pass-input;
|
||||
in-out property <string> export-pass-confirm;
|
||||
in-out property <string> export-output;
|
||||
in-out property <string> export-error;
|
||||
|
||||
in-out property <bool> import-modal-open;
|
||||
in-out property <string> import-keyfile-input;
|
||||
in-out property <string> import-pass-input;
|
||||
in-out property <string> import-error;
|
||||
|
||||
callback close();
|
||||
callback set-passphrase(string);
|
||||
callback export-keyfile();
|
||||
callback import-keyfile();
|
||||
callback set-passphrase(string, string);
|
||||
callback remove-passphrase();
|
||||
callback export-keyfile(string, string);
|
||||
callback import-keyfile(string, string);
|
||||
|
||||
VerticalLayout {
|
||||
HorizontalLayout {
|
||||
Button { text: "Audio"; clicked => { active-tab = "audio"; } }
|
||||
Button { text: "Network"; clicked => { active-tab = "network"; } }
|
||||
Button { text: "Identity"; clicked => { active-tab = "identity"; } }
|
||||
Button { text: "App"; clicked => { active-tab = "app"; } }
|
||||
}
|
||||
Rectangle {
|
||||
background: Theme.bg-base;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
border-width: 1px;
|
||||
border-color: Theme.bg-elevated;
|
||||
|
||||
if active-tab == "identity": VerticalLayout {
|
||||
Text { text: "Identity"; color: Theme.text-primary; font-size: 18px; }
|
||||
Text { text: "Public Key: " + identity-pubkey; color: Theme.text-secondary; }
|
||||
Button { text: vault-enabled ? "Remove Passphrase" : "Set Passphrase"; clicked => { set-passphrase(""); } }
|
||||
Button { text: "Export Keyfile"; clicked => { export-keyfile(); } }
|
||||
Button { text: "Import Keyfile"; clicked => { import-keyfile(); } }
|
||||
}
|
||||
VerticalLayout {
|
||||
padding: 0;
|
||||
spacing: 0;
|
||||
|
||||
if active-tab == "audio": VerticalLayout {
|
||||
Text { text: "Voice Settings"; color: Theme.text-primary; font-size: 18px; }
|
||||
Text { text: "Input: " + voice-input-device; color: Theme.text-secondary; }
|
||||
Text { text: "Output: " + voice-output-device; color: Theme.text-secondary; }
|
||||
HorizontalLayout {
|
||||
Text { text: "Bitrate:"; color: Theme.text-secondary; }
|
||||
Text { text: voice-bitrate; color: Theme.text-primary; }
|
||||
Rectangle {
|
||||
height: 44px;
|
||||
background: Theme.bg-surface;
|
||||
border-radius: 12px;
|
||||
HorizontalLayout {
|
||||
padding: 8px;
|
||||
spacing: 4px;
|
||||
Rectangle {
|
||||
height: 28px;
|
||||
background: active-tab == "audio" ? Theme.accent : Theme.bg-interactive;
|
||||
border-radius: Theme.radius;
|
||||
TouchArea { clicked => { active-tab = "audio"; } }
|
||||
Text { text: "Audio"; color: Theme.text-primary; font-size: 13px; padding-left: 12px; padding-right: 12px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 28px;
|
||||
background: active-tab == "network" ? Theme.accent : Theme.bg-interactive;
|
||||
border-radius: Theme.radius;
|
||||
TouchArea { clicked => { active-tab = "network"; } }
|
||||
Text { text: "Network"; color: Theme.text-primary; font-size: 13px; padding-left: 12px; padding-right: 12px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 28px;
|
||||
background: active-tab == "identity" ? Theme.accent : Theme.bg-interactive;
|
||||
border-radius: Theme.radius;
|
||||
TouchArea { clicked => { active-tab = "identity"; } }
|
||||
Text { text: "Identity"; color: Theme.text-primary; font-size: 13px; padding-left: 12px; padding-right: 12px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
HorizontalLayout { horizontal-stretch: 1; }
|
||||
Rectangle {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
border-radius: Theme.radius;
|
||||
background: Theme.bg-interactive;
|
||||
TouchArea { clicked => { close(); } }
|
||||
Text { text: "✕"; color: Theme.text-muted; font-size: 14px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalLayout {
|
||||
Text { text: "VAD Mode:"; color: Theme.text-secondary; }
|
||||
Text { text: vad-mode == 0 ? "PTT" : vad-mode == 1 ? "VAD" : "Always On"; color: Theme.text-primary; }
|
||||
|
||||
if active-tab == "audio" : VerticalLayout {
|
||||
padding: 16px;
|
||||
spacing: 12px;
|
||||
Text { text: "Voice Settings"; color: Theme.text-primary; font-size: 16px; font-weight: 600; }
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
Text { text: "Input Device:"; color: Theme.text-secondary; }
|
||||
Text { text: voice-input-device; color: Theme.text-primary; }
|
||||
}
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
Text { text: "Output Device:"; color: Theme.text-secondary; }
|
||||
Text { text: voice-output-device; color: Theme.text-primary; }
|
||||
}
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
Text { text: "Bitrate:"; color: Theme.text-secondary; }
|
||||
Text { text: voice-bitrate; color: Theme.text-primary; }
|
||||
}
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
Text { text: "Mode:"; color: Theme.text-secondary; }
|
||||
Text { text: vad-mode == 0 ? "Push to Talk" : vad-mode == 1 ? "Voice Activity" : "Always On"; color: Theme.text-primary; }
|
||||
}
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
Text { text: "Volume:"; color: Theme.text-secondary; }
|
||||
Text { text: volume; color: Theme.text-primary; }
|
||||
}
|
||||
}
|
||||
HorizontalLayout {
|
||||
Text { text: "Volume:"; color: Theme.text-secondary; }
|
||||
Text { text: volume; color: Theme.text-primary; }
|
||||
|
||||
if active-tab == "network" : VerticalLayout {
|
||||
padding: 16px;
|
||||
spacing: 12px;
|
||||
Text { text: "Network"; color: Theme.text-primary; font-size: 16px; font-weight: 600; }
|
||||
Text { text: "Connection settings will appear here."; color: Theme.text-muted; }
|
||||
}
|
||||
|
||||
if active-tab == "identity" : VerticalLayout {
|
||||
padding: 16px;
|
||||
spacing: 12px;
|
||||
Text { text: "Identity"; color: Theme.text-primary; font-size: 16px; font-weight: 600; }
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
Text { text: "Public Key:"; color: Theme.text-secondary; }
|
||||
Text { text: identity-pubkey; color: Theme.text-primary; font-family: "monospace"; }
|
||||
}
|
||||
Text { text: vault-enabled ? "Vault is encrypted" : "Vault is plaintext"; color: Theme.text-secondary; }
|
||||
|
||||
if !passphrase-modal-open && !export-modal-open && !import-modal-open : VerticalLayout {
|
||||
spacing: 8px;
|
||||
Rectangle { height: 28px; background: Theme.accent; border-radius: Theme.radius; TouchArea { clicked => { passphrase-modal-open = true; } } Text { text: vault-enabled ? "Change Passphrase" : "Set Passphrase"; color: Theme.text-primary; horizontal-alignment: center; vertical-alignment: center; } }
|
||||
if vault-enabled : Rectangle { height: 28px; background: Theme.danger; border-radius: Theme.radius; TouchArea { clicked => { remove-passphrase(); } } Text { text: "Remove Passphrase"; color: Theme.text-primary; horizontal-alignment: center; vertical-alignment: center; } }
|
||||
Rectangle { height: 28px; background: Theme.accent; border-radius: Theme.radius; TouchArea { clicked => { export-modal-open = true; } } Text { text: "Export Keyfile"; color: Theme.text-primary; horizontal-alignment: center; vertical-alignment: center; } }
|
||||
Rectangle { height: 28px; background: Theme.accent; border-radius: Theme.radius; TouchArea { clicked => { import-modal-open = true; } } Text { text: "Import Keyfile"; color: Theme.text-primary; horizontal-alignment: center; vertical-alignment: center; } }
|
||||
}
|
||||
|
||||
if passphrase-modal-open : VerticalLayout {
|
||||
spacing: 8px;
|
||||
Rectangle { height: 1px; background: Theme.bg-elevated; }
|
||||
Text { text: vault-enabled ? "Change Passphrase" : "Set Passphrase"; color: Theme.text-primary; font-weight: 600; }
|
||||
LineEdit { text <=> passphrase-input; placeholder-text: "New passphrase"; }
|
||||
LineEdit { text <=> passphrase-confirm; placeholder-text: "Confirm passphrase"; }
|
||||
if passphrase-error != "" : Text { text: passphrase-error; color: Theme.danger; font-size: 12px; }
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
Button { text: "Cancel"; clicked => { passphrase-modal-open = false; } }
|
||||
Button { text: "Save"; clicked => { set-passphrase(passphrase-input, passphrase-confirm); } }
|
||||
}
|
||||
}
|
||||
|
||||
if export-modal-open : VerticalLayout {
|
||||
spacing: 8px;
|
||||
Rectangle { height: 1px; background: Theme.bg-elevated; }
|
||||
Text { text: "Export Keyfile"; color: Theme.text-primary; font-weight: 600; }
|
||||
LineEdit { text <=> export-pass-input; placeholder-text: "Optional passphrase"; }
|
||||
LineEdit { text <=> export-pass-confirm; placeholder-text: "Confirm passphrase"; }
|
||||
if export-error != "" : Text { text: export-error; color: Theme.danger; font-size: 12px; }
|
||||
if export-output != "" : Text { text: export-output; color: Theme.text-muted; font-size: 11px; }
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
Button { text: "Cancel"; clicked => { export-modal-open = false; } }
|
||||
Button { text: "Generate"; clicked => { export-keyfile(export-pass-input, export-pass-confirm); } }
|
||||
}
|
||||
}
|
||||
|
||||
if import-modal-open : VerticalLayout {
|
||||
spacing: 8px;
|
||||
Rectangle { height: 1px; background: Theme.bg-elevated; }
|
||||
Text { text: "Import Keyfile"; color: Theme.text-primary; font-weight: 600; }
|
||||
Text { text: "Paste your keyfile JSON below:"; color: Theme.text-secondary; font-size: 12px; }
|
||||
TextInput { text <=> import-keyfile-input; height: 80px; }
|
||||
LineEdit { text <=> import-pass-input; placeholder-text: "Passphrase (if encrypted)"; }
|
||||
if import-error != "" : Text { text: import-error; color: Theme.danger; font-size: 12px; }
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
Button { text: "Cancel"; clicked => { import-modal-open = false; } }
|
||||
Button { text: "Import & Replace"; clicked => { import-keyfile(import-keyfile-input, import-pass-input); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if active-tab == "network": VerticalLayout {
|
||||
Text { text: "Network"; color: Theme.text-primary; font-size: 18px; }
|
||||
}
|
||||
|
||||
if active-tab == "app": VerticalLayout {
|
||||
Text { text: "Appearance"; color: Theme.text-primary; font-size: 18px; }
|
||||
}
|
||||
|
||||
Button { text: "Close"; horizontal-alignment: center; clicked => { close(); } }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,156 @@ export struct ChannelItem {
|
|||
export struct GuildItem {
|
||||
guild_id: string,
|
||||
name: string,
|
||||
short_name: string,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
component GuildBar {
|
||||
in-out property <[GuildItem]> guilds;
|
||||
in-out property <int> active-guild-idx;
|
||||
|
||||
callback guild-selected(int);
|
||||
|
||||
width: 56px;
|
||||
height: 100%;
|
||||
|
||||
Rectangle {
|
||||
background: #111118;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
VerticalLayout {
|
||||
padding: 8px;
|
||||
spacing: 4px;
|
||||
alignment: start;
|
||||
|
||||
Rectangle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: active-guild-idx == -1 ? 12px : 20px;
|
||||
background: active-guild-idx == -1 ? Theme.accent : Theme.bg-surface;
|
||||
TouchArea {
|
||||
clicked => { guild-selected(-1); }
|
||||
}
|
||||
Text {
|
||||
text: "@";
|
||||
color: Theme.text-primary;
|
||||
font-size: 18px;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 2px;
|
||||
width: 32px;
|
||||
background: Theme.bg-elevated;
|
||||
border-radius: 1px;
|
||||
x: 12px;
|
||||
}
|
||||
|
||||
for g[idx] in guilds: Rectangle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: g.selected ? 12px : 20px;
|
||||
background: g.selected ? Theme.accent : Theme.bg-elevated;
|
||||
TouchArea {
|
||||
clicked => { guild-selected(idx); }
|
||||
}
|
||||
Text {
|
||||
text: g.short_name;
|
||||
color: Theme.text-primary;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component ChannelList {
|
||||
in-out property <[ChannelItem]> channels;
|
||||
in-out property <string> active-channel-id;
|
||||
in-out property <string> section-title;
|
||||
|
||||
callback channel-selected(string);
|
||||
|
||||
VerticalLayout {
|
||||
Rectangle {
|
||||
height: 48px;
|
||||
background: Theme.bg-surface;
|
||||
HorizontalLayout {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
Text { text: section-title; color: Theme.text-primary; font-size: 15px; font-weight: 600; }
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
VerticalLayout {
|
||||
for item in channels: Rectangle {
|
||||
height: 36px;
|
||||
background: item.channel_id == active-channel-id ? Theme.bg-elevated : Theme.bg-base;
|
||||
HorizontalLayout {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
spacing: 6px;
|
||||
Text {
|
||||
text: item.kind == "voice" ? "🔊 " : "# ";
|
||||
color: Theme.text-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
Text {
|
||||
text: item.name;
|
||||
color: item.channel_id == active-channel-id ? Theme.text-primary : Theme.text-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
TouchArea {
|
||||
clicked => { channel-selected(item.channel_id); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component DmList {
|
||||
in-out property <[string]> dm-conversations;
|
||||
|
||||
callback dm-selected(string);
|
||||
|
||||
VerticalLayout {
|
||||
Rectangle {
|
||||
height: 48px;
|
||||
background: Theme.bg-surface;
|
||||
HorizontalLayout {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
Text { text: "Direct Messages"; color: Theme.text-primary; font-size: 15px; font-weight: 600; }
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
VerticalLayout {
|
||||
for dm in dm-conversations: Rectangle {
|
||||
height: 36px;
|
||||
HorizontalLayout {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
spacing: 6px;
|
||||
Text { text: "@"; color: Theme.text-secondary; font-size: 14px; }
|
||||
Text { text: dm; color: Theme.text-secondary; font-size: 14px; }
|
||||
}
|
||||
TouchArea {
|
||||
clicked => { dm-selected(dm); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component Sidebar {
|
||||
|
|
@ -19,71 +169,75 @@ export component Sidebar {
|
|||
in-out property <string> user-status;
|
||||
in-out property <string> active-channel-id;
|
||||
in-out property <int> active-guild-idx;
|
||||
in-out property <bool> show-dms;
|
||||
|
||||
callback channel-selected(string);
|
||||
callback guild-selected(int);
|
||||
callback dm-selected(string);
|
||||
callback disconnect();
|
||||
|
||||
width: 240px;
|
||||
width: 296px;
|
||||
height: 100%;
|
||||
|
||||
Rectangle {
|
||||
background: Theme.bg-base;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
HorizontalLayout {
|
||||
spacing: 0;
|
||||
|
||||
VerticalLayout {
|
||||
Rectangle {
|
||||
height: 48px;
|
||||
background: Theme.bg-surface;
|
||||
HorizontalLayout {
|
||||
padding: 8px;
|
||||
Text { text: "Vnox"; color: Theme.accent; font-size: 18px; }
|
||||
}
|
||||
}
|
||||
GuildBar {
|
||||
guilds: root.guilds;
|
||||
active-guild-idx: root.active-guild-idx;
|
||||
guild-selected(idx) => { root.guild-selected(idx); }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
background: Theme.bg-base;
|
||||
|
||||
VerticalLayout {
|
||||
for item in channels: Rectangle {
|
||||
height: 36px;
|
||||
background: item.channel_id == active-channel-id ? Theme.bg-elevated : Theme.bg-base;
|
||||
spacing: 0;
|
||||
|
||||
if root.show-dms : DmList {
|
||||
dm-conversations: root.dm-conversations;
|
||||
dm-selected(id) => { root.dm-selected(id); }
|
||||
}
|
||||
if !root.show-dms : ChannelList {
|
||||
channels: root.channels;
|
||||
active-channel-id: root.active-channel-id;
|
||||
section-title: root.active-guild-idx >= 0 && root.active-guild-idx < root.guilds.length ? root.guilds[root.active-guild-idx].name : "Channels";
|
||||
channel-selected(id) => { root.channel-selected(id); }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 52px;
|
||||
background: Theme.bg-surface;
|
||||
HorizontalLayout {
|
||||
padding: 8px;
|
||||
Text {
|
||||
text: item.kind == "voice" ? "🔊 " : "# ";
|
||||
color: Theme.text-secondary;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
spacing: 8px;
|
||||
Rectangle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
background: Theme.bg-elevated;
|
||||
}
|
||||
VerticalLayout {
|
||||
spacing: 2px;
|
||||
Text { text: root.username; color: Theme.text-primary; font-size: 14px; font-weight: 600; }
|
||||
Text { text: root.user-status; color: Theme.text-muted; font-size: 11px; }
|
||||
}
|
||||
HorizontalLayout {
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
Rectangle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: Theme.bg-interactive;
|
||||
TouchArea {
|
||||
clicked => { root.disconnect(); }
|
||||
}
|
||||
Text { text: "⏻"; color: Theme.text-muted; font-size: 14px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
Text { text: item.name; color: Theme.text-primary; }
|
||||
}
|
||||
TouchArea {
|
||||
clicked => { channel-selected(item.channel_id); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
for dm in dm-conversations: Rectangle {
|
||||
height: 36px;
|
||||
HorizontalLayout {
|
||||
padding: 8px;
|
||||
Text { text: "@"; color: Theme.text-secondary; }
|
||||
Text { text: dm; color: Theme.text-primary; }
|
||||
}
|
||||
TouchArea {
|
||||
clicked => { dm-selected(dm); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 52px;
|
||||
background: Theme.bg-surface;
|
||||
HorizontalLayout {
|
||||
padding: 8px;
|
||||
Text { text: username; color: Theme.text-primary; }
|
||||
Text { text: user-status; color: Theme.text-muted; }
|
||||
TouchArea {
|
||||
clicked => { disconnect(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,27 @@ impl Default for UiState {
|
|||
remote_speaking: false,
|
||||
last_remote_speaker_id: String::new(),
|
||||
last_voice_packet_ms: 0,
|
||||
|
||||
replying_to_message: None,
|
||||
replying_to_sender: String::new(),
|
||||
replying_to_content: String::new(),
|
||||
editing_message_id: None,
|
||||
|
||||
settings_passphrase_open: false,
|
||||
settings_passphrase_input: String::new(),
|
||||
settings_passphrase_confirm: String::new(),
|
||||
settings_passphrase_error: None,
|
||||
|
||||
settings_export_open: false,
|
||||
settings_export_pass: String::new(),
|
||||
settings_export_confirm: String::new(),
|
||||
settings_export_output: None,
|
||||
settings_export_error: None,
|
||||
|
||||
settings_import_open: false,
|
||||
settings_import_input: String::new(),
|
||||
settings_import_pass: String::new(),
|
||||
settings_import_error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,4 +85,25 @@ pub struct UiState {
|
|||
pub remote_speaking: bool,
|
||||
pub last_remote_speaker_id: String,
|
||||
pub last_voice_packet_ms: u64,
|
||||
|
||||
pub replying_to_message: Option<String>,
|
||||
pub replying_to_sender: String,
|
||||
pub replying_to_content: String,
|
||||
pub editing_message_id: Option<String>,
|
||||
|
||||
pub settings_passphrase_open: bool,
|
||||
pub settings_passphrase_input: String,
|
||||
pub settings_passphrase_confirm: String,
|
||||
pub settings_passphrase_error: Option<String>,
|
||||
|
||||
pub settings_export_open: bool,
|
||||
pub settings_export_pass: String,
|
||||
pub settings_export_confirm: String,
|
||||
pub settings_export_output: Option<String>,
|
||||
pub settings_export_error: Option<String>,
|
||||
|
||||
pub settings_import_open: bool,
|
||||
pub settings_import_input: String,
|
||||
pub settings_import_pass: String,
|
||||
pub settings_import_error: Option<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -588,11 +588,7 @@ fn social_handle(ui: &mut UiState, event: NetEvent) {
|
|||
})
|
||||
.collect();
|
||||
}
|
||||
NetEvent::GuildCreated {
|
||||
guild_id,
|
||||
name,
|
||||
..
|
||||
} => {
|
||||
NetEvent::GuildCreated { guild_id, name, .. } => {
|
||||
ui.guilds.push(crate::ui::state::GuildState {
|
||||
guild_id: guild_id.clone(),
|
||||
name,
|
||||
|
|
@ -656,18 +652,13 @@ fn social_handle(ui: &mut UiState, event: NetEvent) {
|
|||
}
|
||||
NetEvent::FriendRequested { user_id, nickname } => {
|
||||
ui.pending_friend_requests
|
||||
.push(crate::ui::state::FriendState {
|
||||
user_id,
|
||||
nickname,
|
||||
});
|
||||
.push(crate::ui::state::FriendState { user_id, nickname });
|
||||
}
|
||||
NetEvent::FriendAccepted { user_id, nickname } => {
|
||||
ui.pending_friend_requests.retain(|f| f.user_id != user_id);
|
||||
if !ui.friends.iter().any(|f| f.user_id == user_id) {
|
||||
ui.friends.push(crate::ui::state::FriendState {
|
||||
user_id,
|
||||
nickname,
|
||||
});
|
||||
ui.friends
|
||||
.push(crate::ui::state::FriendState { user_id, nickname });
|
||||
}
|
||||
}
|
||||
NetEvent::FriendRemoved { user_id } => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue