Message sending
This commit is contained in:
parent
061276524a
commit
41b8756ac2
|
@ -12,7 +12,7 @@ async-trait = "0.1"
|
|||
crossbeam-channel = "0.4"
|
||||
dirs-next = "2.0"
|
||||
futures = "0.3"
|
||||
iced = { version = "0.2", features = ["debug", "tokio_old"] }
|
||||
iced = { version = "0.2", features = ["debug", "tokio_old", "image"] }
|
||||
iced_futures = "0.2"
|
||||
hostname = "0.3"
|
||||
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", rev = "e65915e" }
|
||||
|
@ -20,3 +20,4 @@ matrix-sdk-common-macros = { git = "https://github.com/matrix-org/matrix-rust-sd
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "0.2", features = ["sync"] }
|
||||
toml = "0.5"
|
||||
tracing-subscriber = { version = "0.2", features = ["parking_lot"] }
|
||||
|
|
|
@ -10,6 +10,8 @@ pub mod matrix;
|
|||
pub mod ui;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let config_dir = dirs::config_dir().unwrap().join("retrix");
|
||||
// Make sure config dir exists and is not accessible by other users.
|
||||
if !config_dir.is_dir() {
|
||||
|
|
118
src/matrix.rs
118
src/matrix.rs
|
@ -1,6 +1,13 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk::{
|
||||
events::AnyRoomEvent, events::AnySyncRoomEvent, identifiers::DeviceId, identifiers::UserId,
|
||||
reqwest::Url, Client, ClientConfig, LoopCtrl, SyncSettings,
|
||||
api::r0::{account::register::Request as RegistrationRequest, uiaa::AuthData},
|
||||
events::AnyRoomEvent,
|
||||
events::AnySyncRoomEvent,
|
||||
identifiers::DeviceId,
|
||||
identifiers::UserId,
|
||||
reqwest::Url,
|
||||
Client, ClientConfig, LoopCtrl, SyncSettings,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -25,6 +32,53 @@ impl From<Session> for matrix_sdk::Session {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn signup(
|
||||
username: &str,
|
||||
password: &str,
|
||||
server: &str,
|
||||
) -> Result<(Client, Session), Error> {
|
||||
let url = Url::parse(server)?;
|
||||
let client = client(url)?;
|
||||
|
||||
let mut request = RegistrationRequest::new();
|
||||
request.username = Some(username);
|
||||
request.password = Some(password);
|
||||
request.initial_device_display_name = Some("retrix");
|
||||
request.inhibit_login = false;
|
||||
|
||||
// Get UIAA session key
|
||||
let uiaa = match client.register(request.clone()).await {
|
||||
Err(e) => match e.uiaa_response().cloned() {
|
||||
Some(uiaa) => uiaa,
|
||||
None => return Err(anyhow::anyhow!("Missing UIAA response")),
|
||||
},
|
||||
Ok(_) => {
|
||||
return Err(anyhow::anyhow!("Missing UIAA response"));
|
||||
}
|
||||
};
|
||||
// Get the first step in the authentication flow (we're ignoring the rest)
|
||||
let stages = uiaa.flows.get(0);
|
||||
let kind = stages.and_then(|flow| flow.stages.get(0)).cloned();
|
||||
|
||||
// Set authentication data, fallback to password type
|
||||
request.auth = Some(AuthData::DirectRequest {
|
||||
kind: kind.as_deref().unwrap_or("m.login.password"),
|
||||
session: uiaa.session.as_deref(),
|
||||
auth_parameters: Default::default(),
|
||||
});
|
||||
|
||||
let response = client.register(request).await?;
|
||||
|
||||
let session = Session {
|
||||
access_token: response.access_token.unwrap(),
|
||||
user_id: response.user_id,
|
||||
device_id: response.device_id.unwrap(),
|
||||
homeserver: server.to_owned(),
|
||||
};
|
||||
|
||||
Ok((client, session))
|
||||
}
|
||||
|
||||
/// Login with credentials, creating a new authentication session
|
||||
pub async fn login(
|
||||
username: &str,
|
||||
|
@ -98,12 +152,13 @@ fn write_session(session: &Session) -> Result<(), Error> {
|
|||
|
||||
pub struct MatrixSync {
|
||||
client: matrix_sdk::Client,
|
||||
join: Option<tokio::task::JoinHandle<()>>,
|
||||
//id: String,
|
||||
}
|
||||
|
||||
impl MatrixSync {
|
||||
pub fn subscription(client: matrix_sdk::Client) -> iced::Subscription<AnyRoomEvent> {
|
||||
iced::Subscription::from_recipe(MatrixSync { client })
|
||||
iced::Subscription::from_recipe(MatrixSync { client, join: None })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,41 +191,48 @@ where
|
|||
}
|
||||
|
||||
fn stream(
|
||||
self: Box<Self>,
|
||||
mut self: Box<Self>,
|
||||
_input: iced_futures::BoxStream<I>,
|
||||
) -> iced_futures::BoxStream<Self::Output> {
|
||||
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||
let client = self.client.clone();
|
||||
tokio::task::spawn(async move {
|
||||
let join = tokio::task::spawn(async move {
|
||||
client
|
||||
.sync_with_callback(SyncSettings::new(), |response| async {
|
||||
for (room_id, room) in response.rooms.join {
|
||||
for event in room.timeline.events {
|
||||
if let Ok(event) = event.deserialize() {
|
||||
let room_id = room_id.clone();
|
||||
let event = match event {
|
||||
AnySyncRoomEvent::Message(e) => {
|
||||
AnyRoomEvent::Message(e.into_full_event(room_id))
|
||||
}
|
||||
AnySyncRoomEvent::State(e) => {
|
||||
AnyRoomEvent::State(e.into_full_event(room_id))
|
||||
}
|
||||
AnySyncRoomEvent::RedactedMessage(e) => {
|
||||
AnyRoomEvent::RedactedMessage(e.into_full_event(room_id))
|
||||
}
|
||||
AnySyncRoomEvent::RedactedState(e) => {
|
||||
AnyRoomEvent::RedactedState(e.into_full_event(room_id))
|
||||
}
|
||||
};
|
||||
sender.send(event).ok();
|
||||
.sync_with_callback(
|
||||
SyncSettings::new().timeout(Duration::from_secs(90)),
|
||||
|response| async {
|
||||
for (room_id, room) in response.rooms.join {
|
||||
for event in room.timeline.events {
|
||||
if let Ok(event) = event.deserialize() {
|
||||
let room_id = room_id.clone();
|
||||
let event = match event {
|
||||
AnySyncRoomEvent::Message(e) => {
|
||||
AnyRoomEvent::Message(e.into_full_event(room_id))
|
||||
}
|
||||
AnySyncRoomEvent::State(e) => {
|
||||
AnyRoomEvent::State(e.into_full_event(room_id))
|
||||
}
|
||||
AnySyncRoomEvent::RedactedMessage(e) => {
|
||||
AnyRoomEvent::RedactedMessage(
|
||||
e.into_full_event(room_id),
|
||||
)
|
||||
}
|
||||
AnySyncRoomEvent::RedactedState(e) => {
|
||||
AnyRoomEvent::RedactedState(e.into_full_event(room_id))
|
||||
}
|
||||
};
|
||||
sender.send(event).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LoopCtrl::Continue
|
||||
})
|
||||
LoopCtrl::Continue
|
||||
},
|
||||
)
|
||||
.await;
|
||||
println!("We stopped syncing!");
|
||||
});
|
||||
self.join = Some(join);
|
||||
Box::pin(receiver)
|
||||
}
|
||||
}
|
||||
|
|
187
src/ui.rs
187
src/ui.rs
|
@ -2,13 +2,13 @@ use std::collections::{BTreeMap, HashMap};
|
|||
|
||||
use iced::{
|
||||
text_input::{self, TextInput},
|
||||
Application, Button, Column, Command, Container, Element, Length, Row, Rule, Scrollable,
|
||||
Application, Button, Column, Command, Container, Element, Length, Radio, Row, Rule, Scrollable,
|
||||
Subscription, Text,
|
||||
};
|
||||
use matrix_sdk::{
|
||||
events::{
|
||||
room::message::MessageEventContent, AnyPossiblyRedactedSyncMessageEvent, AnyRoomEvent,
|
||||
AnySyncMessageEvent,
|
||||
room::message::MessageEventContent, AnyMessageEventContent,
|
||||
AnyPossiblyRedactedSyncMessageEvent, AnyRoomEvent, AnySyncMessageEvent,
|
||||
},
|
||||
identifiers::RoomId,
|
||||
};
|
||||
|
@ -26,6 +26,7 @@ pub enum Retrix {
|
|||
user: String,
|
||||
password: String,
|
||||
server: String,
|
||||
action: PromptAction,
|
||||
error: Option<String>,
|
||||
},
|
||||
AwaitLogin(std::time::Instant),
|
||||
|
@ -38,6 +39,10 @@ pub enum Retrix {
|
|||
messages: BTreeMap<RoomId, MessageEventContent>,
|
||||
selected: Option<RoomId>,
|
||||
room_scroll: iced::scrollable::State,
|
||||
message_scroll: iced::scrollable::State,
|
||||
message_input: iced::text_input::State,
|
||||
draft: String,
|
||||
send_button: iced::button::State,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -52,18 +57,28 @@ impl Retrix {
|
|||
user: String::new(),
|
||||
password: String::new(),
|
||||
server: String::new(),
|
||||
action: PromptAction::Login,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PromptAction {
|
||||
Login,
|
||||
Signup,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
// Login form messages
|
||||
SetUser(String),
|
||||
SetPassword(String),
|
||||
SetServer(String),
|
||||
SetAction(PromptAction),
|
||||
Login,
|
||||
Signup,
|
||||
// Auth result messages
|
||||
LoggedIn(matrix_sdk::Client, matrix::Session),
|
||||
LoginFailed(String),
|
||||
|
||||
|
@ -71,6 +86,8 @@ pub enum Message {
|
|||
ResetRooms(BTreeMap<RoomId, String>),
|
||||
SelectRoom(RoomId),
|
||||
Sync(AnyRoomEvent),
|
||||
SetMessage(String),
|
||||
SendMessage,
|
||||
}
|
||||
|
||||
impl Application for Retrix {
|
||||
|
@ -114,11 +131,13 @@ impl Application for Retrix {
|
|||
user,
|
||||
password,
|
||||
server,
|
||||
action,
|
||||
..
|
||||
} => match message {
|
||||
Message::SetUser(u) => *user = u,
|
||||
Message::SetPassword(p) => *password = p,
|
||||
Message::SetServer(s) => *server = s,
|
||||
Message::SetAction(a) => *action = a,
|
||||
Message::Login => {
|
||||
let user = user.clone();
|
||||
let password = password.clone();
|
||||
|
@ -132,6 +151,19 @@ impl Application for Retrix {
|
|||
},
|
||||
);
|
||||
}
|
||||
Message::Signup => {
|
||||
let user = user.clone();
|
||||
let password = password.clone();
|
||||
let server = server.clone();
|
||||
*self = Retrix::AwaitLogin(std::time::Instant::now());
|
||||
return Command::perform(
|
||||
async move { matrix::signup(&user, &password, &server).await },
|
||||
|result| match result {
|
||||
Ok((client, response)) => Message::LoggedIn(client, response),
|
||||
Err(e) => Message::LoginFailed(e.to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Retrix::AwaitLogin(_) => match message {
|
||||
|
@ -148,10 +180,14 @@ impl Application for Retrix {
|
|||
rooms: BTreeMap::new(),
|
||||
selected: None,
|
||||
room_scroll: Default::default(),
|
||||
message_scroll: Default::default(),
|
||||
message_input: Default::default(),
|
||||
buttons: Default::default(),
|
||||
messages: Default::default(),
|
||||
draft: String::new(),
|
||||
send_button: Default::default(),
|
||||
};
|
||||
let client = client.clone();
|
||||
/*let client = client.clone();
|
||||
return Command::perform(
|
||||
async move {
|
||||
let mut rooms = BTreeMap::new();
|
||||
|
@ -162,15 +198,48 @@ impl Application for Retrix {
|
|||
rooms
|
||||
},
|
||||
|rooms| Message::ResetRooms(rooms),
|
||||
);
|
||||
);*/
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Retrix::LoggedIn {
|
||||
rooms, selected, ..
|
||||
rooms,
|
||||
selected,
|
||||
draft,
|
||||
client,
|
||||
..
|
||||
} => match message {
|
||||
Message::ResetRooms(r) => *rooms = r,
|
||||
Message::SelectRoom(r) => *selected = Some(r),
|
||||
Message::Sync(event) => match event {
|
||||
AnyRoomEvent::Message(_message) => (),
|
||||
AnyRoomEvent::State(_state) => (),
|
||||
AnyRoomEvent::RedactedMessage(_message) => (),
|
||||
AnyRoomEvent::RedactedState(_state) => (),
|
||||
},
|
||||
Message::SetMessage(m) => *draft = m,
|
||||
Message::SendMessage => {
|
||||
let selected = selected.to_owned();
|
||||
let draft = draft.clone();
|
||||
let client = client.clone();
|
||||
return Command::perform(
|
||||
async move {
|
||||
client
|
||||
.room_send(
|
||||
&selected.unwrap(),
|
||||
AnyMessageEventContent::RoomMessage(
|
||||
MessageEventContent::text_plain(draft),
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
},
|
||||
|result| match result {
|
||||
Ok(_) => Message::SetMessage(String::new()),
|
||||
Err(e) => Message::SetMessage(format!("{:?}", e)),
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
};
|
||||
|
@ -187,11 +256,27 @@ impl Application for Retrix {
|
|||
user,
|
||||
password,
|
||||
server,
|
||||
action,
|
||||
error,
|
||||
} => {
|
||||
// Login form
|
||||
let mut content = Column::new()
|
||||
.width(500.into())
|
||||
.push(
|
||||
Row::new()
|
||||
.push(Radio::new(
|
||||
PromptAction::Login,
|
||||
"Login",
|
||||
Some(*action),
|
||||
Message::SetAction,
|
||||
))
|
||||
.push(Radio::new(
|
||||
PromptAction::Signup,
|
||||
"Sign up",
|
||||
Some(*action),
|
||||
Message::SetAction,
|
||||
)),
|
||||
)
|
||||
.push(Text::new("Username"))
|
||||
.push(TextInput::new(user_input, "Username", user, Message::SetUser).padding(5))
|
||||
.push(Text::new("Password"))
|
||||
|
@ -204,8 +289,22 @@ impl Application for Retrix {
|
|||
.push(
|
||||
TextInput::new(server_input, "Server", server, Message::SetServer)
|
||||
.padding(5),
|
||||
)
|
||||
.push(Button::new(login_button, Text::new("Login")).on_press(Message::Login));
|
||||
);
|
||||
let button = match *action {
|
||||
PromptAction::Login => {
|
||||
Button::new(login_button, Text::new("Login")).on_press(Message::Login)
|
||||
}
|
||||
PromptAction::Signup => {
|
||||
content = content.push(
|
||||
Text::new(
|
||||
"NB: Signup is very naively implemented, and prone to breaking",
|
||||
)
|
||||
.color([0.2, 0.2, 0.0]),
|
||||
);
|
||||
Button::new(login_button, Text::new("Sign up")).on_press(Message::Signup)
|
||||
}
|
||||
};
|
||||
content = content.push(button);
|
||||
if let Some(ref error) = error {
|
||||
content = content.push(Text::new(error).color([1.0, 0.0, 0.0]));
|
||||
}
|
||||
|
@ -234,8 +333,12 @@ impl Application for Retrix {
|
|||
Retrix::LoggedIn {
|
||||
client,
|
||||
room_scroll,
|
||||
message_scroll,
|
||||
message_input,
|
||||
send_button,
|
||||
buttons,
|
||||
selected,
|
||||
draft,
|
||||
..
|
||||
} => {
|
||||
let mut root_row = Row::new().width(Length::Fill).height(Length::Fill);
|
||||
|
@ -290,32 +393,66 @@ impl Application for Retrix {
|
|||
.padding(5)
|
||||
.push(Text::new(room.display_name()).size(25))
|
||||
.push(Rule::horizontal(2));
|
||||
let mut scroll = Scrollable::new(message_scroll)
|
||||
.scrollbar_width(2)
|
||||
.height(Length::Fill);
|
||||
for message in room.messages.iter() {
|
||||
if let AnyPossiblyRedactedSyncMessageEvent::Regular(event) = message {
|
||||
match event {
|
||||
AnySyncMessageEvent::RoomMessage(room_message) => {
|
||||
match &room_message.content {
|
||||
MessageEventContent::Text(text) => {
|
||||
let row = Row::new()
|
||||
.spacing(5)
|
||||
.push(
|
||||
Text::new(room_message.sender.localpart())
|
||||
.color([0.2, 0.2, 1.0]),
|
||||
if let AnySyncMessageEvent::RoomMessage(room_message) = event {
|
||||
match &room_message.content {
|
||||
MessageEventContent::Text(text) => {
|
||||
let row = Row::new()
|
||||
.spacing(5)
|
||||
.push(
|
||||
// Render senders disambiguated name or fallback to
|
||||
// mxid
|
||||
Text::new(
|
||||
room.joined_members
|
||||
.get(&room_message.sender)
|
||||
.map(|sender| sender.disambiguated_name())
|
||||
.unwrap_or(room_message.sender.to_string()),
|
||||
)
|
||||
.push(Text::new(&text.body).width(Length::Fill))
|
||||
.push(Text::new(format_systime(
|
||||
room_message.origin_server_ts,
|
||||
)));
|
||||
col = col.push(row);
|
||||
}
|
||||
_ => (),
|
||||
.color([0.2, 0.2, 1.0]),
|
||||
)
|
||||
.push(Text::new(&text.body).width(Length::Fill))
|
||||
.push(Text::new(format_systime(
|
||||
room_message.origin_server_ts,
|
||||
)));
|
||||
scroll = scroll.push(row);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
col = col.push(scroll).push(
|
||||
Row::new()
|
||||
.push(
|
||||
TextInput::new(
|
||||
message_input,
|
||||
"Write a message...",
|
||||
draft,
|
||||
Message::SetMessage,
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(5)
|
||||
.on_submit(Message::SendMessage),
|
||||
)
|
||||
.push(
|
||||
Button::new(send_button, Text::new("Send"))
|
||||
.on_press(Message::SendMessage),
|
||||
),
|
||||
);
|
||||
|
||||
root_row = root_row.push(col);
|
||||
} else {
|
||||
root_row = root_row.push(
|
||||
Container::new(Text::new("Select a room to start chatting"))
|
||||
.center_x()
|
||||
.center_y()
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
);
|
||||
}
|
||||
|
||||
root_row.into()
|
||||
|
|
Loading…
Reference in a new issue