diff --git a/Cargo.lock b/Cargo.lock index 631d1a0c3..c206d725a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -637,6 +637,27 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bzip2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.10+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17fa3d1ac1ca21c5c4e36a97f3c3eb25084576f6fc47bf0139c1123434216c6c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cache-padded" version = "1.1.1" @@ -1175,6 +1196,7 @@ dependencies = [ "toml", "url", "uuid", + "zip", ] [[package]] @@ -4212,3 +4234,17 @@ dependencies = [ "syn", "synstructure", ] + +[[package]] +name = "zip" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c83dc9b784d252127720168abd71ea82bf8c3d96b17dc565b5e2a02854f2b27" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "flate2", + "thiserror", + "time 0.1.44", +] diff --git a/Cargo.toml b/Cargo.toml index a97b73cd8..481ef7065 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ thiserror = "1.0.25" toml = "0.5.6" url = "2.2.2" uuid = { version = "0.8", features = ["serde", "v4"] } +zip = "0.5.12" [dev-dependencies] ansi_term = "0.12.0" diff --git a/assets/exported-chat.css b/assets/exported-chat.css new file mode 100644 index 000000000..3776f83c9 --- /dev/null +++ b/assets/exported-chat.css @@ -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; +} diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index e6baac1fe..1ed9b75e5 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -1,6 +1,7 @@ extern crate dirs; use std::str::FromStr; +use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{bail, ensure, Error}; use async_std::path::Path; @@ -13,6 +14,8 @@ use deltachat::contact::*; 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::location; use deltachat::log::LogExt; @@ -387,6 +390,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu protect \n\ unprotect \n\ delchat \n\ + export-chat \n\ ===========================Contact requests==\n\ decidestartchat \n\ decideblock \n\ @@ -1022,6 +1026,24 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu let chat_id = ChatId::new(arg1.parse()?); chat_id.delete(&context).await?; } + "export-chat" => { + ensure!(!arg1.is_empty(), "Argument 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 missing."); let id = MsgId::new(arg1.parse()?); diff --git a/examples/repl/main.rs b/examples/repl/main.rs index 328389870..0890664e8 100644 --- a/examples/repl/main.rs +++ b/examples/repl/main.rs @@ -169,7 +169,7 @@ const DB_COMMANDS: [&str; 9] = [ "housekeeping", ]; -const CHAT_COMMANDS: [&str; 34] = [ +const CHAT_COMMANDS: [&str; 35] = [ "listchats", "listarchived", "chat", @@ -204,6 +204,7 @@ const CHAT_COMMANDS: [&str; 34] = [ "protect", "unprotect", "delchat", + "export-chat", ]; const MESSAGE_COMMANDS: [&str; 6] = [ "listmsgs", diff --git a/src/export_chat.rs b/src/export_chat.rs new file mode 100644 index 000000000..f1b0f9e00 --- /dev/null +++ b/src/export_chat.rs @@ -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, +} + +struct ContactInfo { + name: String, + initial: String, + color: String, + profile_img: Option, +} + +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> = + 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 = 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 = 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#"
  • +
    +
    +
    +
    {:?}
    +
    +
    +
    +
  • "#, + 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!("", path) + } + None => format!( + "
    {}
    ", + 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!( + "\ + \ + {chat_name}\ + \ + \ + \ +
    \ + {chat_avatar}\ +
    {chat_name}
    \ +
    \ +
    \ +
    \ +
      {messages}
    \ +
    \ +
    \ + \ + ", + chat_name = chat.get_name(), + chat_avatar = chat_avatar, + messages = html_messages.join("") + ), + referenced_blobs: blobs, + } +} + +fn message_to_html( + author_cache: &HashMap, + 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!( + "
    \ + \"{author_name}\"\\ +
    ", + author_name = author.name, + author_avatar_src = profile_img + ) + } else { + format!( + "
    \ +
    \ + {initial}\ +
    \ +
    ", + 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!("", modifier_class ,filename) + }, + Viewtype::Gif | Viewtype::Image | Viewtype::Sticker => { + format!(" \ + \ + ", modifier_class=modifier_class, filename=filename) + }, + Viewtype::Video => { + format!(" \ + ", modifier_class=modifier_class, filename=filename) + }, + _ => { + format!("
    \ +
    \ +
    \ + {extension} \ +
    \ +
    \ +
    \ + {filename}\ +
    {filesize}
    \ +
    \ +
    ", + 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!( + "
  • \ +
    \ + {avatar}\ +
    \ + {author_name}\ +
    \ + {attachment} +
    \ + {content}\ +
    \ + \ +
    \ +
    \ +
    \ +
  • ", + 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#"
    "#, + 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 diff --git a/src/lib.rs b/src/lib.rs index eb40102b1..f5af8e787 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,7 @@ pub mod contact; pub mod context; mod e2ee; pub mod ephemeral; +pub mod export_chat; mod imap; pub mod imex; mod scheduler;