initial commit
This commit is contained in:
commit
7c0563240c
|
@ -0,0 +1 @@
|
|||
/target
|
|
@ -0,0 +1,4 @@
|
|||
[language-server.rust-analyzer]
|
||||
command = "rust-analyzer"
|
||||
environment = { "DATABASE_URL" = "sqlite://luz.db" }
|
||||
config = { cargo.features = "all" }
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "macaw"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
iced = { version = "0.13.1", features = ["tokio"] }
|
||||
luz = { version = "0.1.0", path = "../luz/luz" }
|
||||
jid = { version = "0.1.0", path = "../luz/jid" }
|
||||
tokio = "1.43.0"
|
||||
tokio-stream = "0.1.17"
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing = "0.1.41"
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# components
|
||||
|
||||
## basic building blocks: can use iced widgets for now
|
||||
|
||||
- button
|
||||
- search box (1 line)
|
||||
- bubble selections/tab bar (for filters)
|
||||
- pop-up menu
|
||||
- text input box
|
||||
- pop-up/overlay
|
||||
- selectable
|
||||
- switches
|
||||
- tooltip (for when hover over icon and need description text)
|
||||
- dialog
|
||||
- image
|
||||
- slider
|
||||
- checkbox
|
||||
- radio
|
||||
- image
|
||||
- menu bar
|
||||
|
||||
## components
|
||||
|
||||
- log in menu (for first time connection)
|
||||
- error toast
|
||||
- user info, click for menu to set status, switch accounts
|
||||
- profile icon (3 styles: user, guild, group)
|
||||
- contact list
|
||||
- contact list item
|
||||
- pinnable shortcuts bar
|
||||
- chats list (configurable: what's included, ordering, filtering, preview length)
|
||||
- dms
|
||||
- groups
|
||||
- guilds
|
||||
- chats list item
|
||||
- message view
|
||||
- user list (for group chats, channels)
|
||||
- scrollable menu
|
||||
- separated navigation bar (to put in contacts, guilds, spaces)
|
||||
- settings page (not very customizable)
|
||||
- profile settings/customisation
|
||||
- keymap settings
|
||||
- privacy settings
|
||||
- contact pop-up
|
||||
- contact profile
|
||||
- dm info/settings
|
||||
- group chat info/settings
|
||||
- keyboard hints
|
||||
- notification inbox (for pings/mentions, pending messages)
|
||||
- pop-in sidebar
|
||||
- debug console window (for showing logs, and both showing incoming raw xmpp stanzas and sending raw xmpp stanzas)
|
||||
- service discovery
|
||||
- command execution
|
||||
|
||||
## temporary
|
||||
|
||||
- message
|
||||
|
||||
## later
|
||||
|
||||
- video widget
|
||||
- animated images
|
||||
- 3d model viewer (lol why not)
|
||||
- everything to do with voice and video
|
||||
- reactions
|
||||
- stuff to do with gifs, voice messages, etc.
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
- e2ee
|
||||
- default omemo and no mam for 1to1 and private group chats
|
||||
- default archiving and no e2ee for guilds and public group chats
|
||||
- configurable between e2e and no archiving or no e2e and archiving at any point
|
||||
- the default is omemo 2, but can also choose worse omemo for contacts who do not use the client
|
||||
- intention to either implement mls or omemo post quantum for later
|
||||
- moving of xmpp accounts, download of account data from server
|
||||
- proper oauth
|
||||
- guilds
|
||||
- voice channels
|
||||
- video streaming
|
||||
- broadcast rooms
|
||||
- theme parts
|
||||
- main application style (component styles) done through setting variables to do with components
|
||||
- may include assets for borders, backgrounds, etc.
|
||||
- icon themes
|
||||
- app icon
|
||||
- emojis
|
||||
- status icons
|
||||
- general icons (e.g. call button, send message, etc)
|
||||
- menu bar icons
|
||||
- sound theme
|
||||
- message style (through custom blitz render), html and css templates
|
||||
- much later: main application layout, done through editing an xml file with a custom schema
|
||||
- adium+-level themeability
|
||||
- ichat theme
|
||||
- window layout customisation as part of theme
|
||||
- media chat features:
|
||||
- investigate whether it is worth encrypting all media chats using dtls-srtp
|
||||
- ichat photo booth video chat filters
|
||||
- webcam settings on a per-camera basis
|
||||
- crop, exposure, focus, white balance, etc. settings
|
||||
- mirroring in preview
|
||||
- ability for multiple cameras and streams per person, i.e. for video of desk and of face, stream desktop, desktop audio and mic audio all at same time + even more if needed
|
||||
- display status of chat, if sfu, if p2p, etc.
|
||||
- posting-like channels designed for proper broadcast, with commenting (like telegram)
|
||||
- importing and management of chat history
|
||||
- perhaps splitting chat history editing into a separate application?
|
||||
- in-app sticker/emoji creator/publisher
|
||||
- disappearing messages (with proper disclaimers, notify when contract is broken and somebody joins with a client that doesn't support them, etc., maybe when enabled, don't use message bodies, so clients that don't support them don't store them. or instead send like a 'this message was sent with the expectation it would be deleted, but your client doesn't support this', like omemo)
|
||||
- spaces, for organising guilds and chats together (like folders)
|
||||
- chat tabs/split views (select window, open chat. right click, open in new tab, new panel or new window, etc.). can enable or disable for advanced view. ability to maximize and fullscreen chat panels.
|
||||
- broadcasting status as looking for game/chat
|
||||
- broadcast/connect music streaming
|
||||
- advanced transports to different networks like irc, discord, whatsapp and telegram (access to hosted versions of which would be premium feature potentially)
|
||||
- broadcast your profile's theme: fonts, sounds, colors and padding etc
|
||||
- profile badges (including supporter tag, but also allowing custom badges from guilds, friends, etc. some kind of system to add badges sent to you from others to your profile, or automatically through guild menus)
|
||||
- as an extension, clans/clan tags?
|
||||
- greylisting
|
||||
- vouches as an anti-spam measure
|
||||
- fallback/backup servers through friends
|
||||
- composable moderation
|
||||
- subscriptions to labelers (like bsky)
|
||||
- advanced privacy settings
|
||||
- allow certain profile information to only be seen by certain people, in certain contexts
|
||||
- how open your inbox is
|
||||
- profile styling/customization
|
||||
- profile colors, fonts, background, profile border, effects
|
||||
- message styling?
|
||||
- per channel/guild styling and privacy settings
|
||||
- guild/channel level styling
|
||||
- pinned messages
|
||||
- mam extension for non-e2e channels
|
||||
- non-e2e channels rely on mam first and foremost to save client storage
|
||||
- encrypted message history sync across devices
|
||||
- polls
|
||||
- server admin menu
|
||||
- (this one is so stupid) caw button, akin to a poke feature, as a way to nudge if people are available to hang out, or to announce you are available to hang out (can configure who, in this case, would be notified)
|
||||
- live encrypted location sharing (requires mobile app)
|
||||
- (taler) payments
|
||||
- consider research of encryption of private server-side user data such as roster, settings, etc. (would require changing how things such as presence work)
|
||||
- media hashing
|
||||
- possible guild formats:
|
||||
- guild at guild.example.org, channels at example@guild.example.org, users at guild.example.org/user and example@guild.example.org/user
|
||||
- guilds at guild@guilds.example.org, channels at guild#channel@guilds.example.org or channel#guild@guilds.example.org or #channel%guild@guilds.example.org
|
||||
- guilds as a special type of mix channel, guild@mix.example.org, rest of channels at channel@mix.example.org
|
||||
- guilds at guild@guilds.example.org, channels at channel@mix.example.org (probably won't work)
|
||||
- proper rtl, ttb and bbt text rendering which switches depending on xml message language.
|
|
@ -0,0 +1,204 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use iced::futures::{SinkExt, Stream, StreamExt};
|
||||
use iced::widget::{button, column, row, text, text_input};
|
||||
use iced::{stream, Element, Subscription, Task, Theme};
|
||||
use jid::JID;
|
||||
use luz::chat::{Chat, Message as ChatMessage};
|
||||
use luz::presence::{Offline, Presence};
|
||||
use luz::CommandMessage;
|
||||
use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage};
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Macaw {
|
||||
client: Option<LuzHandle>,
|
||||
roster: HashMap<JID, Contact>,
|
||||
users: HashMap<JID, User>,
|
||||
presences: HashMap<JID, Presence>,
|
||||
chats: HashMap<JID, (Chat, Vec<ChatMessage>)>,
|
||||
subscription_requests: HashSet<JID>,
|
||||
connection_status: Option<Presence>,
|
||||
}
|
||||
|
||||
fn main() -> iced::Result {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
iced::application("Macaw", Macaw::update, Macaw::view)
|
||||
.subscription(Macaw::subscription)
|
||||
.run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
ClientCreated(LuzHandle),
|
||||
Luz(UpdateMessage),
|
||||
Roster(HashMap<JID, Contact>),
|
||||
Connect,
|
||||
Disconnect,
|
||||
OpenChat(JID),
|
||||
}
|
||||
|
||||
impl Macaw {
|
||||
fn stream() -> impl Stream<Item = Message> {
|
||||
stream::channel(100, |mut output| async {
|
||||
let (luz, recv) = LuzHandle::new(
|
||||
"test@blos.sm".try_into().unwrap(),
|
||||
"slayed".to_string(),
|
||||
"./macaw.db",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
output.send(Message::ClientCreated(luz)).await;
|
||||
let stream = ReceiverStream::new(recv);
|
||||
let stream = stream.map(|message| Message::Luz(message)).map(Ok);
|
||||
stream.forward(output).await;
|
||||
})
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::run(Macaw::stream)
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Luz(update_message) => match update_message {
|
||||
UpdateMessage::Error(error) => {
|
||||
tracing::error!("Luz error: {:?}", error);
|
||||
Task::none()
|
||||
}
|
||||
UpdateMessage::Online(online, vec) => {
|
||||
self.connection_status = Some(Presence::Online(online));
|
||||
let mut roster = HashMap::new();
|
||||
for contact in vec {
|
||||
roster.insert(contact.user_jid.clone(), contact);
|
||||
}
|
||||
self.roster = roster;
|
||||
Task::none()
|
||||
}
|
||||
UpdateMessage::Offline(offline) => {
|
||||
self.connection_status = Some(Presence::Offline(offline));
|
||||
Task::none()
|
||||
}
|
||||
UpdateMessage::FullRoster(vec) => {
|
||||
let mut macaw_roster = HashMap::new();
|
||||
for contact in vec {
|
||||
macaw_roster.insert(contact.user_jid.clone(), contact);
|
||||
}
|
||||
self.roster = macaw_roster;
|
||||
Task::none()
|
||||
}
|
||||
UpdateMessage::RosterUpdate(contact) => {
|
||||
self.roster.insert(contact.user_jid.clone(), contact);
|
||||
Task::none()
|
||||
}
|
||||
UpdateMessage::RosterDelete(jid) => {
|
||||
self.roster.remove(&jid);
|
||||
Task::none()
|
||||
}
|
||||
UpdateMessage::Presence { from, presence } => {
|
||||
self.presences.insert(from, presence);
|
||||
Task::none()
|
||||
}
|
||||
UpdateMessage::Message { to, message } => {
|
||||
if let Some((_chat, message_history)) = self.chats.get_mut(&to) {
|
||||
message_history.push(message);
|
||||
} else {
|
||||
let chat = Chat {
|
||||
correspondent: to.clone(),
|
||||
};
|
||||
let message_history = vec![message];
|
||||
self.chats.insert(to, (chat, message_history));
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
UpdateMessage::SubscriptionRequest(jid) => {
|
||||
// TODO: subscription requests
|
||||
Task::none()
|
||||
}
|
||||
},
|
||||
Message::ClientCreated(luz_handle) => {
|
||||
let cloned: LuzHandle = luz_handle.clone();
|
||||
self.client = Some(cloned);
|
||||
let (send, recv) = oneshot::channel();
|
||||
Task::perform(
|
||||
async move {
|
||||
luz_handle.send(CommandMessage::GetRoster(send)).await;
|
||||
recv.await
|
||||
},
|
||||
|result| {
|
||||
let roster = result.unwrap().unwrap();
|
||||
let mut macaw_roster = HashMap::new();
|
||||
for contact in roster {
|
||||
macaw_roster.insert(contact.user_jid.clone(), contact);
|
||||
}
|
||||
Message::Roster(macaw_roster)
|
||||
},
|
||||
)
|
||||
}
|
||||
Message::Roster(hash_map) => {
|
||||
self.roster = hash_map;
|
||||
Task::none()
|
||||
}
|
||||
Message::Connect => {
|
||||
let client = self.client.clone();
|
||||
Task::future(async move {
|
||||
client.clone().unwrap().send(CommandMessage::Connect).await;
|
||||
})
|
||||
.discard()
|
||||
}
|
||||
Message::Disconnect => {
|
||||
let client = self.client.clone();
|
||||
Task::future(async move {
|
||||
client
|
||||
.clone()
|
||||
.unwrap()
|
||||
.send(CommandMessage::Disconnect(Offline::default()))
|
||||
.await;
|
||||
})
|
||||
.discard()
|
||||
}
|
||||
Message::OpenChat(jid) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let mut contacts: Vec<Element<Message>> = Vec::new();
|
||||
for (_, contact) in &self.roster {
|
||||
contacts.push(
|
||||
button(match &contact.user_jid.localpart {
|
||||
Some(u) => u,
|
||||
None => "no username",
|
||||
})
|
||||
.on_press(Message::OpenChat(contact.user_jid.clone()))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let column = column(contacts);
|
||||
let connection_status = match &self.connection_status {
|
||||
Some(s) => match s {
|
||||
Presence::Online(online) => "connected",
|
||||
Presence::Offline(offline) => "disconnected",
|
||||
},
|
||||
None => "no account",
|
||||
};
|
||||
column![
|
||||
row![
|
||||
text("test@blos.sm"),
|
||||
text(connection_status),
|
||||
button("connect").on_press(Message::Connect),
|
||||
button("disconnect").on_press(Message::Disconnect)
|
||||
],
|
||||
text("Buddy List:"),
|
||||
//
|
||||
//
|
||||
column,
|
||||
]
|
||||
.into()
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
Theme::Dark
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue