Compare commits

...

13 Commits

Author SHA1 Message Date
Simon Laux
6161b50bc7 attachments 2020-05-07 02:09:01 +02:00
Simon Laux
cbd45bc0ea cargo fmt 2020-05-07 01:01:44 +02:00
Simon Laux
b3eaf6730f store chat image by filename,
same as all other blobs
also deduplicate blobs before saving
2020-05-07 00:34:23 +02:00
Simon Laux
c3be1e3163 fix: image avatars went missing 2020-05-07 00:28:58 +02:00
Simon Laux
2e60380803 fix css bug 2020-05-07 00:25:09 +02:00
Simon Laux
108826d4af hide avatar icon and author name on outgoing
messages
2020-05-07 00:22:06 +02:00
Simon Laux
d36d6bc87c add header with chat name and avatar 2020-05-07 00:16:55 +02:00
Simon Laux
7b5c946c82 add a todo point to css file 2020-04-15 22:48:08 +02:00
Simon Laux
8665a3f8ad write zip file, pack blobs into it
and copy some styles over from desktop
2020-04-15 22:45:20 +02:00
Simon Laux
c99131f551 change string style 2020-04-07 20:59:32 +02:00
Simon Laux
9f24d57835 checkpoint 3 2020-04-07 20:59:29 +02:00
Simon Laux
817050260f second checkpoint 2020-04-07 20:43:14 +02:00
Simon Laux
e3b8b64c16 just a backup commit, nothing works yet 2020-04-07 20:43:14 +02:00
7 changed files with 1198 additions and 1 deletions

40
Cargo.lock generated
View File

