web & server: add support for email photos
This commit is contained in:
parent
f27f0deb38
commit
5a997e61da
@ -0,0 +1,3 @@
|
|||||||
|
DROP TABLE IF EXISTS email_address;
|
||||||
|
DROP TABLE IF EXISTS photo;
|
||||||
|
DROP TABLE IF EXISTS google_person;
|
||||||
19
server/migrations/20250114045217_create-email-photos.up.sql
Normal file
19
server/migrations/20250114045217_create-email-photos.up.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- Add up migration script here
|
||||||
|
CREATE TABLE IF NOT EXISTS google_person (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
resource_name TEXT NOT NULL UNIQUE,
|
||||||
|
display_name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS email_photo (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
google_person_id INTEGER REFERENCES google_person (id) UNIQUE,
|
||||||
|
url TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS email_address (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
address TEXT NOT NULL UNIQUE,
|
||||||
|
email_photo_id INTEGER REFERENCES email_photo (id),
|
||||||
|
google_person_id INTEGER REFERENCES google_person (id)
|
||||||
|
);
|
||||||
@ -237,6 +237,7 @@ impl Body {
|
|||||||
pub struct Email {
|
pub struct Email {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub addr: Option<String>,
|
pub addr: Option<String>,
|
||||||
|
pub photo_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Email {
|
impl fmt::Display for Email {
|
||||||
@ -422,6 +423,7 @@ impl QueryRoot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut connection = Connection::new(has_previous_page, has_next_page);
|
let mut connection = Connection::new(has_previous_page, has_next_page);
|
||||||
|
// Set starting offset as the value from cursor to preserve state if no results from a corpus survived the truncation
|
||||||
let mut newsreader_offset =
|
let mut newsreader_offset =
|
||||||
after.as_ref().map(|sc| sc.newsreader_offset).unwrap_or(0);
|
after.as_ref().map(|sc| sc.newsreader_offset).unwrap_or(0);
|
||||||
let mut notmuch_offset = after.as_ref().map(|sc| sc.notmuch_offset).unwrap_or(0);
|
let mut notmuch_offset = after.as_ref().map(|sc| sc.notmuch_offset).unwrap_or(0);
|
||||||
@ -487,7 +489,7 @@ impl QueryRoot {
|
|||||||
if newsreader::is_newsreader_thread(&thread_id) {
|
if newsreader::is_newsreader_thread(&thread_id) {
|
||||||
Ok(newsreader::thread(config, pool, thread_id).await?)
|
Ok(newsreader::thread(config, pool, thread_id).await?)
|
||||||
} else {
|
} else {
|
||||||
Ok(nm::thread(nm, thread_id, debug_content_tree).await?)
|
Ok(nm::thread(nm, pool, thread_id, debug_content_tree).await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ use log::{error, info, warn};
|
|||||||
use mailparse::{parse_content_type, parse_mail, MailHeader, MailHeaderMap, ParsedMail};
|
use mailparse::{parse_content_type, parse_mail, MailHeader, MailHeaderMap, ParsedMail};
|
||||||
use memmap::MmapOptions;
|
use memmap::MmapOptions;
|
||||||
use notmuch::Notmuch;
|
use notmuch::Notmuch;
|
||||||
|
use rocket::http::uri::error::PathError;
|
||||||
|
use sqlx::PgPool;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -147,6 +149,7 @@ pub fn tags(nm: &Notmuch, needs_unread: bool) -> Result<Vec<Tag>, ServerError> {
|
|||||||
#[instrument(name="nm::thread", skip_all, fields(thread_id=thread_id))]
|
#[instrument(name="nm::thread", skip_all, fields(thread_id=thread_id))]
|
||||||
pub async fn thread(
|
pub async fn thread(
|
||||||
nm: &Notmuch,
|
nm: &Notmuch,
|
||||||
|
pool: &PgPool,
|
||||||
thread_id: String,
|
thread_id: String,
|
||||||
debug_content_tree: bool,
|
debug_content_tree: bool,
|
||||||
) -> Result<Thread, ServerError> {
|
) -> Result<Thread, ServerError> {
|
||||||
@ -159,7 +162,7 @@ pub async fn thread(
|
|||||||
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
||||||
let m = parse_mail(&mmap)?;
|
let m = parse_mail(&mmap)?;
|
||||||
let from = email_addresses(&path, &m, "from")?;
|
let from = email_addresses(&path, &m, "from")?;
|
||||||
let from = match from.len() {
|
let mut from = match from.len() {
|
||||||
0 => None,
|
0 => None,
|
||||||
1 => from.into_iter().next(),
|
1 => from.into_iter().next(),
|
||||||
_ => {
|
_ => {
|
||||||
@ -171,6 +174,16 @@ pub async fn thread(
|
|||||||
from.into_iter().next()
|
from.into_iter().next()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
match from.as_mut() {
|
||||||
|
Some(from) => {
|
||||||
|
if let Some(addr) = from.addr.as_mut() {
|
||||||
|
let photo_url = photo_url_for_email_address(&pool, &addr).await?;
|
||||||
|
from.photo_url = photo_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
let to = email_addresses(&path, &m, "to")?;
|
let to = email_addresses(&path, &m, "to")?;
|
||||||
let cc = email_addresses(&path, &m, "cc")?;
|
let cc = email_addresses(&path, &m, "cc")?;
|
||||||
let subject = m.headers.get_first_value("subject");
|
let subject = m.headers.get_first_value("subject");
|
||||||
@ -329,6 +342,7 @@ fn email_addresses(
|
|||||||
mailparse::MailAddr::Single(s) => addrs.push(Email {
|
mailparse::MailAddr::Single(s) => addrs.push(Email {
|
||||||
name: s.display_name,
|
name: s.display_name,
|
||||||
addr: Some(s.addr),
|
addr: Some(s.addr),
|
||||||
|
photo_url: None,
|
||||||
}), //println!("Single: {s}"),
|
}), //println!("Single: {s}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,12 +357,14 @@ fn email_addresses(
|
|||||||
addrs.push(Email {
|
addrs.push(Email {
|
||||||
name: Some(name.to_string()),
|
name: Some(name.to_string()),
|
||||||
addr: Some(addr.to_string()),
|
addr: Some(addr.to_string()),
|
||||||
|
photo_url: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addrs.push(Email {
|
addrs.push(Email {
|
||||||
name: Some(v),
|
name: Some(v),
|
||||||
addr: None,
|
addr: None,
|
||||||
|
photo_url: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -877,3 +893,24 @@ pub async fn set_read_status<'ctx>(
|
|||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn photo_url_for_email_address(
|
||||||
|
pool: &PgPool,
|
||||||
|
addr: &str,
|
||||||
|
) -> Result<Option<String>, ServerError> {
|
||||||
|
let row = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
url
|
||||||
|
FROM email_photo ep
|
||||||
|
JOIN email_address ea
|
||||||
|
ON ep.id = ea.email_photo_id
|
||||||
|
WHERE
|
||||||
|
address = $1
|
||||||
|
"#,
|
||||||
|
addr
|
||||||
|
)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(row.map(|r| r.url))
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,28 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"__schema": {
|
"__schema": {
|
||||||
"directives": [
|
"directives": [
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"defaultValue": "\"No longer supported\"",
|
||||||
|
"description": "A reason for why it is deprecated, formatted using Markdown syntax",
|
||||||
|
"name": "reason",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Marks an element of a GraphQL schema as no longer supported.",
|
||||||
|
"locations": [
|
||||||
|
"FIELD_DEFINITION",
|
||||||
|
"ARGUMENT_DEFINITION",
|
||||||
|
"INPUT_FIELD_DEFINITION",
|
||||||
|
"ENUM_VALUE"
|
||||||
|
],
|
||||||
|
"name": "deprecated"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"args": [
|
"args": [
|
||||||
{
|
{
|
||||||
@ -27,6 +49,14 @@
|
|||||||
],
|
],
|
||||||
"name": "include"
|
"name": "include"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"description": "Indicates that an Input Object is a OneOf Input Object (and thus requires\n exactly one of its field be provided)",
|
||||||
|
"locations": [
|
||||||
|
"INPUT_OBJECT"
|
||||||
|
],
|
||||||
|
"name": "oneOf"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"args": [
|
"args": [
|
||||||
{
|
{
|
||||||
@ -51,6 +81,29 @@
|
|||||||
"INLINE_FRAGMENT"
|
"INLINE_FRAGMENT"
|
||||||
],
|
],
|
||||||
"name": "skip"
|
"name": "skip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"description": "URL that specifies the behavior of this scalar.",
|
||||||
|
"name": "url",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides a scalar specification URL for specifying the behavior of custom scalar types.",
|
||||||
|
"locations": [
|
||||||
|
"SCALAR"
|
||||||
|
],
|
||||||
|
"name": "specifiedBy"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mutationType": {
|
"mutationType": {
|
||||||
@ -311,6 +364,18 @@
|
|||||||
"name": "String",
|
"name": "String",
|
||||||
"ofType": null
|
"ofType": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "photoUrl",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputFields": null,
|
"inputFields": null,
|
||||||
@ -880,22 +945,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"args": [],
|
|
||||||
"deprecationReason": null,
|
|
||||||
"description": "Drop and recreate tantivy index. Warning this is slow",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"name": "dropAndLoadIndex",
|
|
||||||
"type": {
|
|
||||||
"kind": "NON_NULL",
|
|
||||||
"name": null,
|
|
||||||
"ofType": {
|
|
||||||
"kind": "SCALAR",
|
|
||||||
"name": "Boolean",
|
|
||||||
"ofType": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"args": [],
|
"args": [],
|
||||||
"deprecationReason": null,
|
"deprecationReason": null,
|
||||||
@ -1839,7 +1888,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"args": [],
|
"args": [
|
||||||
|
{
|
||||||
|
"defaultValue": "false",
|
||||||
|
"description": null,
|
||||||
|
"name": "includeDeprecated",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Boolean",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"deprecationReason": null,
|
"deprecationReason": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
@ -2110,7 +2174,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"args": [],
|
"args": [
|
||||||
|
{
|
||||||
|
"defaultValue": "false",
|
||||||
|
"description": null,
|
||||||
|
"name": "includeDeprecated",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Boolean",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"deprecationReason": null,
|
"deprecationReason": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
@ -2243,6 +2322,34 @@
|
|||||||
"name": "String",
|
"name": "String",
|
||||||
"ofType": null
|
"ofType": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "isDeprecated",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Boolean",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "deprecationReason",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputFields": null,
|
"inputFields": null,
|
||||||
@ -2255,6 +2362,22 @@
|
|||||||
"description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes\nall available types and directives on the server, as well as the entry\npoints for query, mutation, and subscription operations.",
|
"description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes\nall available types and directives on the server, as well as the entry\npoints for query, mutation, and subscription operations.",
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"fields": [
|
"fields": [
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": "description of __Schema for newer graphiql introspection schema\nrequirements",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "description",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"args": [],
|
"args": [],
|
||||||
"deprecationReason": null,
|
"deprecationReason": null,
|
||||||
@ -2505,7 +2628,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"args": [],
|
"args": [
|
||||||
|
{
|
||||||
|
"defaultValue": "false",
|
||||||
|
"description": null,
|
||||||
|
"name": "includeDeprecated",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Boolean",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"deprecationReason": null,
|
"deprecationReason": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
|
|||||||
@ -21,6 +21,7 @@ query ShowThreadQuery($threadId: String!) {
|
|||||||
from {
|
from {
|
||||||
name
|
name
|
||||||
addr
|
addr
|
||||||
|
photoUrl
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
name
|
name
|
||||||
|
|||||||
@ -375,7 +375,21 @@ fn has_unread(tags: &[String]) -> bool {
|
|||||||
tags.contains(&String::from("unread"))
|
tags.contains(&String::from("unread"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_avatar(_avatar: Option<String>, from: &str, big: bool) -> Node<Msg> {
|
fn render_avatar(photo_url: Option<String>, from: &str, big: bool) -> Node<Msg> {
|
||||||
|
let size = if big {
|
||||||
|
C!["w-16", "h-16", "text-4xl"]
|
||||||
|
} else {
|
||||||
|
C!["w-8", "h-8", "text-l"]
|
||||||
|
};
|
||||||
|
if let Some(photo_url) = photo_url {
|
||||||
|
return div![
|
||||||
|
size,
|
||||||
|
img![attrs! {
|
||||||
|
At::Src => photo_url,
|
||||||
|
}]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
let initials: String = from
|
let initials: String = from
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
.trim()
|
.trim()
|
||||||
@ -386,11 +400,7 @@ fn render_avatar(_avatar: Option<String>, from: &str, big: bool) -> Node<Msg> {
|
|||||||
.take(2)
|
.take(2)
|
||||||
.collect();
|
.collect();
|
||||||
let from_color = compute_color(from);
|
let from_color = compute_color(from);
|
||||||
let size = if big {
|
|
||||||
C!["w-16", "h-16", "text-4xl"]
|
|
||||||
} else {
|
|
||||||
C!["w-8", "h-8", "text-l"]
|
|
||||||
};
|
|
||||||
div![
|
div![
|
||||||
C![
|
C![
|
||||||
"[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]",
|
"[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]",
|
||||||
@ -416,22 +426,22 @@ fn copy_text_widget(text: &str) -> Node<Msg> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> {
|
fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> {
|
||||||
let (from, from_detail) = match &msg.from {
|
let (from, from_detail, photo_url) = match &msg.from {
|
||||||
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
|
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
|
||||||
name: Some(name),
|
name: Some(name),
|
||||||
addr,
|
addr,
|
||||||
}) => (name.to_string(), addr.clone()),
|
photo_url,
|
||||||
|
}) => (name.to_string(), addr.clone(), photo_url.clone()),
|
||||||
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
|
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
|
||||||
addr: Some(addr), ..
|
addr: Some(addr),
|
||||||
}) => (addr.to_string(), None),
|
photo_url,
|
||||||
_ => (String::from("UNKNOWN"), None),
|
..
|
||||||
|
}) => (addr.to_string(), None, photo_url.clone()),
|
||||||
|
_ => (String::from("UNKNOWN"), None, None),
|
||||||
};
|
};
|
||||||
// TODO(wathiede): get this from server
|
|
||||||
let avatar: Option<String> = None;
|
|
||||||
//let avatar: Option<String> = Some(String::from("https://bulma.io/images/placeholders/64x64.png"));
|
|
||||||
let id = msg.id.clone();
|
let id = msg.id.clone();
|
||||||
let is_unread = has_unread(&msg.tags);
|
let is_unread = has_unread(&msg.tags);
|
||||||
let avatar = render_avatar(avatar, &from, true);
|
let avatar = render_avatar(photo_url, &from, true);
|
||||||
let unknown = "UNKNOWN".to_string();
|
let unknown = "UNKNOWN".to_string();
|
||||||
div![
|
div![
|
||||||
C!["flex", "p-4"],
|
C!["flex", "p-4"],
|
||||||
@ -513,22 +523,22 @@ fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> {
|
fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> {
|
||||||
let (from, from_detail) = match &msg.from {
|
let (from, from_detail, photo_url) = match &msg.from {
|
||||||
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
|
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
|
||||||
name: Some(name),
|
name: Some(name),
|
||||||
addr,
|
addr,
|
||||||
}) => (name.to_string(), addr.clone()),
|
photo_url,
|
||||||
|
}) => (name.to_string(), addr.clone(), photo_url.clone()),
|
||||||
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
|
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
|
||||||
addr: Some(addr), ..
|
addr: Some(addr),
|
||||||
}) => (addr.to_string(), None),
|
photo_url,
|
||||||
_ => (String::from("UNKNOWN"), None),
|
..
|
||||||
|
}) => (addr.to_string(), None, photo_url.clone()),
|
||||||
|
_ => (String::from("UNKNOWN"), None, None),
|
||||||
};
|
};
|
||||||
// TODO(wathiede): get this from server
|
|
||||||
let avatar: Option<String> = None;
|
|
||||||
//let avatar: Option<String> = Some(String::from("https://bulma.io/images/placeholders/64x64.png"));
|
|
||||||
let id = msg.id.clone();
|
let id = msg.id.clone();
|
||||||
let is_unread = has_unread(&msg.tags);
|
let is_unread = has_unread(&msg.tags);
|
||||||
let avatar = render_avatar(avatar, &from, false);
|
let avatar = render_avatar(photo_url, &from, false);
|
||||||
let unknown = "UNKNOWN".to_string();
|
let unknown = "UNKNOWN".to_string();
|
||||||
div![
|
div![
|
||||||
C!["flex", "p-4"],
|
C!["flex", "p-4"],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user