~nicohman/signal-rs

e9f3f6e2a7d62f07e30b5d44e6ee15b4cc872e6f — nicohman 8 months ago 37a5438
Add deleting messages
12 files changed, 254 insertions(+), 293 deletions(-)

M .gitignore
M qml/ChatHistory.qml
M qml/ChatList.qml
M qml/MessageUI.qml
M qml/SignalState.qml
M qml/main.qml
D src/base.css
M src/main.rs
M src/scli.rs
D src/signal.css
M src/signal.rs
D src/style.css
M .gitignore => .gitignore +2 -1
@@ 1,4 1,5 @@
build
.clickable
target
signal-cli
\ No newline at end of file
signal-cli
.cargo

M qml/ChatHistory.qml => qml/ChatHistory.qml +40 -40
@@ 14,50 14,50 @@ Page {
    }
    ColumnLayout {
        anchors.fill: parent
    ListView {
        Component.onCompleted: positionViewAtEnd()
        id: chatView
        x: 0
        y: 0
        anchors.left: parent.left
        Layout.fillWidth: true
        Layout.fillHeight: true
        Layout.rightMargin: 5
        delegate:
            MessageUI {
                messagetext: Message
                outgoing: Outgoing
                message: JsonMsg
        ListView {
            highlight: Rectangle {
                color: "transparent"
            }

        model: signalState.messagesModel
    }
    Row {
                Layout.fillWidth: true

    TextField {
        id: msgTextInput
        width: parent.width
        onAccepted: {
            chatHistory.sendMessage();
            Component.onCompleted: {
                chatView.positionViewAtEnd()
            }
            id: chatView
            x: 0
            y: 0
            anchors.left: parent.left
            Layout.fillWidth: true
            Layout.fillHeight: true
            Layout.rightMargin: 5
            delegate:
                MessageUI {
                    outgoing: outgoing
                    message: model
                }
            model: signal.messages
        }
    }
}
    Row {
                Layout.fillWidth: true

    Button {
        background: Rectangle {
            color:theme.button
        Row {
            Layout.fillWidth: true
            TextField {
                id: msgTextInput
                width: parent.width
                onAccepted: {
                    chatHistory.sendMessage();
                }
            }
        }
        Row {
            Layout.fillWidth: true
            Button {
                background: Rectangle {
                    color:theme.button
                }
                width: parent.width

        id: sendButton
        text: "Send Message"
        onClicked: {
            chatHistory.sendMessage();
                id: sendButton
                text: "Send Message"
                onClicked: {
                    chatHistory.sendMessage();
                }
            }
        }
    }
}
}
}

M qml/ChatList.qml => qml/ChatList.qml +20 -16
@@ 5,6 5,7 @@ Page {
    anchors.fill: stackView
    title: qsTr("Chats")
    objectName: "chats"
    property string avatar: ""
    ListView {
        id: chatView
        x: 0


@@ 12,50 13,53 @@ Page {
        anchors.fill: parent
        delegate: ItemDelegate {
            Component.onCompleted:{
                console.log("TEL"+tel);
                var avatar = signal.avatar(tel);
                if (avatar != "") {
                    Qt.createQmlObject(`import QtQuick 2.15; Image { width: 40\nheight:40\nsource:"file://${avatar}"}`, imgPlace, "chatAvatar");
                }
            }
            x: 5
            width: chatView.width
            height: 40
            onClicked: {
                console.log(tel);
                signalState.currentChat = model;
                signalState.setCurrent(tel);
                signal.show_chat(tel);
                signalState.messagesModel.clear();
                signalState.openView("chatHistory");
            }
            Row {
                id: row1
                spacing: 10
                bottomPadding: 20
                Item {
                    id: imgPlace
                    width: 40
                    height: 40
                    visible: signal.avatar(tel) != ""
                }
                Rectangle {
                    width: 40
                    height: 40
                    color: "grey"
                    radius: 50
                    visible:signal.avatar(tel) == ""
                    Label {
                        color: theme.text
                        text: model.name[0]
                        text: new String(model.name)[0]
                        anchors.verticalCenter: parent.verticalCenter
                        anchors.horizontalCenter: parent.horizontalCenter
                    }
                }

                Label {
                    text: name
                    anchors.verticalCenter: parent.verticalCenter
                Column {
                    Label {
                        text: name
                    }
                    Label {
                        text: last
                    }          
                }

            }
        }
        model: signal.chats
    }
}

/*##^##
Designer {
    D{i:0;autoSize:true;height:480;width:640}
}
##^##*/


M qml/MessageUI.qml => qml/MessageUI.qml +34 -18
@@ 17,10 17,10 @@ ItemDelegate {
    property ListModel attachmentModel: ListModel {
    }
    Component.onCompleted : {
        if (message.message.Attachment) {
            message.message.Attachment = JSON.parse(message.message.Attachment);
            message.message.Attachment.forEach(function(at) {
                console.log(at);
        console.log(message.message);
        if (message.message.attachment && message.message.attachment !== "[]") {
            var attachments  = JSON.parse(new String(message.message.attachment));
            attachments.forEach(function(at) {
                message.attachmentModel.append(at);
            });
        }


@@ 33,14 33,34 @@ ItemDelegate {
        anchors.bottomMargin: 0
        anchors.topMargin: 0
        anchors.horizontalCenterOffset: 0
        layoutDirection: message.message.Outgoing ? Qt.RightToLeft : Qt.LeftToRight
        layoutDirection: message.message.outgoing ? Qt.RightToLeft : Qt.LeftToRight
        Layout.rightMargin: 5
        Rectangle {
            MouseArea {
                anchors.fill: parent
                acceptedButtons: Qt.RightButton
                onClicked : {
                    if (mouse.button === Qt.RightButton) {
                        msgRightClick.popup();
                    }
                }
                Menu {
                    id: msgRightClick
                    MenuItem {
                        onClicked: {
                            console.log(signalState.currentChat.tel);
                            signal.del_message(signalState.currentChat.tel, message.message.sent_at);
                            console.log("Delete")
                        }
                        text: "Delete"
                    }
                }
            }
            id: rectangle
            width: Math.min(
                       message.maxDelegateWidth, Math.max(
                           message.oneLine ? message.oneLineWidth : textLabel.implicitWidth,
                           dateLabel.implicitWidth, attachmentList.implicitWidth)) + 10
                           dateLabel.implicitWidth, attachmentList.implicitWidth, dynName.implicitWidth)) + 10
            height: textLabel.implicitHeight + textLabel.anchors.topMargin + 5
                    + dateLabel.implicitHeight + attachmentList.implicitHeight + dynName.implicitHeight + dynName.anchors.topMargin +attachmentList.anchors.topMargin
            color: theme.highlight//"#6393e0"


@@ 59,7 79,7 @@ ItemDelegate {
                        font.bold: false
                        id: textLabel
                        width: message.oneLine ? textLabel.implicitWidth : message.maxDelegateWidth
                        text: message.message.Message
                        text: message.message.message
                        wrapMode: Text.Wrap
                    }
                }


@@ 82,37 102,32 @@ ItemDelegate {
                        }
                    }
                }
                                Row {
                Row {
                	id: dynName
                	leftPadding: 5
                	Label {
                		color: theme.highlightedText
                		Component.onCompleted : {
                			var cont = signalState.contact(message.message.Source);
                			console.log("CONTACT"+cont.name+cont.tel+cont.Tel+cont.Name);
                		}
                		text: signalState.contact(message.message.Source).Name || message.message.Source
                		text: signalState.contact(message.message.source).Name || message.message.source
                		visible: signalState.currentChat.is_group
                	}
                }
                Row {
                    leftPadding: 5

                    Label {
                        color: theme.highlightedText
                        Layout.leftMargin: 5
                        id: dateLabel
                        text: {
                            var now = new Date()
                            var dateobj = new Date(message.message.SentAt)
                            var now = new Date();
                            var dateobj = new Date(message.message.sent_at);
                            if (dateobj.getDate() === now.getDate()
                                    && dateobj.getMonth() === now.getMonth()) {
                                return dateobj.toLocaleTimeString(
                                            Qt.locale("en_US"), Locale.ShortFormat)
                                            Qt.locale("en_US"), Locale.ShortFormat);
                            } else {
                                return dateobj.toLocaleDateString(
                                            Qt.locale("en_US"),
                                            Locale.ShortFormat)
                                            Locale.ShortFormat);
                            }
                        }
                    }


@@ 121,3 136,4 @@ ItemDelegate {
        }
    }
}


M qml/SignalState.qml => qml/SignalState.qml +2 -36
@@ 40,51 40,17 @@ Item {
        return JSON.parse(signal.contact(tel));
    }
    function setCurrent(cur) {
        console.log("setCur"+cur);
        signalState.current = cur;
        for (var i=0; i < chatModel.count; i++) {
        for (var i=0; i < signal.chats.count; i++) {
            if (chatModel.get(i).Tel === cur) {
                signalState.currentName = chatModel.get(i).Name;
                signal.chats.currentName = signal.chats.get(i).Name;
                break;
            }
        }
        console.log("setCurrent"+signalState.current);
        signalState.messagesModel.clear();
        console.log(signalState.messages[cur]);
        console.log(JSON.stringify(signalState.messages));
        if (signalState.messages[cur]) {
            console.log("go");
            signalState.messages[cur].forEach(function(msg) {
                console.log("msg"+msg.Message);
                signalState.messagesModel.append({ChatID: qsTr(msg.ChatID),Source: qsTr(msg.Source), Message: qsTr(msg.Message), Outgoing: msg.Outgoing, JsonMsg: msg});
            });
        }
    }
    Connections {
        target: signal
        function onSignalResponse(response) {
            console.log(response);
            var parsed = JSON.parse(response);
            if (parsed.HistoryMessage) {
                signalState.messages[parsed.HistoryMessage.phone] = parsed.HistoryMessage.messages;
                console.log(JSON.stringify(signalState.messages));
                console.log("parsed"+parsed.HistoryMessage.phone);
                console.log("cur"+signalState.cur);
                if (parsed.HistoryMessage.phone === signalState.current) {
                    signalState.setCurrent(signalState.current);
                }
            } else if (parsed.AddHist){
                if (!signalState.messages[parsed.AddHist[0].ChatID]) {
                    signalState.messages[parsed.AddHist[0].ChatID] = [];
                }
                signalState.messages[parsed.AddHist[0].ChatID].push(parsed.AddHist[0]);
                if (parsed.AddHist[1] && parsed.AddHist[0].ChatID == signalState.current) {
                    signalState.messagesModel.append({ChatID: parsed.AddHist[0].ChatID, Source: qsTr(parsed.AddHist[0].Source), Message: qsTr(parsed.AddHist[0].Message), Outgoing: parsed.AddHist[0].Outgoing, JsonMsg: parsed.AddHist[0]});
                } else {
                    signalState.messagesModel.insert(0, {ChatID: parsed.AddHist[0].ChatID, Source: qsTr(parsed.AddHist[0].Source), Message: qsTr(parsed.AddHist[0].Message), Outgoing: parsed.AddHist[0].Outgoing, JsonMsg: parsed.AddHist[0]});

                }
            }
        }
    }
}

M qml/main.qml => qml/main.qml +10 -23
@@ 28,17 28,6 @@ ApplicationWindow {
        }
        name: "signal"
        onSignalResponse: {
            console.log("c"+signal.chats);
            console.log(response);
            var parsed = JSON.parse(response);
            console.log(parsed);
            if (parsed.ChatList) {
                signalState.chatModel.clear();
                parsed.ChatList.forEach(function(x) {
                    var key = x.Tel;
                    signalState.chatModel.append({Name: qsTr(x.Name), Tel:key, Last: x.Last, Whole: x});
                });
            }
        }
        onReady: {
            stackView.replace(null, chatViewStack);


@@ 63,7 52,6 @@ ApplicationWindow {
                }
            }
        }

        Label {
            text: stackView.currentItem.title
            anchors.centerIn: parent


@@ 75,23 63,22 @@ ApplicationWindow {
            id: headerBarDynamic
            ToolButton {
                visible: signalState.currentView === "chatHistory"
    id: editContactButton
    text: "Edit"
    anchors.right: parent.right
    anchors.rightMargin: 5
    anchors.top: parent.top
    anchors.topMargin: 5
    onClicked: {
        signalState.openView("editContact");
    }
}
                id: editContactButton
                text: "Edit"
                anchors.right: parent.right
                anchors.rightMargin: 5
                anchors.top: parent.top
                anchors.topMargin: 5
                onClicked: {
                    signalState.openView("editContact");
                }
            }
        }
    }
    Drawer {
        id: drawer
        width: window.width * 0.66
        height: window.height

        Column {
            anchors.fill: parent
            ItemDelegate {

D src/base.css => src/base.css +0 -27
@@ 1,27 0,0 @@
.msg {
    border: 1px solid #2e2e2e;
    border-radius: 5px;
    padding: 0.1em;
    padding-left: 0.4em;
    padding-right: 0.4em;
}
a {
    color: inherit;
    text-decoration: inherit;
}

.chat-name {
    font-weight: bold;
}

.when-label {
    font-size: 75%;
    padding-left: 2em;
}
.small-text {
    font-size: 75%;
}

.chat-last {
    font-size: 75%;
}
\ No newline at end of file

M src/main.rs => src/main.rs +80 -67
@@ 40,24 40,20 @@ extern crate tokio_tungstenite;
use std::ffi::CStr;
extern crate tungstenite;
extern crate url;
use cpp_core::{Ptr, Ref, StaticUpcast};

#[macro_use]
extern crate lazy_static;
use futures_util::StreamExt;
use qmetaobject::QAbstractListModel;
use qmetaobject::*;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::io::AsyncBufReadExt;
use tokio::sync::mpsc::*;
use tokio_tungstenite::connect_async;
const CONNECTION: &str = "ws://localhost:9080/ws";

#[allow(non_snake_case)]
mod signal;
use signal::*;
use std::collections::*;
use std::env::args;
use std::io::BufRead;
use tungstenite::*;
use url::Url;

mod config;
mod util;
/// Contains the custom widgets


@@ 73,12 69,8 @@ use std::path::PathBuf;
use std::result::Result;

use gettextrs::{bindtextdomain, textdomain};
use qmetaobject::*;

mod scli;
const BASE_CSS: &'static str = include_str!("base.css");
const SIGNAL_CSS: &'static str = include_str!("signal.css");
const STYLE_CSS: &'static str = include_str!("style.css");
pub trait SignalBackend {
    fn messages_id<'a>(&'a self, tel: &str) -> Result<BTreeMap<i64, SignalMessage>, anyhow::Error>;
    fn message<'a>(&'a self, tel: &str, id: i64) -> Result<SignalMessage, anyhow::Error>;


@@ 90,6 82,7 @@ pub trait SignalBackend {
    fn process_response(&mut self, response: SignalResponse);
    fn new(res_sender: glib::Sender<SignalResponse>, config: Config) -> Self;
    fn groups<'a>(&'a self) -> HashMap<String, Group>;
    fn avatar(&self, id: &str) -> Option<String>;
}
#[derive(QObject, Default)]
pub struct SignalUI {


@@ 100,15 93,39 @@ pub struct SignalUI {
    show_view: qt_method!(fn(&mut self, view: String)),
    signalResponse: qt_signal!(response: String),
    chats: qt_property!(RefCell<SimpleListModel<Chat>>; NOTIFY chats_changed),
    messages: qt_property!(RefCell<SimpleListModel<QMLSignalMessage>>; NOTIFY msgs_changed),
    current_messages: qt_property!(String),
    msgs_changed: qt_signal!(),
    chats_changed: qt_signal!(),
    show_chat: qt_method!(fn(&self, tel: String)),
    add_contact: qt_method!(fn(&self, name: String, tel: String)),
    send_message: qt_method!(fn(&self, to: String, message: String)),
    edit_contact: qt_method!(fn(&self, name: String, tel: String, id: String)),
    contact: qt_method!(fn(&self, tel: String) -> String),
    del_message: qt_method!(fn(&self, chat_id: String, sent_at: i64)),
    ready: qt_signal!(),
    avatar: qt_method!(fn(&self, id: String) -> QString),
}
impl SignalUI {
    fn del_message(&self, chat_id: String, sent_at: i64) {
        self.state
            .as_ref()
            .unwrap()
            .sbackend
            .res_sender
            .send(SignalResponse::RmMessage { chat_id, sent_at })
            .expect("Couldn't send RmMessage");
    }
    fn avatar(&self, id: String) -> QString {
        QString::from(
            self.state
                .as_ref()
                .unwrap()
                .sbackend
                .avatar(&id)
                .unwrap_or_default(),
        )
    }
    fn contact(&self, tel: String) -> String {
        let fetched = self
            .state


@@ 130,26 147,42 @@ impl SignalUI {
                name,
                phone: tel,
                id,
            });
            })
            .expect("Couldn't send EditContact");
    }
    fn send_message(&self, to: String, message: String) {
        self.state
            .as_ref()
            .unwrap()
            .req_sender
            .send(SignalRequest::SendMessage { to, message });
            .send(SignalRequest::SendMessage { to, message })
            .expect("Couldn't send SendMessage");
    }
    fn add_contact(&self, name: String, tel: String) {
        self.state
            .as_ref()
            .unwrap()
            .req_sender
            .send(SignalRequest::AddContact { name, phone: tel });
            .send(SignalRequest::AddContact { name, phone: tel })
            .expect("Couldn't send AddContact");
    }
    fn show_view(&mut self, view: String) {
        self.state.as_mut().unwrap().show_view(view);
    }
    fn show_chat(&mut self, view: String) {
        self.messages.replace(
            self.state
                .as_ref()
                .unwrap()
                .sbackend
                .messages_id(view.as_str())
                .unwrap()
                .values()
                .cloned()
                .map(|x| QMLSignalMessage::from_msg(x))
                .collect(),
        );
        self.current_messages = view.clone();
        self.state.as_ref().unwrap().show_chat(view);
    }
    fn init_state(&mut self) {


@@ 170,10 203,34 @@ impl SignalUI {
                let ref mut slf = self_.borrow_mut();
                slf.state.as_mut().unwrap().process_response(res.clone());
                if let SignalResponse::ChatList(ref chats) = res {
                    println!("gochat");
                    slf.chats.replace(chats.into_iter().collect());
                    slf.chats_changed();
                }
                if let SignalResponse::AddHist(ref msg, new) = res {
                    if new {
                        if msg.chat_id == slf.current_messages {
                            slf.messages
                                .get_mut()
                                .push(QMLSignalMessage::from_msg(msg.clone()));
                        }
                    }
                }
                if let SignalResponse::RmMessage {
                    ref chat_id,
                    sent_at,
                } = res
                {
                    if &slf.current_messages == chat_id {
                        let ref mut msg_model = slf.messages.get_mut();
                        let count = msg_model.row_count() as usize;
                        for i in 0..count {
                            if msg_model[i].sent_at == sent_at {
                                msg_model.remove(i);
                                break;
                            }
                        }
                    }
                }
                if let SignalResponse::Groups(_) = res {
                    slf.show_view("chats".to_string());
                    slf.ready();


@@ 187,19 244,6 @@ impl SignalUI {
        let (req_sender, req_receiver) = unbounded_channel::<SignalRequest>();
        let clconf = config.clone();
        match config.backend {
            /*Backend::Axolotl => {
                let mut state: SignalState<AxolotlSession> =
                    SignalState::new(req_sender.clone(), l_res_sender.clone(), config.clone());
                //state.change_theme(config.theme);
                state.user_tel = config.number.clone();
                state.connect_signals();
                //window.add(&state.app_box);
                state.show_view(View::Chats);
                res_receiver.attach(None, move |value| {
                    process_res(value);
                    glib::Continue(true)
                });
            }*/
            Backend::SignalCLI => {
                let mut state: SignalState<scli::SCLISession> =
                    SignalState::new(req_sender.clone(), l_res_sender.clone(), config.clone());


@@ 221,8 265,9 @@ impl SignalUI {
                .send(SignalRequest::GetContacts)
                .expect("Couldn't send GetContacts");
        }
        let cloned_req = req_sender.clone();
        req_sender.send(SignalRequest::GetGroups);
        req_sender
            .send(SignalRequest::GetGroups)
            .expect("Couldn't send GetGroups");
        qmetaobject::future::execute_async(async move {
            match clconf.backend {
                Backend::Axolotl => {


@@ 279,32 324,16 @@ where
        res_sender: glib::Sender<SignalResponse>,
        config: Config,
    ) -> SignalState<T> {
        //let chat_list = ChatList::default();
        //let headerbar = Header::new(&res_sender);

        /*let pw = PasswordDialog::new();
        let cc = CreateChat::new();
        let create_contact = CreateContact::new();
        let edit_contact = EditContact::new();
        let settings = widgets::Settings::new(config.clone());*/
        SignalState {
            current: None,
            //history: None,
            req_sender,
            last_id: Rc::new(RefCell::new(None)),
            //chat_list,
            res_sender: res_sender.clone(),
            current_chat: Rc::new(RefCell::new(None)),
            //headerbar,
            //pw,
            //cc,
            //ccontact: create_contact,
            //edit_contact,
            user_tel: None,
            backend: config.backend,
            config: Rc::new(RefCell::new(config.clone())),
            sbackend: std::boxed::Box::new(T::new(res_sender, config)),
            //settings,
        }
    }
    /// Connects the gtk event signals to the relevant callbacks


@@ 449,6 478,7 @@ where
                })
            }
        }
        chats.sort_by_key(|c| -1 * c.timestamp);
        self.res_sender.send(SignalResponse::ChatList(chats));
    }
    /// Shows a given chat by tel


@@ 468,7 498,7 @@ where
        self.sbackend.process_response(response.clone());
        match response {
            SignalResponse::Quit => {}
            SignalResponse::ShowTheme(theme) => {}
            SignalResponse::ShowTheme(_theme) => {}
            SignalResponse::ShowView(to_show) => {
                self.show_view(to_show);
            }


@@ 476,29 506,12 @@ where
                self.update_chats();
            }
            SignalResponse::Type(typ) => match typ.as_ref() {
                "getEncryptionPw" => {
                    //self.show_view(View::Password);
                }
                "registrationDone" => {
                    //self.show_view(View::Chats);
                }
                "getEncryptionPw" => {}
                "registrationDone" => {}
                _ => {
                    println!("Type: {}", typ);
                }
            },
            SignalResponse::AddHist(msg, new) => {
                /*let msg = self.sbackend.message(&tel, id).unwrap();
                if let Some(hist) = self.history.as_mut() {
                    if hist.tel == msg.source {
                        if new {
                            hist.insert_bottom(Element::Message(Message::new(msg.clone())));
                        } else {
                            hist.insert_top(Element::Message(Message::new(msg.clone())));
                        }
                        self.last_id.replace(hist.get_last_id());
                    }
                }*/
            }
            _ => {}
        }
    }

M src/scli.rs => src/scli.rs +24 -0
@@ 309,6 309,23 @@ impl SCLISession {
    }
}
impl SignalBackend for SCLISession {
    fn avatar(&self, id: &str) -> Option<String> {
        let opts = vec![
            "profile".to_string(),
            "contact".to_string(),
            "group".to_string(),
        ];
        let av_dir = home::home_dir()
            .unwrap()
            .join(".local/share/signal-cli/avatars");
        for opt in opts {
            let now_path = av_dir.join(format!("{}-{}", opt, id));
            if now_path.exists() {
                return Some(now_path.to_str().unwrap().to_string());
            }
        }
        None
    }
    fn new(res_sender: glib::Sender<SignalResponse>, config: Config) -> SCLISession {
        let scli_config = config.scli.expect("No configured SCLI settings");
        let data_dir = scli_config.data_dir.clone().replace(


@@ 418,6 435,13 @@ impl SignalBackend for SCLISession {
    }
    fn process_response(&mut self, response: SignalResponse) {
        match response {
            SignalResponse::RmMessage { chat_id, sent_at } => {
                if let Some(doc) = self.message_doc(&chat_id, sent_at).ok() {
                    self.msg_store
                        .delete(doc.id)
                        .expect("Couldn't delete message");
                }
            }
            SignalResponse::SignalCLIEnvelope(msg) => {
                println!("Processing");
                self.process_scli_msg(msg);

D src/signal.css => src/signal.css +0 -32
@@ 1,32 0,0 @@
.msg-scroll listbox {
    background-color: black;
}
.msg-box {
    background-color: black;
}
window {
    background-color: black;
}
.header {
    background-color: black;
    color: white;
}
.chat-list {
    background-color: black;
    color: white;
}
.history-listbox {
    background-color: black;
}
.msg-box entry {
    background-color: #141414;
}
.msg-box button {
    background-color: #141414;
}
.msg {
    background-color: #2e2e2e;
}
.outgoing {
    background-color: #2c6bed;
}

M src/signal.rs => src/signal.rs +42 -0
@@ 98,6 98,10 @@ pub enum SignalResponse {
        messages: Vec<SignalMessage>,
    },
    Groups(Vec<Group>),
    RmMessage {
        chat_id: String,
        sent_at: i64,
    },
}
impl Default for SignalResponse {
    fn default() -> Self {


@@ 258,6 262,44 @@ pub struct SignalMessage {
    pub receipt: bool,
    pub status_message: bool,*/
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, QGadget, SimpleListItem)]
#[serde(rename_all = "PascalCase")]
pub struct QMLSignalMessage {
    #[serde(rename = "ID")]
    pub ID: i32,
    /*#[serde(rename = "SID")]
    pub SID: i32,*/
    #[serde(rename = "ChatID")]
    pub chat_id: String,
    pub source: String,
    pub message: String,
    pub outgoing: bool,
    pub sent_at: i64,
    pub attachment: String,
    //pub received_at: i64,
    /*pub h_time: String,
    pub c_type: i32,
    pub is_sent: bool,
    pub is_read: bool,
    pub flags: i32,
    pub expire_timer: i32,
    pub sending_error: bool,
    pub receipt: bool,
    pub status_message: bool,*/
}
impl QMLSignalMessage {
    pub fn from_msg(msg: SignalMessage) -> QMLSignalMessage {
        QMLSignalMessage {
            ID: msg.ID,
            chat_id: msg.chat_id,
            source: msg.source,
            message: msg.message,
            outgoing: msg.outgoing,
            sent_at: msg.sent_at,
            attachment: serde_json::to_string(&msg.attachment).unwrap(),
        }
    }
}
fn ser_attachments<S>(ats: &Option<Vec<Attachment>>, ser: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,

D src/style.css => src/style.css +0 -33
@@ 1,33 0,0 @@
.msg-scroll listbox {
    background-color: @theme_base_color;
}
.msg-box {
    background-color: @theme_base_color;
}
window {
    background-color: @theme_base_color;
}
.header {
    background-color: @titlebar_color;
    color: white;
}
.chat-list {
    background-color: @theme_base_color;
    color: white;
}
.history-listbox {
    background-color: @metacity_color;
}
.msg-box entry {
    background-color: #141414;
}
.msg-box button {
    background-color: #141414;
}

.outgoing {
    background-color: #2c6bed;
}
.msg {
    background-color: #2e2e2e;
}
\ No newline at end of file