@@ -373,6 +373,24 @@ name = "bytes"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bzip2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bzip2-sys 0.1.8+1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bzip2-sys"
version = "0.1.8+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "c2-chacha"
version = "0.2.3"
@@ -678,6 +696,7 @@ dependencies = [
"strum_macros 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"thread-local-object 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"zip 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1903,6 +1922,11 @@ dependencies = [
"inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "podio"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ppv-lite86"
version = "0.2.6"
@@ -3136,6 +3160,18 @@ dependencies = [
"synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zip"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bzip2 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
"podio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
"checksum aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "54eb1d8fe354e5fc611daf4f2ea97dd45a765f4f1e4512306ec183ae2e8f20c9"
@@ -3180,6 +3216,8 @@ dependencies = [
"checksum bytecount 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b92204551573580e078dc80017f36a213eb77a0450e4ddd8cfa0f3f2d1f0178f"
"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
"checksum bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1"
"checksum bzip2 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b"
"checksum bzip2-sys 0.1.8+1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "05305b41c5034ff0e93937ac64133d109b5a2660114ec45e9760bc6816d83038"
"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
"checksum cargo_metadata 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e5d1b4d380e1bab994591a24c2bdd1b054f64b60bef483a8c598c7c345bc3bbe"
"checksum cast5 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ce5759b4c52ca74f9a98421817c882f1fd9b0071ae41cd61ab9f9d059c04fd6"
@@ -3343,6 +3381,7 @@ dependencies = [
"checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587"
"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
"checksum png 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef859a23054bbfee7811284275ae522f0434a3c8e7f4b74bd4a35ae7e1c4a283"
"checksum podio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "780fb4b6698bbf9cf2444ea5d22411cef2953f0824b98f33cf454ec5615645bd"
"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
"checksum pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
"checksum pretty_env_logger 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "717ee476b1690853d222af4634056d830b5197ffd747726a9a1eee6da9f49074"
@@ -3485,3 +3524,4 @@ dependencies = [
"checksum x25519-dalek 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217"
"checksum zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8"
"checksum zeroize_derive 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2"
"checksum zip 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6df134e83b8f0f8153a094c7b0fd79dfebe437f1d76e7715afa18ed95ebe2fd7"

View File

@@ -58,6 +58,7 @@ encoded-words = { git = "https://github.com/async-email/encoded-words", branch="
native-tls = "0.2.3"
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
pretty_env_logger = "0.3.1"
zip = "0.5"
rustyline = { version = "4.1.0", optional = true }

788
assets/exported-chat.css Normal file
View File

@@ -0,0 +1,788 @@
/* TODO inlcude the referenced svgs as base64 data uris */
.header {
background-color: #415e6b;
color: #fff;
position: absolute;
height: 52px;
width: 100%;
z-index: 5;
display:flex;
}
.header .avatar {
height: 36px;
width: 36px;
border-radius: 100%;
user-select: none;
margin: 8px;
}
.header .avatar.text-avatar {
background-color: #505050;
color: white;
font-size: 26px;
line-height: 36px;
text-align: center;
}
.header .name {
height: 52px;
line-height: 52px;
margin-left: 3px;
font-size: 20px;
}
.message.outgoing .author-avatar, .message.outgoing .author {
display: none!important;
}
:root {--colorPrimary: #42A5F5;--colorDanger: #f96856;--colorNone: #a0a0a0;--ovalButtonBg: #415e6b;--ovalButtonBgHover: rgb(120, 156, 173);--ovalButtonText: #fff;--ovalButtonTextHover: rgb(0, 0, 0);--navBarBackground: #415e6b;--navBarText: #fff;--navBarSearchPlaceholder: rgb(186, 186, 186);--navBarGroupSubtitle: rgb(186, 186, 186);--chatViewBg: #e6dcd4;--chatViewBgImgPath: url(../images/background_light.svg);--composerBg: #fff;--composerText: #010101;--composerPlaceholderText: rgba(1, 1, 1, 0.5);--composerBtnColor: rgba(1, 1, 1, 0.9);--composerSendButton: #415e6b;--emojiSelectorSelectionColor: #2090ea;--chatListItemSelectedBg: #4c6e7d;--chatListItemSelectedBgHover: #5E889B;--chatListItemSelectedText: #fff;--chatListItemBgHover: rgb(228, 228, 228);--chatListBorderColor: #b9b9b9;--chatListBorder: 1px solid undefined;--messageText: #010101;--messageTextLink: #010101;--setupMessageText: #ed824e;--infoMessageBubbleBg: #0000008c;--infoMessageBubbleText: white;--messageIncommingBg: #fff;--messageIncommingDate: #010101;--messageOutgoingBg: #efffde;--messageOutgoingStatusColor: #4caf50;--messageButtons: #8b8e91;--messageButtonsHover: #070c14;--messageStatusIcon: #4caf50;--messageStatusIconSending: #62656a;--messagePadlockOutgoing: #4caf50;--messagePadlockIncomming: #a4a6a9;--messageMetadataDate: #62656a;--messageMetadataIncomming: rgba(#ffffff, 0.7);--messageAuthor: #ffffff;--messageAttachmentIconExtentionColor: #070c14;--messageAttachmentIconBg: transparent;--messageAttachmentFileInfo: #010101;--loginInputFocusColor: #42A5F5;--loginButtonText: #42A5F5;--deltaChatPrimaryFg: #010101;--deltaChatPrimaryFgLight: #62656a;--contextMenuBg: #fff;--contextMenuBorder: rgb(221, 221, 221);--contextMenuText: #62656a;--contextMenuSelected: #f5f5f5;--contextMenuSelectedBg: #a4a6a9;--bp3DialogHeaderBg: #fff;--bp3DialogHeaderIcon: #666666;--bp3DialogBgSecondary: #ececec;--bp3DialogBgPrimary: #fff;--bp3Heading: #010101;--bp3ButtonText: #010101;--bp3ButtonBg: #fff;--bp3ButtonGradientTop: rgba(255,255,255,0.8);--bp3ButtonGradientBottom: rgba(255,255,255,0);--bp3ButtonHoverBg: #ebf1f5;--bp3InputText: #010101;--bp3InputBg: #fff;--bp3InputPlaceholder: lightgray;--bp3MenuText: #010101;--bp3MenuBg: #fff;--bp3Switch: #7a8084;--bp3SwitchShadow: unset;--bp3SwitchChecked: #acd4e8;--bp3SwitchShadowChecked: unset;--bp3SwitchKnob: #f5f5f5;--bp3SwitchKnobShadow: 0px 2px 0 0px #d2cfcfad;--bp3SwitchKnobChecked: #42A5F5;--bp3SwitchKnobShadowChecked: 0px 1px 0 0px #c9d4d2d1;--bp3SpinnerTrack: #acd4e8;--bp3SpinnerHead: #42a5f5;--bp3SelectorTop: rgba(255, 255, 255, 0.8);--bp3SelectorBottom: rgba(255, 255, 255, 0.0);--outlineProperties: 1px solid transparent;--outlineColor: b9b9b9;--emojiMartText: #010101;--emojiMartSearchBorder: lightgrey;--emojiMartBg: #fff;--emojiMartOutsideRadius: 5px;--emojiMartCategoryIcons: rgb(99, 99, 99);--emojiMartInputBg: #f5f5f5;--emojiMartInputText: #010101;--emojiMartInputPlaceholder: rgb(74, 74, 74);--emojiMartSelect: rgb(198, 198, 198);--galleryBg: #fff;--avatarLabelColor: #ffffff;--brokenMediaText: #070c14;--brokenMediaBg: #ffffff;--unreadCountBg: #2090ea;--unreadCountLabel: #ffffff;--contactListItemBg: #62656a;--contactListInitalColor: #62656a;--contactEmailColor: #62656a;--errorColor: #f44336;--globalLinkColor: #2090ea;--globalBackground: #fff;--globalText: #010101;--mapOverlayBg: #fff;--videoPlayBtnIcon: #2090ea;--videoPlayBtnBg: #ffffff;--scrollbarThumb: #666666;--scrollbarThumbHover: #606060;}
* {
box-sizing: border-box;
}
html {
height: 100%;
--messageIncommingBg: rgb(232, 232, 232);
}
body {
position: relative;
height: 100%;
width: 100%;
margin: 0;
font-family: Roboto, "Apple Color Emoji", NotoEmoji, "Helvetica Neue", Arial,
Helvetica, NotoMono, sans-serif !important;
font-size: 14px;
color: black;
background-color: white;
}
ul {
list-style: none;
padding-left: 0;
}
input:focus {
outline: 0 !important;
}
button:focus {
outline: none;
}
button:focus {
outline: none;
}
::-webkit-scrollbar {
width: 6px;
height: 0;
}
::-webkit-scrollbar-track {
background: white;
}
::-webkit-scrollbar-thumb {
background: var(--scrollbarThumb);
}
::-webkit-scrollbar-thumb:hover {
background: var(--scrollbarThumbHover);
}
::-webkit-scrollbar-corner {
background: transparent;
}
span.module-contact-name {
font-weight: 200;
font-size: medium;
}
.module-contact-name__profile-name {
font-style: italic;
}
.AvatarBubble {
position: relative;
z-index: 2;
object-fit: cover;
height: 48px;
width: 48px;
margin-top: 8px;
margin-bottom: 8px;
border-radius: 100%;
background-color: #505050;
color: var(--avatarLabelColor);
font-size: 26px;
line-height: 48px;
text-align: center;
user-select: none;
}
.AvatarBubble.large {
height: 64px;
width: 64px;
line-height: 64px;
font-size: 39px;
}
.AvatarBubble--NoSearchResults {
transform: rotate(45deg);
line-height: 46px;
letter-spacing: 1px;
}
.AvatarBubble--NoSearchResults::after {
content: ":-(";
}
.AvatarImage {
position: relative;
z-index: 2;
object-fit: cover;
height: 48px;
width: 48px;
margin-top: 8px;
margin-bottom: 8px;
border-radius: 100%;
user-select: none;
}
.AvatarImage.large {
height: 64px;
width: 64px;
}
.attachment-overlay .attachment-view {
height: 100%;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: #313131;
}
.attachment-overlay .attachment-view img,
.attachment-overlay .attachment-view video {
width: 100vw;
max-height: 100vh;
object-fit: contain;
}
.attachment-overlay .attachment-view video {
width: 95vw;
}
.attachment-overlay .render-media-wrapper {
width: 100vw;
height: 100vh;
}
.attachment-overlay .btn-wrapper {
float: right;
position: absolute;
z-index: 10;
cursor: pointer;
}
.attachment-overlay .download-btn {
height: 36px;
width: 36px;
display: inline-block;
-webkit-mask: url("../images/download.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageButtons);
}
.attachment-overlay .download-btn:hover {
background-color: var(--messageButtons);
}
.message-attachment-media {
text-align: center;
position: relative;
cursor: pointer;
margin-left: -12px;
margin-right: -12px;
margin-top: -10px;
margin-bottom: -10px;
border-radius: 16px;
overflow: hidden;
background-color: var(--messageAttachmentIconBg);
}
.message-attachment-media > .attachment-content {
object-fit: scale-down;
object-position: center;
min-height: 150px;
max-height: 300px;
max-width: 40vw;
margin-bottom: -4px;
cursor: pointer;
}
.message-attachment-media > .video-play-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 48px;
height: 48px;
background-color: var(--videoPlayBtnBg);
border-radius: 24px;
}
.message-attachment-media > .video-play-btn > .video-play-btn-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 36px;
width: 36px;
-webkit-mask: url("../images/play.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--videoPlayBtnIcon);
}
.message-attachment-media.content-below {
margin-bottom: 7px;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.message-attachment-media.content-above {
margin-top: 4px;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
.message-attachment-broken-media {
font-size: 11px;
line-height: 16px;
letter-spacing: 0.3px;
padding: 10px;
text-align: center;
text-transform: uppercase;
color: var(--brokenMediaBg);
}
.message-attachment-broken-media.incoming {
color: var(--brokenMediaText);
}
.message-attachment-audio {
margin-top: 2px;
display: block;
margin-right: 30px;
}
.message-attachment-audio.content-below {
margin-bottom: 5px;
}
.message-attachment-audio.content-above {
margin-top: 6px;
}
.message-attachment-generic {
display: flex;
flex-direction: row;
}
.message-attachment-generic.content-below {
padding-bottom: 6px;
}
.message-attachment-generic.content-above {
padding-top: 4px;
}
.message-attachment-generic > .file-icon {
background: url("../images/file-gradient.svg") no-repeat center;
height: 44px;
width: 56px;
margin-left: -13px;
margin-right: -14px;
margin-bottom: -4px;
display: flex;
flex-direction: row;
align-items: center;
}
.message-attachment-generic > .file-icon > .file-extension {
font-size: 10px;
line-height: 13px;
letter-spacing: 0.1px;
text-transform: uppercase;
text-align: center;
width: 25px;
margin-left: auto;
margin-right: auto;
overflow-x: hidden;
white-space: nowrap;
text-overflow: clip;
color: var(--messageAttachmentIconExtentionColor);
font-family: monospace;
}
.message-attachment-generic > .text-part {
flex-grow: 1;
margin-left: 8px;
max-width: calc(100% - 37px);
}
.message-attachment-generic > .text-part > .name {
color: var(--messageAttachmentFileInfo);
font-size: 14px;
line-height: 18px;
font-weight: 300;
margin-top: 2px;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.message-attachment-generic > .text-part > .size {
color: var(--messageAttachmentFileInfo);
font-size: 11px;
line-height: 16px;
letter-spacing: 0.3px;
margin-top: 3px;
}
.module-message-detail {
margin-top: -20px;
}
.module-message-detail .bp3-callout {
max-height: 50vh;
overflow: auto;
}
.module-message-detail p {
white-space: pre-line;
user-select: text;
}
.module-message-detail__message-container {
padding-top: 20px;
padding-bottom: 20px;
}
.module-message-detail__message-container:after {
content: ".";
visibility: hidden;
display: block;
height: 0;
clear: both;
}
.module-message-detail__label {
font-weight: 300;
padding-right: 5px;
}
.module-message-detail__unix-timestamp {
color: #eeefef;
}
.module-message-detail__delete-button-container {
text-align: center;
margin-top: 10px;
}
.module-message-detail__delete-button {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
background-color: #f44336;
color: #fff;
box-shadow: 0 0 10px -3px rgba(97, 97, 97, 0.7);
border-radius: 5px;
border: solid 1px #a4a6a9;
cursor: pointer;
margin: 1em auto;
padding: 1em;
}
.module-message-detail .message-content * {
background-color: lightgrey;
width: 100%;
resize: none;
padding: 1rem;
}
.message-list-and-composer {
width: 70%;
float: right;
display: grid;
grid-template-columns: auto;
height: calc(100vh - 50px);
margin-top: 50px;
background-image: var(--chatViewBgImgPath);
background-size: cover;
background-color: var(--chatViewBg);
}
.message-list-and-composer__message-list #message-list {
background: #dbdbdb;
position: absolute;
bottom: 0;
overflow: scroll;
max-height: 100%;
width: 100%;
padding: 0 0.5em;
top: 52px;
}
.message-list-and-composer__message-list
#message-list::-webkit-scrollbar-track {
background: transparent;
}
.message-list-and-composer__message-list ul {
list-style: none;
min-width: 200px;
}
.message-list-and-composer__message-list ul li {
margin-bottom: 10px;
min-width: 200px;
}
.message-list-and-composer__message-list ul li::after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.message-list-and-composer__message-list ul li .info-message {
max-width: 550px;
font-size: 1rem;
padding: 2rem;
font-style: normal;
white-space: pre-wrap;
text-align: left;
}
.message {
position: relative;
display: inline-flex;
flex-direction: row;
align-items: stretch;
}
.message:hover .message-buttons {
opacity: 1;
}
.message > .author-avatar {
align-self: flex-end;
bottom: 0px;
position: static;
margin-right: 8px;
user-select: none;
}
.message > .author-avatar img {
height: 36px;
width: 36px;
border-radius: 18px;
object-fit: cover;
}
.message > .author-avatar.default {
text-align: center;
}
.message > .author-avatar.default > .label {
user-select: none;
color: var(--avatarLabelColor);
top: -121px;
left: -10px;
border-radius: 50%;
width: 36px;
height: 36px;
font-size: 25px;
line-height: 36px;
}
.message .message-buttons {
position: absolute;
top: 5px;
right: -4px;
display: inline-flex;
flex-direction: row;
align-items: center;
opacity: 0;
z-index: 10;
user-select: text;
}
.message .message-buttons .msg-button {
height: 24px;
width: 24px;
display: inline-block;
cursor: pointer;
}
.message .message-buttons .msg-button:hover {
background-color: var(--messageButtons);
}
.message .message-buttons .msg-button.download {
-webkit-mask: url("../images/download.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageButtons);
}
.message .message-buttons .msg-button.reply {
display: none;
-webkit-mask: url("../images/reply.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageButtons);
user-select: none;
}
.message .message-buttons .msg-button.menu {
-webkit-mask: url("../images/ellipsis.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageButtons);
transform: rotate(90deg);
-webkit-mask-position-y: 4px;
user-select: none;
}
.message .msg-container {
position: relative;
display: inline-block;
border-radius: 16px;
padding-right: 12px;
padding-left: 12px;
padding-top: 10px;
padding-bottom: 10px;
}
.message .msg-container > .author {
display: inline-block;
max-width: 40vw;
font-size: 13px;
font-weight: 300;
line-height: 18px;
height: 18px;
overflow-x: hidden;
overflow-y: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.message .msg-container .msg-body.msg-body--clickable {
cursor: pointer;
}
.message .msg-container .msg-body > .text {
color: var(--messageText);
font-size: 14px;
line-height: 18px;
text-align: start;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
white-space: pre-wrap;
margin-right: 10px;
}
.message .msg-container .msg-body > .text a {
text-decoration: underline;
color: var(--messageTextLink);
}
.message .msg-container .msg-body > .text .double-line-break {
height: 28px;
}
.message .msg-container .msg-body > .text .line-break {
height: 14px;
}
.message .msg-container .msg-body > .text .line-break:last-child {
height: 0px;
}
.message .metadata {
margin-top: 10px;
margin-bottom: -7px;
float: right;
}
.message .module-message__img-attachment {
object-fit: cover;
width: auto;
max-width: 100%;
min-height: unset;
}
.message.incoming {
margin-left: 0;
margin-right: 32px;
}
.message.incoming .metadata:not(.with-image-no-caption) > .padlock-icon {
-webkit-mask: url("../images/padlock.svg") no-repeat center;
-webkit-mask-size: 125%;
background-color: var(--messagePadlockIncomming);
}
.message.incoming .metadata:not(.with-image-no-caption) > .location-icon {
-webkit-mask: url("../images/map-marker.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messagePadlockIncomming);
}
.message.incoming .metadata:not(.with-image-no-caption) > .date {
color: var(--messageMetadataIncomming);
}
.message.incoming .msg-container {
background-color: var(--messageIncommingBg);
}
.message.incoming .msg-container,
.message.incoming .msg-container .message-attachment-media {
border-bottom-left-radius: 1px;
}
.message.outgoing {
float: right;
margin-right: 0;
margin-left: 32px;
}
.message.outgoing .metadata > .date {
color: var(--messageOutgoingStatusColor);
}
.message.outgoing .metadata > .padlock-icon {
-webkit-mask: url("../images/padlock.svg") no-repeat center;
-webkit-mask-size: 125%;
background-color: var(--messagePadlockOutgoing);
}
.message.outgoing .metadata > .location-icon {
-webkit-mask: url("../images/map-marker.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messagePadlockOutgoing);
}
.message.outgoing .metadata > .status-icon.read,
.message.outgoing .metadata > .status-icon.delivered {
background-color: var(--messageOutgoingStatusColor);
}
.message.outgoing .msg-container {
background-color: var(--messageOutgoingBg);
}
.message.outgoing .msg-container,
.message.outgoing .msg-container .message-attachment-media {
border-bottom-right-radius: 1px;
}
.message.type-sticker .msg-container {
background-color: transparent !important;
}
.message.type-sticker .message-attachment-media {
background-color: transparent;
}
.message.type-sticker .message-attachment-media > .attachment-content {
margin-bottom: 20px;
}
.message.type-sticker .metadata {
float: right;
padding: 4px 10px 1px 10px;
margin-bottom: -7px;
background-color: #01010159;
border-radius: 4px;
color: black;
font-weight: bold;
}
.message.type-sticker .metadata > .date {
font-size: 11px;
color: white;
}
.message.type-sticker .metadata > .padlock-icon {
-webkit-mask: url("../images/padlock.svg") no-repeat center;
-webkit-mask-size: 125%;
background-color: #fff;
}
.message.type-sticker .metadata > .location-icon {
-webkit-mask: url("../images/map-marker.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: #fff;
}
.message.type-sticker .status-icon.read,
.message.type-sticker .status-icon.delivered {
background-color: white;
}
.message.type-sticker:hover .msg-button.menu {
background-color: white;
}
.message.type-sticker:hover .react-contextmenu-wrapper {
background-color: #2525258f;
border-radius: 4px;
}
.message.error.incoming .text {
font-style: italic;
}
.message.forwarded .forwarded-indicator {
font-weight: bold;
font-size: 0.9em;
margin-bottom: 3px;
opacity: 0.86;
}
.message.forwarded .message-attachment-media {
border-top-left-radius: 0;
border-top-right-radius: 0;
margin-top: 0;
}
.setupMessage .message .text {
color: var(--setupMessageText);
}
.hide-on-small {
display: initial;
}
@media (max-width: 800px) {
.hide-on-small {
display: none;
}
}
@media (min-width: 800px) and (max-width: 925px) {
.message {
max-width: 374px;
}
.message.incoming {
margin-right: auto;
}
.message.outgoing {
margin-left: auto;
}
}
@media (min-width: 926px) {
.message {
max-width: 66%;
}
.message.incoming {
margin-right: auto;
}
.message.outgoing {
margin-left: auto;
}
}
.metadata {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 3px;
margin-bottom: -3px;
}
.metadata.with-image-no-caption {
position: absolute;
right: 5px;
bottom: 5px;
float: right;
padding: 4px 10px 1px 10px;
margin: 0;
background-color: #0000008f;
border-radius: 4px;
font-weight: bold;
}
.metadata.with-image-no-caption > .date {
color: white;
}
.metadata.with-image-no-caption > .padlock-icon {
-webkit-mask: url("../images/padlock.svg") no-repeat center;
-webkit-mask-size: 125%;
background-color: #fff;
}
.metadata.with-image-no-caption .status-icon.sending {
background-color: white;
}
.metadata > .status-icon {
margin-bottom: 2px;
}
.metadata > .username {
margin-right: 10px;
}
.metadata > .date {
font-size: 11.5px;
line-height: 16px;
letter-spacing: 0.3px;
color: var(--messageMetadataDate);
text-transform: uppercase;
}
.metadata > .spacer {
flex-grow: 1;
}
.metadata > .padlock-icon,
.metadata > .location-icon {
width: 12px;
height: 12px;
display: inline-block;
margin-right: 2px;
margin-bottom: 3px;
}
.metadata > .location-icon {
margin-bottom: 0;
}
@keyframes __status-icon--spinning {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.status-icon {
width: 18px;
height: 12px;
display: inline-block;
margin-left: 2px;
}
.status-icon.sending {
-webkit-mask: url("../images/sending.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageStatusIconSending);
animation: __status-icon--spinning 4s linear infinite;
width: 12px;
margin-left: 8px;
}
.status-icon.delivered {
-webkit-mask: url("../images/sent.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageStatusIcon);
}
.status-icon.read {
-webkit-mask: url("../images/read.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageStatusIcon);
}
.status-icon.error {
-webkit-mask: url("../images/error.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--errorColor);
width: 12px;
margin-left: 8px;
}

View File

@@ -1,5 +1,6 @@
use std::path::Path;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
use deltachat::chatlist::*;
@@ -9,6 +10,7 @@ use deltachat::context::*;
use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::error::Error;
use deltachat::export_chat::{export_chat, pack_exported_chat};
use deltachat::imex::*;
use deltachat::job::*;
use deltachat::location;
@@ -374,6 +376,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
pin <chat-id>\n\
unpin <chat-id>\n\
delchat <chat-id>\n\
export-chat <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
msginfo <msg-id>\n\
@@ -868,6 +871,24 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let chat_id = ChatId::new(arg1.parse()?);
chat_id.delete(context)?;
}
"export-chat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
let res = export_chat(context, chat_id);
println!("{:?}", res);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let destination_raw = context.get_blobdir().join(format!(
"exported_{}_{}.zip",
chat_id.to_u32(),
timestamp
));
let destination = destination_raw.to_str().unwrap();
let pack_res = pack_exported_chat(context, res, destination);
println!("{:?} - destination: {}", pack_res, destination);
}
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);

View File

@@ -262,7 +262,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 26] = [
const CHAT_COMMANDS: [&str; 27] = [
"listchats",
"listarchived",
"chat",
@@ -289,6 +289,7 @@ const CHAT_COMMANDS: [&str; 26] = [
"pin",
"unpin",
"delchat",
"export-chat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
"listmsgs",

345
src/export_chat.rs Normal file
View File

@@ -0,0 +1,345 @@
// use crate::dc_tools::*;
use crate::chat::*;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::*;
use crate::context::Context;
use crate::error::Error;
use crate::message::*;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use zip::write::FileOptions;
#[derive(Debug)]
pub struct ExportChatResult {
html: String,
referenced_blobs: Vec<String>,
}
struct ContactInfo {
name: String,
initial: String,
color: String,
profile_img: Option<String>,
}
pub fn pack_exported_chat(
context: &Context,
artifact: ExportChatResult,
filename: &str,
) -> zip::result::ZipResult<()> {
let path = std::path::Path::new(filename);
let file = std::fs::File::create(&path).unwrap();
let mut zip = zip::ZipWriter::new(file);
zip.start_file("index.html", Default::default())?;
zip.write_all(artifact.html.as_bytes())?;
zip.start_file("styles.css", Default::default())?;
zip.write_all(include_bytes!("../assets/exported-chat.css"))?;
zip.add_directory("blobs/", Default::default())?;
let options = FileOptions::default();
for blob_name in artifact.referenced_blobs {
let path = context.get_blobdir().join(&blob_name);
// println!("adding file {:?} as {:?} ...", path, &blob_name);
zip.start_file_from_path(Path::new(&format!("blobs/{}", &blob_name)), options)?;
let mut f = File::open(path)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
zip.write_all(&*buffer)?;
buffer.clear();
}
zip.finish()?;
Ok(())
}
pub fn export_chat(context: &Context, chat_id: ChatId) -> ExportChatResult {
let mut blobs = Vec::new();
let mut chat_author_ids = Vec::new();
// get all messages
let messages: Vec<std::result::Result<Message, Error>> =
get_chat_msgs(context, chat_id, 0, None)
.into_iter()
.map(|msg_id| Message::load_from_db(context, msg_id))
.collect();
// push all referenced blobs and populate contactid list
for message in &messages {
if let Ok(msg) = &message {
let filename = msg.get_filename();
if let Some(file) = filename {
// push referenced blobs (attachments)
blobs.push(file);
}
chat_author_ids.push(msg.from_id);
}
}
// deduplicate contact list and load the contacts
chat_author_ids.dedup();
// chache information about the authors
let mut chat_authors: HashMap<u32, ContactInfo> = HashMap::new();
chat_authors.insert(
0,
ContactInfo {
name: "Err: Contact not found".to_owned(),
initial: "#".to_owned(),
profile_img: None,
color: "grey".to_owned(),
},
);
for author_id in chat_author_ids {
let contact = Contact::get_by_id(context, author_id);
if let Ok(c) = contact {
let profile_img_path: String;
if let Some(path) = c.get_profile_image(context) {
profile_img_path = path
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
.to_owned();
// push referenced blobs (avatars)
blobs.push(profile_img_path.clone());
} else {
profile_img_path = "".to_owned();
}
chat_authors.insert(
author_id,
ContactInfo {
name: c.get_display_name().to_owned(),
initial: "#".to_owned(), // TODO
profile_img: match profile_img_path != "" {
true => Some(profile_img_path),
false => None,
},
color: "rgb(18, 126, 208)".to_owned(), // TODO
},
);
}
}
// run message_to_html for each message and generate the html that way
let mut html_messages: Vec<String> = Vec::new();
for message in messages {
if let Ok(msg) = message {
html_messages.push(message_to_html(&chat_authors, msg, context));
} else {
html_messages.push(format!(
r#"<li>
<div class='message error'>
<div class="msg-container">
<div class="msg-body">
<div dir="auto" class="text">{:?}</div>
</div>
</div>
</div>
</li>"#,
message.unwrap_err()
));
}
}
// todo chat image, chat name and so on..
let chat = Chat::load_from_db(context, chat_id).unwrap();
let chat_avatar = match chat.get_profile_image(context) {
Some(img) => {
let path = img
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
.to_owned();
blobs.push(path.clone());
format!("<img class=\"avatar\" src=\"blobs/{}\" />", path)
}
None => format!(
"<div class=\"avatar text-avatar\" style=\"background-color:#{:#}\">{}</div>",
chat.get_color(context),
chat.get_name().chars().next().unwrap()
),
};
// todo option to export locations as kml?
// todo export message infos and save them to txt files
// (those can be linked from the messages, they are stored in msg_info/[msg-id].txt)
blobs.dedup();
ExportChatResult {
html: format!(
"<html>\
<head>\
<title>{chat_name}</title>\
<link rel=\"stylesheet\" href=\"styles.css\" type=\"text/css\">\
</head>\
<body>\
<div class=\"header\">\
{chat_avatar}\
<div class=\"name\">{chat_name}</div>\
</div>\
<div class=\"message-list-and-composer__message-list\">\
<div id=\"message-list\">\
<ul>{messages}</ul>\
</div>\
</div>\
</body>\
</html>",
chat_name = chat.get_name(),
chat_avatar = chat_avatar,
messages = html_messages.join("")
),
referenced_blobs: blobs,
}
}
fn message_to_html(
author_cache: &HashMap<u32, ContactInfo>,
message: Message,
context: &Context,
) -> String {
let author: &ContactInfo = {
if let Some(c) = author_cache.get(&message.get_from_id()) {
c
} else {
author_cache.get(&0).unwrap()
}
};
let avatar: String = {
if let Some(profile_img) = &author.profile_img {
format!(
"<div class=\"author-avatar\">\
<img \
alt=\"{author_name}\"\
src=\"blobs/{author_avatar_src}\"\
/>\
</div>",
author_name = author.name,
author_avatar_src = profile_img
)
} else {
format!(
"<div class=\"author-avatar default\" alt=\"{name}\">\
<div class=\"label\" style=\"background-color: {color}\">\
{initial}\
</div>\
</div>",
name = author.name,
initial = author.initial,
color = author.color
)
}
};
// save and refernce message source code somehow?
let has_text = message.get_text().is_some() && !message.get_text().unwrap().is_empty();
let attachment = match message.get_file(context) {
None => "".to_owned(),
Some(file) => {
let modifier_class = if has_text { "content-below" } else { "" };
let filename = file
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
.to_owned();
match message.get_viewtype() {
Viewtype::Audio => {
format!("<audio \
controls \
class=\"message-attachment-audio {}\"> \
<source src=\"blobs/{}\" /> \
</audio>", modifier_class ,filename)
},
Viewtype::Gif | Viewtype::Image | Viewtype::Sticker => {
format!("<a \
href=\"blobs/{filename}\" \
role=\"button\" \
class=\"message-attachment-media {modifier_class}\"> \
<img className='attachment-content' src=\"blobs/{filename}\" /> \
</a>", modifier_class=modifier_class, filename=filename)
},
Viewtype::Video => {
format!("<a \
href=\"blobs/{filename}\" \
role=\"button\" \
class=\"message-attachment-media {modifier_class}\"> \
<video className='attachment-content' src=\"blobs/{filename}\" controls=\"true\" /> \
</a>", modifier_class=modifier_class, filename=filename)
},
_ => {
format!("<div class=\"message-attachment-generic {modifier_class}\">\
<div class=\"file-icon\">\
<div class=\"file-extension\">\
{extension} \
</div>\
</div>\
<div className=\"text-part\">\
<a href=\"blobs/{filename}\" className=\"name\">{filename}</a>\
<div className=\"size\">{filesize}</div>\
</div>\
</div>",
modifier_class=modifier_class,
filename=filename,
filesize=message.get_filebytes(&context) /* todo human readable file size*/,
extension=file.extension().unwrap_or_else(|| std::ffi::OsStr::new("")).to_str().unwrap().to_owned())
}
}
}
};
format!(
"<li>\
<div class=\"message {direction}\">\
{avatar}\
<div class=\"msg-container\">\
<span class=\"author\" style=\"color: {author_color};\">{author_name}</span>\
<div class=\"msg-body\">\
{attachment}
<div dir=\"auto\" class=\"text\">\
{content}\
</div>\
<div class=\"metadata {with_image_no_caption}\">\
{encryption}\
<span class=\"date date--{direction}\" title=\"{full_time}\">{relative_time}</span>\
<span class=\"spacer\"></span>\
</div>\
</div>\
</div>\
<div>\
</li>",
direction = match message.from_id == DC_CONTACT_ID_SELF {
true => "outgoing",
false => "incoming",
},
avatar = avatar,
author_name = author.name,
author_color = author.color,
attachment = attachment,
content = message.get_text().unwrap_or_else(|| "".to_owned()),
with_image_no_caption = if !has_text && message.get_viewtype() == Viewtype::Image {
"with-image-no-caption"
} else {
""
},
encryption = match message.get_showpadlock() {
true => r#"<div aria-label="Encryption padlock" class="padlock-icon"></div>"#,
false => "",
},
full_time = "Tue, Feb 25, 2020 3:49 PM", // message.get_timestamp() ? // todo
relative_time = "Tue 3:49 PM" // todo
)
// todo link to raw message data
// todo link to message info
}
//TODO tests

View File

@@ -36,6 +36,7 @@ pub mod constants;
pub mod contact;
pub mod context;
mod e2ee;
pub mod export_chat;
mod imap;
pub mod imex;
#[macro_use]