mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-09-20 07:35:55 +08:00
Base idea to use a dialog to view messages
This commit is contained in:
parent
2916583d0b
commit
76be9de5a2
|
@ -22,6 +22,9 @@ import { LanguageStore } from 'Stores/Language';
|
|||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
import { MessagePopupView } from 'View/Popup/Message';
|
||||
|
||||
const
|
||||
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
|
||||
|
||||
|
@ -347,6 +350,7 @@ export class MessageModel extends AbstractModel {
|
|||
* @param {boolean=} print = false
|
||||
*/
|
||||
popupMessage(print) {
|
||||
if (print) {
|
||||
const
|
||||
timeStampInUTC = this.dateTimestamp() || 0,
|
||||
ccLine = this.cc.toString(),
|
||||
|
@ -375,6 +379,9 @@ export class MessageModel extends AbstractModel {
|
|||
);
|
||||
sdoc.close();
|
||||
(true === print) && setTimeout(() => win.print(), 100);
|
||||
} else {
|
||||
showScreenPopup(MessagePopupView, [this]);
|
||||
}
|
||||
}
|
||||
|
||||
printMessage() {
|
||||
|
|
453
dev/View/Popup/Message.js
Normal file
453
dev/View/Popup/Message.js
Normal file
|
@ -0,0 +1,453 @@
|
|||
import ko from 'ko';
|
||||
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||
|
||||
import { ScopeMessageList, ScopeMessageView } from 'Common/Enums';
|
||||
|
||||
import {
|
||||
ComposeType,
|
||||
ClientSideKeyNameMessageHeaderFullInfo,
|
||||
ClientSideKeyNameMessageAttachmentControls
|
||||
} from 'Common/EnumsUser';
|
||||
import { RFC822 } from 'Common/File';
|
||||
import {
|
||||
keyScopeReal,
|
||||
Settings,
|
||||
SettingsCapa,
|
||||
stopEvent,
|
||||
addShortcut,
|
||||
registerShortcut
|
||||
} from 'Common/Globals';
|
||||
|
||||
import { arrayLength } from 'Common/Utils';
|
||||
import { download, downloadZip, mailToHelper, showMessageComposer, moveAction } from 'Common/UtilsUser';
|
||||
import { isFullscreen, exitFullscreen, toggleFullscreen } from 'Common/Fullscreen';
|
||||
|
||||
import { SMAudio } from 'Common/Audio';
|
||||
|
||||
import { i18n } from 'Common/Translator';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
import { FolderUserStore, isAllowedKeyword } from 'Stores/User/Folder';
|
||||
|
||||
import * as Local from 'Storage/Client';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||
|
||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
||||
|
||||
import { MimeToMessage } from 'Mime/Utils';
|
||||
|
||||
import { MessageModel } from 'Model/Message';
|
||||
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
||||
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
||||
|
||||
const
|
||||
fetchRaw = url => rl.fetch(url).then(response => response.ok && response.text());
|
||||
|
||||
export class MessagePopupView extends AbstractViewPopup {
|
||||
constructor() {
|
||||
super('Message');
|
||||
|
||||
const
|
||||
/**
|
||||
* @param {Function} fExecute
|
||||
* @param {Function} fCanExecute = true
|
||||
* @returns {Function}
|
||||
*/
|
||||
createCommand = (fExecute, fCanExecute) => {
|
||||
let fResult = () => {
|
||||
fCanExecute() && fExecute.call(null);
|
||||
return false;
|
||||
};
|
||||
fResult.canExecute = fCanExecute;
|
||||
return fResult;
|
||||
},
|
||||
|
||||
createCommandReplyHelper = type =>
|
||||
createCommand(() => this.replyOrforward(type), ()=>1);
|
||||
|
||||
this.msgDefaultAction = SettingsUserStore.msgDefaultAction;
|
||||
this.simpleAttachmentsList = SettingsUserStore.simpleAttachmentsList;
|
||||
|
||||
addObservablesTo(this, {
|
||||
message: null,
|
||||
showAttachmentControls: !!Local.get(ClientSideKeyNameMessageAttachmentControls),
|
||||
downloadAsZipLoading: false,
|
||||
showFullInfo: '1' === Local.get(ClientSideKeyNameMessageHeaderFullInfo),
|
||||
// viewer
|
||||
viewFromShort: '',
|
||||
dkimData: ['none', '', '']
|
||||
});
|
||||
|
||||
this.moveAction = moveAction;
|
||||
|
||||
const attachmentsActions = Settings.app('attachmentsActions');
|
||||
this.attachmentsActions = ko.observableArray(arrayLength(attachmentsActions) ? attachmentsActions : []);
|
||||
|
||||
this.fullScreenMode = isFullscreen;
|
||||
this.toggleFullScreen = toggleFullscreen;
|
||||
|
||||
this.downloadAsZipError = ko.observable(false).extend({ falseTimeout: 7000 });
|
||||
|
||||
this.messageDomFocused = ko.observable(false).extend({ rateLimit: 0 });
|
||||
|
||||
// viewer
|
||||
this.viewHash = '';
|
||||
|
||||
addComputablesTo(this, {
|
||||
allowAttachmentControls: () => arrayLength(attachmentsActions) && SettingsCapa('AttachmentsActions'),
|
||||
|
||||
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
|
||||
&& (this.message()?.attachments || [])
|
||||
.filter(item => item?.download /*&& !item?.isLinked()*/ && item?.checked())
|
||||
.length,
|
||||
|
||||
tagsAllowed: () => FolderUserStore.currentFolder()?.tagsAllowed(),
|
||||
|
||||
tagsToHTML: () => this.message()?.flags().map(value =>
|
||||
isAllowedKeyword(value)
|
||||
? '<span class="focused msgflag-'+value+'">' + i18n('MESSAGE_TAGS/'+value,0,value) + '</span>'
|
||||
: ''
|
||||
).join(' '),
|
||||
|
||||
listAttachments: () => this.message()?.attachments()
|
||||
.filter(item => SettingsUserStore.listInlineAttachments() || !item.isLinked()),
|
||||
hasAttachments: () => this.listAttachments()?.length,
|
||||
|
||||
viewDkimIcon: () => 'none' !== this.dkimData()[0],
|
||||
|
||||
dkimIconClass:() => {
|
||||
switch (this.dkimData()[0]) {
|
||||
case 'none':
|
||||
return '';
|
||||
case 'pass':
|
||||
return 'icon-ok iconcolor-green'; // ✔️
|
||||
default:
|
||||
return 'icon-cross iconcolor-red'; // ✖ ❌
|
||||
}
|
||||
},
|
||||
|
||||
dkimTitle:() => {
|
||||
const dkim = this.dkimData();
|
||||
return dkim[0] ? dkim[2] || 'DKIM: ' + dkim[0] : '';
|
||||
},
|
||||
|
||||
showWhitelistOptions: () => 'match' === SettingsUserStore.viewImages(),
|
||||
|
||||
firstUnsubsribeLink: () => this.message()?.unsubsribeLinks()[0] || '',
|
||||
|
||||
pgpSupported: () => this.message() && PgpUserStore.isSupported()
|
||||
});
|
||||
|
||||
addSubscribablesTo(this, {
|
||||
message: message => {
|
||||
if (message) {
|
||||
this.viewHash = message.hash;
|
||||
// TODO: make first param a user setting #683
|
||||
this.viewFromShort(message.from.toString(false, true));
|
||||
this.dkimData(message.dkim[0] || ['none', '', '']);
|
||||
} else {
|
||||
this.viewHash = '';
|
||||
}
|
||||
},
|
||||
|
||||
showFullInfo: value => Local.set(ClientSideKeyNameMessageHeaderFullInfo, value ? '1' : '0')
|
||||
});
|
||||
|
||||
// commands
|
||||
this.replyCommand = createCommandReplyHelper(ComposeType.Reply);
|
||||
this.replyAllCommand = createCommandReplyHelper(ComposeType.ReplyAll);
|
||||
this.forwardCommand = createCommandReplyHelper(ComposeType.Forward);
|
||||
this.forwardAsAttachmentCommand = createCommandReplyHelper(ComposeType.ForwardAsAttachment);
|
||||
this.editAsNewCommand = createCommandReplyHelper(ComposeType.EditAsNew);
|
||||
}
|
||||
|
||||
toggleFullInfo() {
|
||||
this.showFullInfo(!this.showFullInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sType
|
||||
* @returns {void}
|
||||
*/
|
||||
replyOrforward(sType) {
|
||||
showMessageComposer([sType, this.message()]);
|
||||
}
|
||||
|
||||
onBuild(dom) {
|
||||
const eqs = (ev, s) => ev.target.closestWithin(s, dom);
|
||||
dom.addEventListener('click', event => {
|
||||
let el = eqs(event, 'a');
|
||||
if (el && 0 === event.button && mailToHelper(el.href)) {
|
||||
stopEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
el = eqs(event, '.attachmentsPlace .showPreview');
|
||||
if (el) {
|
||||
const attachment = ko.dataFor(el), url = attachment?.linkDownload();
|
||||
// if (url && FileType.Eml === attachment.fileType) {
|
||||
if (url && RFC822 == attachment.mimeType) {
|
||||
stopEvent(event);
|
||||
fetchRaw(url).then(text => {
|
||||
const oMessage = new MessageModel();
|
||||
MimeToMessage(text, oMessage);
|
||||
// cleanHTML
|
||||
oMessage.popupMessage();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
el = eqs(event, '.attachmentsPlace .showPreplay');
|
||||
if (el) {
|
||||
stopEvent(event);
|
||||
const attachment = ko.dataFor(el);
|
||||
if (attachment && SMAudio.supported) {
|
||||
switch (true) {
|
||||
case SMAudio.supportedMp3 && attachment.isMp3():
|
||||
SMAudio.playMp3(attachment.linkDownload(), attachment.fileName);
|
||||
break;
|
||||
case SMAudio.supportedOgg && attachment.isOgg():
|
||||
SMAudio.playOgg(attachment.linkDownload(), attachment.fileName);
|
||||
break;
|
||||
case SMAudio.supportedWav && attachment.isWav():
|
||||
SMAudio.playWav(attachment.linkDownload(), attachment.fileName);
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
el = eqs(event, '.attachmentItem');
|
||||
if (el) {
|
||||
const attachment = ko.dataFor(el), url = attachment?.linkDownload();
|
||||
if (url) {
|
||||
if ('application/pgp-keys' == attachment.mimeType
|
||||
&& (OpenPGPUserStore.isSupported() || GnuPGUserStore.isSupported())) {
|
||||
fetchRaw(url).then(text =>
|
||||
showScreenPopup(OpenPgpImportPopupView, [text])
|
||||
);
|
||||
} else {
|
||||
download(url, attachment.fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
keyScopeReal.subscribe(value => this.messageDomFocused(ScopeMessageView === value));
|
||||
|
||||
// initShortcuts
|
||||
|
||||
// exit fullscreen, back
|
||||
addShortcut('escape', '', ScopeMessageView, () => {
|
||||
if (!this.viewModelDom.hidden && this.message()) {
|
||||
const preview = SettingsUserStore.usePreviewPane();
|
||||
if (isFullscreen()) {
|
||||
exitFullscreen();
|
||||
if (preview) {
|
||||
AppUserStore.focusedState(ScopeMessageList);
|
||||
}
|
||||
} else if (!preview) {
|
||||
this.message(null);
|
||||
} else {
|
||||
AppUserStore.focusedState(ScopeMessageList);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// fullscreen
|
||||
addShortcut('enter,open', '', ScopeMessageView, () => {
|
||||
isFullscreen() || toggleFullscreen();
|
||||
return false;
|
||||
});
|
||||
|
||||
// reply
|
||||
registerShortcut('r,mailreply', '', [ScopeMessageList, ScopeMessageView], () => {
|
||||
if (this.message()) {
|
||||
this.replyCommand();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// replyAll
|
||||
registerShortcut('a', '', [ScopeMessageList, ScopeMessageView], () => {
|
||||
if (this.message()) {
|
||||
this.replyAllCommand();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
registerShortcut('mailreply', 'shift', [ScopeMessageList, ScopeMessageView], () => {
|
||||
if (this.message()) {
|
||||
this.replyAllCommand();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// forward
|
||||
registerShortcut('f,mailforward', '', [ScopeMessageList, ScopeMessageView], () => {
|
||||
if (this.message()) {
|
||||
this.forwardCommand();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// message information
|
||||
registerShortcut('i', 'meta', [ScopeMessageList, ScopeMessageView], () => {
|
||||
this.message() && this.toggleFullInfo();
|
||||
return false;
|
||||
});
|
||||
|
||||
// toggle message blockquotes
|
||||
registerShortcut('b', '', [ScopeMessageList, ScopeMessageView], () => {
|
||||
const message = this.message();
|
||||
if (message?.body) {
|
||||
message.body.querySelectorAll('details').forEach(node => node.open = !node.open);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
this.body = dom.querySelector('.bodyText');
|
||||
}
|
||||
|
||||
onShow(message) {
|
||||
this.message(message);
|
||||
message.body = this.body;
|
||||
message.viewBody(message.html());
|
||||
}
|
||||
|
||||
toggleAttachmentControls() {
|
||||
const b = !this.showAttachmentControls();
|
||||
this.showAttachmentControls(b);
|
||||
Local.set(ClientSideKeyNameMessageAttachmentControls, b);
|
||||
}
|
||||
|
||||
downloadAsZip() {
|
||||
const hashes = (this.message()?.attachments || [])
|
||||
.map(item => item?.checked() /*&& !item?.isLinked()*/ ? item.download : '')
|
||||
.filter(v => v);
|
||||
downloadZip(
|
||||
this.message().subject(),
|
||||
hashes,
|
||||
() => this.downloadAsZipError(true),
|
||||
this.downloadAsZipLoading
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MessageModel} oMessage
|
||||
* @returns {void}
|
||||
*/
|
||||
showImages() {
|
||||
this.message().showExternalImages();
|
||||
}
|
||||
|
||||
whitelistText(txt) {
|
||||
let value = (SettingsUserStore.viewImagesWhitelist().trim() + '\n' + txt).trim();
|
||||
/*
|
||||
if ('pass' === this.message().spf[0]?.[0]) value += '+spf';
|
||||
if ('pass' === this.message().dkim[0]?.[0]) value += '+dkim';
|
||||
if ('pass' === this.message().dmarc[0]?.[0]) value += '+dmarc';
|
||||
*/
|
||||
SettingsUserStore.viewImagesWhitelist(value);
|
||||
Remote.saveSetting('ViewImagesWhitelist', value);
|
||||
this.message().showExternalImages(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MessageModel} oMessage
|
||||
* @returns {void}
|
||||
*/
|
||||
readReceipt() {
|
||||
let oMessage = this.message()
|
||||
if (oMessage.readReceipt()) {
|
||||
Remote.request('SendReadReceiptMessage', iError => {
|
||||
if (!iError) {
|
||||
oMessage.flags.push('$mdnsent');
|
||||
// oMessage.flags.valueHasMutated();
|
||||
}
|
||||
}, {
|
||||
messageFolder: oMessage.folder,
|
||||
messageUid: oMessage.uid,
|
||||
readReceipt: oMessage.readReceipt(),
|
||||
subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
|
||||
plain: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
newTag() {
|
||||
let message = this.message();
|
||||
if (message) {
|
||||
let keyword = prompt(i18n('MESSAGE/NEW_TAG'), '')?.replace(/[\s\\]+/g, '');
|
||||
if (keyword.length && isAllowedKeyword(keyword)) {
|
||||
message.toggleTag(keyword);
|
||||
FolderUserStore.currentFolder().permanentFlags.push(keyword);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pgpDecrypt() {
|
||||
const oMessage = this.message();
|
||||
PgpUserStore.decrypt(oMessage).then(result => {
|
||||
if (result) {
|
||||
oMessage.pgpDecrypted(true);
|
||||
if (result.data) {
|
||||
MimeToMessage(result.data, oMessage);
|
||||
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
|
||||
if (result.signatures?.length) {
|
||||
oMessage.pgpSigned(true);
|
||||
oMessage.pgpVerified({
|
||||
signatures: result.signatures,
|
||||
success: !!result.signatures.length
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: translate
|
||||
alert('Decryption failed, canceled or not possible');
|
||||
}
|
||||
})
|
||||
.catch(e => console.error(e));
|
||||
}
|
||||
|
||||
pgpVerify(/*self, event*/) {
|
||||
const oMessage = this.message()/*, ctrl = event.target.closest('.openpgp-control')*/;
|
||||
PgpUserStore.verify(oMessage).then(result => {
|
||||
if (result) {
|
||||
oMessage.pgpVerified(result);
|
||||
} else {
|
||||
alert('Verification failed or no valid public key found');
|
||||
}
|
||||
/*
|
||||
if (result?.success) {
|
||||
i18n('OPENPGP/GOOD_SIGNATURE', {
|
||||
USER: validKey.user + ' (' + validKey.id + ')'
|
||||
});
|
||||
message.getText()
|
||||
} else {
|
||||
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
|
||||
additional = keyIds
|
||||
? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
|
||||
: '';
|
||||
|
||||
i18n('OPENPGP/ERROR', {
|
||||
ERROR: 'message'
|
||||
}) + (additional ? ' (' + additional + ')' : '');
|
||||
}
|
||||
*/
|
||||
});
|
||||
}
|
||||
|
||||
}
|
220
snappymail/v/0.0.0/app/templates/Views/User/PopupsMessage.html
Normal file
220
snappymail/v/0.0.0/app/templates/Views/User/PopupsMessage.html
Normal file
|
@ -0,0 +1,220 @@
|
|||
<header class="g-ui-user-select-none">
|
||||
<a href="#" class="close" data-bind="click: close">×</a>
|
||||
<h3 data-bind="text: message()?.subject"></h3>
|
||||
</header>
|
||||
<div class="modal-body b-message" data-bind="i18nUpdate: message, css: message()?.lineAsCss(0)">
|
||||
<div class="message-fixed-button-toolbar">
|
||||
<div class="btn-group" style="margin-right: -8px; display: inline;">
|
||||
<span>
|
||||
<a class="btn btn-thin btn-transparent buttonReply fontastic"
|
||||
data-bind="visible: 1 == msgDefaultAction(), command: replyCommand" data-i18n="[title]MESSAGE/BUTTON_REPLY">←</a>
|
||||
<a class="btn btn-thin btn-transparent buttonReplyAll fontastic"
|
||||
data-bind="visible: 2 == msgDefaultAction(), command: replyAllCommand" data-i18n="[title]MESSAGE/BUTTON_REPLY_ALL">↞</a>
|
||||
</span>
|
||||
<div class="btn-group" data-bind="registerBootstrapDropdown: true" style="display: inline-block">
|
||||
<a class="btn btn-thin btn-transparent dropdown-toggle fontastic" id="more-view-dropdown-id" href="#" tabindex="-1">☰</a>
|
||||
<menu class="dropdown-menu right-edge" role="menu" aria-labelledby="more-view-dropdown-id">
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="command: replyCommand" data-icon="←" data-i18n="MESSAGE/BUTTON_REPLY"></a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="command: replyAllCommand" data-icon="↞" data-i18n="MESSAGE/BUTTON_REPLY_ALL"></a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="command: forwardCommand" data-icon="→" data-i18n="MESSAGE/BUTTON_FORWARD"></a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="command: forwardAsAttachmentCommand" data-icon="⥅" data-i18n="MESSAGE/BUTTON_FORWARD_AS_ATTACHMENT"></a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="command: editAsNewCommand" data-icon="🖉" data-i18n="MESSAGE/BUTTON_EDIT_AS_NEW"></a>
|
||||
</li>
|
||||
<li class="dividerbar" role="presentation" data-bind="visible: firstUnsubsribeLink">
|
||||
<a target="_blank" href="#" tabindex="-1" data-bind="attr: { href: firstUnsubsribeLink }" data-icon="✖" data-i18n="MESSAGE/BUTTON_UNSUBSCRIBE"></a>
|
||||
</li>
|
||||
<div data-bind="with: message" class="dividerbar">
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="click: printMessage" data-icon="🖨" data-i18n="MESSAGE/MENU_PRINT"></a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="click: $root.toggleFullScreen">
|
||||
<i data-bind="css: { 'icon-arrows-out': !$root.fullScreenMode(), 'icon-arrows-in': $root.fullScreenMode }"></i>
|
||||
<span data-i18n="SHORTCUTS_HELP/LABEL_FULLSCREEN_TOGGLE"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="click: popupMessage" data-icon="⧉" data-i18n="MESSAGE/BUTTON_IN_NEW_WINDOW"></a>
|
||||
</li>
|
||||
<li role="presentation" data-bind="visible: html() && !isHtml()">
|
||||
<a href="#" tabindex="-1" data-bind="click: viewHtml" data-icon="👁" data-i18n="MESSAGE/HTML_VIEW"></a>
|
||||
</li>
|
||||
<li role="presentation" data-bind="visible: isHtml()">
|
||||
<a href="#" tabindex="-1" data-bind="click: viewPlain" data-icon="👁" data-i18n="MESSAGE/PLAIN_VIEW"></a>
|
||||
</li>
|
||||
</div>
|
||||
</menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="messageItemHeader" data-bind="if: message, i18nUpdate: message">
|
||||
<div class="subjectParent">
|
||||
<span class="infoParent g-ui-user-select-none fontastic" data-bind="click: toggleFullInfo">ℹ</span>
|
||||
<span class="flagParent g-ui-user-select-none flagOff fontastic" data-bind="text: message().isFlagged() ? '★' : '☆', css: {flagOn: message().isFlagged(), flagOff: !message().isFlagged()}"></span>
|
||||
<span class="subject" data-bind="text: message().subject"></span>
|
||||
</div>
|
||||
<div class="informationShort">
|
||||
<span class="from" data-bind="html: viewFromShort, title: message().from"></span>
|
||||
<i data-bind="visible: viewDkimIcon, css: dkimIconClass, title: dkimTitle"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="messageItem">
|
||||
<div class="messageItemHeader" data-bind="if: message, i18nUpdate: message">
|
||||
<div data-bind="hidden: showFullInfo">
|
||||
<time class="date" data-moment-format="FULL" data-bind="visible: 0 < message().dateTimestamp(), moment: message().dateTimestamp()"></time>
|
||||
<div class="informationShortWrp">
|
||||
<div class="informationShort" data-bind="visible: message().to.length">
|
||||
<span data-i18n="GLOBAL/TO"></span>:
|
||||
<span data-bind="text: message().to"></span>
|
||||
</div>
|
||||
<div class="informationShort" data-bind="visible: message().cc.length">
|
||||
<span data-i18n="GLOBAL/CC"></span>:
|
||||
<span data-bind="text: message().cc"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="informationShort" data-bind="visible: message().spamResult()">
|
||||
<span data-i18n="MESSAGE/SPAM_SCORE"></span>:
|
||||
<meter min="0" max="100" optimum="0" low="33" high="66" data-bind="value: message().spamScore(), title: message().spamStatus()"></meter>
|
||||
</div>
|
||||
</div>
|
||||
<div class="informationFull" data-bind="visible: showFullInfo, with: message">
|
||||
<table>
|
||||
<tr data-bind="visible: from.length">
|
||||
<td data-i18n="GLOBAL/FROM"></td>
|
||||
<td><span data-bind="text: from"></span>
|
||||
<i data-bind="visible: $parent.viewDkimIcon, css: $parent.dkimIconClass, title: $parent.dkimTitle"></i>
|
||||
</td>
|
||||
</tr>
|
||||
<tr data-bind="visible: to.length">
|
||||
<td data-i18n="GLOBAL/TO"></td>
|
||||
<td data-bind="text: to"></td>
|
||||
</tr>
|
||||
<tr data-bind="visible: cc.length">
|
||||
<td data-i18n="GLOBAL/CC"></td>
|
||||
<td data-bind="text: cc"></td>
|
||||
</tr>
|
||||
<tr data-bind="visible: bcc.length">
|
||||
<td data-i18n="GLOBAL/BCC"></td>
|
||||
<td data-bind="text: bcc"></td>
|
||||
</tr>
|
||||
<tr data-bind="visible: replyTo.length">
|
||||
<td data-i18n="GLOBAL/REPLY_TO"></td>
|
||||
<td data-bind="text: replyTo"></td>
|
||||
</tr>
|
||||
<tr data-bind="visible: dateTimestamp">
|
||||
<td data-i18n="MESSAGE/LABEL_DATE"></td>
|
||||
<td>
|
||||
<time data-moment-format="FULL" data-bind="moment: dateTimestamp"></time>
|
||||
|
||||
(<time data-moment-format="FROMNOW" data-bind="moment: dateTimestamp"></time>)
|
||||
</td>
|
||||
</tr>
|
||||
<tr data-bind="visible: spamResult">
|
||||
<td data-i18n="GLOBAL/SPAM"></td>
|
||||
<td data-bind="text: spamStatus()"></td>
|
||||
</tr>
|
||||
<tr data-bind="visible: friendlySize()">
|
||||
<td data-i18n="POPUPS_FILTER/SELECT_FIELD_SIZE"></td>
|
||||
<td class="size" data-bind="text: friendlySize()"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="hasVirus" data-bind="visible: message().hasVirus()" data-i18n="MESSAGE/HAS_VIRUS_WARNING"></div>
|
||||
<!-- ko if: tagsAllowed -->
|
||||
<div class="messageTags">
|
||||
<span data-i18n="MESSAGE/TAGS"></span>:
|
||||
<span class="messageAssignedTags" data-bind="html: tagsToHTML"></span>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<div tabindex="0" data-bind="hasfocus: messageDomFocused">
|
||||
<div class="bodySubHeader" data-bind="if: message, i18nUpdate: message">
|
||||
<details class="attachmentsPlace" data-bind="visible: hasAttachments, css: {'selection-mode' : showAttachmentControls}, attr: { open: hasAttachments }">
|
||||
<summary data-i18n="MESSAGE/PRINT_LABEL_ATTACHMENTS"></summary>
|
||||
<!-- ko ifnot: simpleAttachmentsList -->
|
||||
<ul class="attachmentList" data-bind="foreach: listAttachments">
|
||||
<li class="attachmentItem" draggable="true"
|
||||
data-bind="event: { dragstart: eventDragStart }, attr: { title: fileName }, css: {checked: checked}">
|
||||
<div class="attachmentIcon" data-bind="css: { hasPreview: hasPreview(), hasPreplay: hasPreplay(), isImage: isImage() }, attr: { style: thumbnailStyle() }">
|
||||
<i class="hidePreview iconMain" data-bind="css: iconClass()"></i>
|
||||
<div class="showPreview">
|
||||
<a data-bind="css: {attachmentImagePreview: isImage()}, attr: { title: fileName, href: linkPreviewMain() }" target="_blank">
|
||||
<i class="iconMain" data-bind="visible: !hasThumbnail(), css: iconClass()"></i>
|
||||
<div class="iconPreview fontastic">👁</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachmentNameParent">
|
||||
<div class="attachmentName" data-bind="text: fileName"></div>
|
||||
<div class="attachmentSize" data-bind="text: friendlySize()"></div>
|
||||
</div>
|
||||
<div class="checkboxAttachment fontastic"
|
||||
data-bind="visible: download, text: checked() ? '☑' : '☐', click: toggleChecked"></div>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: simpleAttachmentsList -->
|
||||
<ul class="attachmentListSimple" data-bind="foreach: listAttachments">
|
||||
<li class="attachmentItem" data-bind="attr: { title: fileName }, css: {checked: checked}">
|
||||
<span class="attachmentName" data-bind="text: fileName"></span>
|
||||
<i class="checkboxAttachment fontastic"
|
||||
data-bind="visible: download, text: checked() ? '☑' : '☐', click: toggleChecked"></i>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- /ko -->
|
||||
<i class="fontastic controls-handle" data-bind="visible: allowAttachmentControls, click: toggleAttachmentControls">⚙</i>
|
||||
<div class="attachmentsControls" data-bind="visible: showAttachmentControls">
|
||||
<span data-bind="visible: downloadAsZipAllowed">
|
||||
<i class="fontastic iconcolor-red" data-bind="visible: downloadAsZipError">✖</i>
|
||||
<i class="icon-file-archive" data-bind="visible: !downloadAsZipError(),
|
||||
css: {'icon-file-archive': !downloadAsZipLoading(), 'icon-spinner': downloadAsZipLoading()}"></i>
|
||||
<span class="g-ui-link" data-bind="click: downloadAsZip"
|
||||
data-i18n="MESSAGE/LINK_DOWNLOAD_AS_ZIP"></span>
|
||||
</span>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="btn-toolbar showImages" data-bind="visible: message().hasImages()">
|
||||
<div class="btn" data-bind="click: showImages" data-icon="🖼" data-i18n="MESSAGE/BUTTON_SHOW_IMAGES"></div>
|
||||
<div class="btn-group dropdown" data-bind="registerBootstrapDropdown: true, visible: showWhitelistOptions" style="display: inline-block">
|
||||
<a class="btn dropdown-toggle" id="whitelist-dropdown-id" data-icon="🖼" href="#" tabindex="-1"><span data-i18n="SETTINGS_GENERAL/IMAGES_WHITELIST"></span> ▼</a>
|
||||
<menu class="dropdown-menu" role="menu" aria-labelledby="whitelist-dropdown-id">
|
||||
<!-- ko foreach: message().whitelistOptions() -->
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="click: $root.whitelistText, text: $data"></a>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
</menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: message().pgpEncrypted()">
|
||||
<div class="openpgp-control encrypted" data-bind="css: {success: message().pgpDecrypted()}">
|
||||
<span data-icon="🔒" data-i18n="OPENPGP/ENCRYPTED_MESSAGE"></span>
|
||||
<button class="btn" data-bind="visible: pgpSupported, click: pgpDecrypt" data-i18n="OPENPGP/BUTTON_DECRYPT"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: message().pgpSigned()">
|
||||
<div class="openpgp-control signed" data-bind="css: {success: message().pgpVerified() && message().pgpVerified().success, error: message().pgpVerified() && !message().pgpVerified().success}">
|
||||
<span data-icon="✍" data-i18n="OPENPGP/SIGNED_MESSAGE"></span>
|
||||
<button class="btn" data-bind="visible: pgpSupported, click: pgpVerify" data-i18n="OPENPGP/BUTTON_VERIFY"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bodyText"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
</footer>
|
Loading…
Reference in a new issue