Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/mail.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSilvio Zimmer <silvio.zimmer@die-kinderwelt.com>2021-04-21 13:45:47 +0300
committerSilvio Zimmer <silvio.zimmer@die-kinderwelt.com>2021-04-21 13:45:47 +0300
commit3e8edcf91acd7ccbff6e4fa8fd81d1f67a3955a3 (patch)
tree3a62db28114044c5869c45aeaae6cd71414b2eab
parent513b5e38b539e0f16f4cdbe27d91be2d52c60290 (diff)
add option to place signature above quoted text
Signed-off-by: Silvio Zimmer <silvio.zimmer@die-kinderwelt.com>
-rw-r--r--lib/Controller/AccountsController.php10
-rw-r--r--lib/Db/MailAccount.php9
-rw-r--r--lib/Migration/Version1100Date20210421113423.php57
-rw-r--r--src/ReplyBuilder.js18
-rw-r--r--src/ckeditor/quote/QuotePlugin.js53
-rw-r--r--src/ckeditor/signature/InsertSignatureCommand.js56
-rw-r--r--src/components/Composer.vue6
-rw-r--r--src/components/SignatureSettings.vue32
-rw-r--r--src/components/TextEditor.vue10
-rw-r--r--src/store/index.js1
-rw-r--r--src/tests/unit/ReplyBuilder.spec.js10
-rw-r--r--src/util/text.js1
-rw-r--r--tests/Integration/Db/MailAccountTest.php2
13 files changed, 233 insertions, 32 deletions
diff --git a/lib/Controller/AccountsController.php b/lib/Controller/AccountsController.php
index 607f543ab..ed90346a1 100644
--- a/lib/Controller/AccountsController.php
+++ b/lib/Controller/AccountsController.php
@@ -234,6 +234,10 @@ class AccountsController extends Controller {
* @param string|null $editorMode
* @param int|null $order
* @param bool|null $showSubscribedOnly
+ * @param int|null $draftsMailboxId
+ * @param int|null $sentMailboxId
+ * @param int|null $trashMailboxId
+ * @param bool|null $signatureAboveQuote
*
* @return JSONResponse
*
@@ -245,7 +249,8 @@ class AccountsController extends Controller {
bool $showSubscribedOnly = null,
int $draftsMailboxId = null,
int $sentMailboxId = null,
- int $trashMailboxId = null): JSONResponse {
+ int $trashMailboxId = null,
+ bool $signatureAboveQuote = null): JSONResponse {
$account = $this->accountService->find($this->currentUserId, $id);
$dbAccount = $account->getMailAccount();
@@ -267,6 +272,9 @@ class AccountsController extends Controller {
if ($trashMailboxId !== null) {
$dbAccount->setTrashMailboxId($trashMailboxId);
}
+ if ($signatureAboveQuote !== null) {
+ $dbAccount->setSignatureAboveQuote($signatureAboveQuote);
+ }
return new JSONResponse(
$this->accountService->save($dbAccount)->toJson()
);
diff --git a/lib/Db/MailAccount.php b/lib/Db/MailAccount.php
index 1fa65bb2d..a41b0528c 100644
--- a/lib/Db/MailAccount.php
+++ b/lib/Db/MailAccount.php
@@ -89,6 +89,8 @@ use OCP\AppFramework\Db\Entity;
* @method void setSieveUser(?string $sieveUser)
* @method string|null getSievePassword()
* @method void setSievePassword(?string $sievePassword)
+ * @method bool|null isSignatureAboveQuote()
+ * @method void setSignatureAboveQuote(bool $signatureAboveQuote)
*/
class MailAccount extends Entity {
protected $userId;
@@ -133,6 +135,8 @@ class MailAccount extends Entity {
protected $sieveUser;
/** @var string|null */
protected $sievePassword;
+ /** @var bool */
+ protected $signatureAboveQuote = false;
/**
* @param array $params
@@ -182,6 +186,9 @@ class MailAccount extends Entity {
if (isset($params['showSubscribedOnly'])) {
$this->setShowSubscribedOnly($params['showSubscribedOnly']);
}
+ if (isset($params['signatureAboveQuote'])) {
+ $this->setSignatureAboveQuote($params['signatureAboveQuote']);
+ }
$this->addType('inboundPort', 'integer');
$this->addType('outboundPort', 'integer');
@@ -195,6 +202,7 @@ class MailAccount extends Entity {
$this->addType('trashMailboxId', 'integer');
$this->addType('sieveEnabled', 'boolean');
$this->addType('sievePort', 'integer');
+ $this->addType('signatureAboveQuote', 'boolean');
}
/**
@@ -220,6 +228,7 @@ class MailAccount extends Entity {
'sentMailboxId' => $this->getSentMailboxId(),
'trashMailboxId' => $this->getTrashMailboxId(),
'sieveEnabled' => ($this->isSieveEnabled() === true),
+ 'signatureAboveQuote' => ($this->isSignatureAboveQuote() === true),
];
if (!is_null($this->getOutboundHost())) {
diff --git a/lib/Migration/Version1100Date20210421113423.php b/lib/Migration/Version1100Date20210421113423.php
new file mode 100644
index 000000000..54ee74feb
--- /dev/null
+++ b/lib/Migration/Version1100Date20210421113423.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2021 Silvio Zimmer <silvio.zimmer@die-kinderwelt.com>
+ *
+ * @author 2021 Silvio Zimmer <silvio.zimmer@die-kinderwelt.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Mail\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version1100Date20210421113423 extends SimpleMigrationStep
+{
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ *
+ * @return ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options)
+ {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ $accountsTable = $schema->getTable('mail_accounts');
+
+ $accountsTable->addColumn('signature_above_quote', 'boolean', [
+ 'notnull' => false,
+ 'default' => false,
+ ]);
+
+ return $schema;
+ }
+}
diff --git a/src/ReplyBuilder.js b/src/ReplyBuilder.js
index 26199b897..3efba5522 100644
--- a/src/ReplyBuilder.js
+++ b/src/ReplyBuilder.js
@@ -36,29 +36,31 @@ export const buildReplyBody = (original, from, date, replyOnTop = true) => {
const startEnd = '<p></p><p></p>'
const plainBody = '<br>&gt; ' + original.value.replace(/\n/g, '<br>&gt; ')
const htmlBody = `<blockquote>${original.value}</blockquote>`
+ const quoteStart = '<div class="quote">'
+ const quoteEnd = '</div>'
switch (original.format) {
case 'plain':
if (from) {
const dateString = moment.unix(date).format('LLL')
return replyOnTop
- ? html(`${startEnd}"${from.label}" ${from.email} – ${dateString}` + plainBody)
- : html(`"${from.label}" ${from.email} – ${dateString}` + plainBody + startEnd)
+ ? html(`${startEnd}${quoteStart}"${from.label}" ${from.email} – ${dateString}` + plainBody + quoteEnd)
+ : html(`${quoteStart}"${from.label}" ${from.email} – ${dateString}` + plainBody + quoteEnd + startEnd)
} else {
return replyOnTop
- ? html(`${startEnd}${plainBody}`)
- : html(`${plainBody}${startEnd}`)
+ ? html(`${startEnd}${quoteStart}${plainBody}${quoteEnd}`)
+ : html(`${quoteStart}${plainBody}${quoteEnd}${startEnd}`)
}
case 'html':
if (from) {
const dateString = moment.unix(date).format('LLL')
return replyOnTop
- ? html(`${startEnd}"${from.label}" ${from.email} – ${dateString}<br>${htmlBody}`)
- : html(`"${from.label}" ${from.email} – ${dateString}<br>${htmlBody}${startEnd}`)
+ ? html(`${startEnd}${quoteStart}"${from.label}" ${from.email} – ${dateString}<br>${htmlBody}${quoteEnd}`)
+ : html(`${quoteStart}"${from.label}" ${from.email} – ${dateString}<br>${htmlBody}${quoteEnd}${startEnd}`)
} else {
return replyOnTop
- ? html(`${startEnd}${htmlBody}`)
- : html(`${htmlBody}${startEnd}`)
+ ? html(`${startEnd}${quoteStart}${htmlBody}${quoteEnd}`)
+ : html(`${quoteStart}${htmlBody}${quoteEnd}${startEnd}`)
}
}
diff --git a/src/ckeditor/quote/QuotePlugin.js b/src/ckeditor/quote/QuotePlugin.js
new file mode 100644
index 000000000..a937037e5
--- /dev/null
+++ b/src/ckeditor/quote/QuotePlugin.js
@@ -0,0 +1,53 @@
+/**
+ * @copyright 2021 Silvio Zimmer <silvio.zimmer@die-kinderwelt.com>
+ *
+ * @author 2021 Silvio Zimmer <silvio.zimmer@die-kinderwelt.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin'
+
+export default class Quote extends Plugin {
+
+ init() {
+ this._defineSchema()
+ this._defineConverters()
+ }
+
+ _defineSchema() {
+ const schema = this.editor.model.schema
+
+ schema.register('quote', {
+ allowIn: '$root',
+ isLimit: true,
+ allowContentOf: '$block',
+ })
+ }
+
+ _defineConverters() {
+ const conversion = this.editor.conversion
+
+ conversion.elementToElement({
+ model: 'quote',
+ view: {
+ name: 'div',
+ classes: 'quote',
+ },
+ })
+ }
+
+}
diff --git a/src/ckeditor/signature/InsertSignatureCommand.js b/src/ckeditor/signature/InsertSignatureCommand.js
index f9c8855e8..d3a82d7a3 100644
--- a/src/ckeditor/signature/InsertSignatureCommand.js
+++ b/src/ckeditor/signature/InsertSignatureCommand.js
@@ -35,14 +35,21 @@ export default class InsertSignatureCommand extends Command {
}
}
- insertSignatureElement(editor, writer, value) {
+ /**
+ *
+ * @param {*} editor the editor instance
+ * @param {*} writer the writer instance
+ * @param {string} signature the signature text
+ * @param {boolean} signatureAboveQuote the signature position: above/below the quoted text
+ */
+ insertSignatureElement(editor, writer, signature, signatureAboveQuote) {
// Skip empty signature
- if (value.length === 0) {
+ if (signature.length === 0) {
return
}
// Convert an HTML string to a view document fragment:
- const viewFragment = editor.data.processor.toView(value)
+ const viewFragment = editor.data.processor.toView(signature)
// Convert the view document fragment to a model document fragment
// in the context of $root. This conversion takes the schema into
@@ -54,24 +61,47 @@ export default class InsertSignatureCommand extends Command {
// which has a loosened schema.
const modelFragment = editor.data.toModel(viewFragment)
- const signature = writer.createElement('signature')
- writer.append(writer.createText('--'), signature)
- writer.append(writer.createElement('paragraph'), signature)
- writer.append(modelFragment, signature)
+ const signatureElement = writer.createElement('signature')
+ writer.append(writer.createText('-- '), signatureElement)
+ writer.append(writer.createElement('paragraph'), signatureElement)
+ writer.append(modelFragment, signatureElement)
+ if (signatureAboveQuote) {
+ writer.append(writer.createElement('paragraph'), signatureElement)
+ }
+
+ const signaturePosition = signatureAboveQuote ? this.findPositionAboveQuote(editor, writer) : writer.createPositionAt(editor.model.document.getRoot(), 'end')
+ editor.model.insertContent(signatureElement, signaturePosition)
+ }
- editor.model.insertContent(
- signature,
- writer.createPositionAt(editor.model.document.getRoot(), 'end')
+ /**
+ *
+ * @param {*} editor the editor instance
+ * @param {*} writer the writer instance
+ * @returns {*} the position above the quoted text; position 1 if no quote found
+ */
+ findPositionAboveQuote(editor, writer) {
+ // Create a range spanning over the entire root content:
+ const range = editor.model.createRangeIn(
+ editor.model.document.getRoot()
)
+
+ // Iterate over all items in this range:
+ for (const value of range.getWalker({ shallow: true })) {
+ if (value.item.is('element') && value.item.name === 'quote') {
+ return writer.createPositionBefore(value.item)
+ }
+ }
+
+ return writer.createPositionAt(editor.model.document.getRoot(), 'end')
}
/**
- * @param {string} value signature to append
+ * @param {*} param0 the signature text and position
*/
- execute({ value }) {
+ execute({ signature, signatureAboveQuote }) {
this.editor.model.change(writer => {
this.removeSignatureElement(this.editor, writer)
- this.insertSignatureElement(this.editor, writer, value)
+ this.insertSignatureElement(this.editor, writer, signature, signatureAboveQuote)
})
}
diff --git a/src/components/Composer.vue b/src/components/Composer.vue
index f3293df5b..dcf1b41cd 100644
--- a/src/components/Composer.vue
+++ b/src/components/Composer.vue
@@ -404,6 +404,7 @@ export default {
signature: account.signature,
name: account.name,
emailAddress: account.emailAddress,
+ signatureAboveQuote: account.signatureAboveQuote,
},
account.aliases.map((alias) => {
return {
@@ -414,6 +415,7 @@ export default {
signature: alias.signature,
name: alias.name,
emailAddress: alias.alias,
+ signatureAboveQuote: account.signatureAboveQuote,
}
}),
])
@@ -668,8 +670,8 @@ export default {
onInputChanged() {
this.saveDraftDebounced(this.getMessageData)
if (this.appendSignature) {
- const signature = this.selectedAlias?.signature || ''
- this.bus.$emit('insertSignature', toHtml(detect(signature)).value)
+ const signatureValue = toHtml(detect(this.selectedAlias.signature)).value
+ this.bus.$emit('insertSignature', signatureValue, this.selectedAlias.signatureAboveQuote)
this.appendSignature = false
}
},
diff --git a/src/components/SignatureSettings.vue b/src/components/SignatureSettings.vue
index 48d2c6bad..8a90f903d 100644
--- a/src/components/SignatureSettings.vue
+++ b/src/components/SignatureSettings.vue
@@ -21,6 +21,16 @@
<template>
<div class="section">
+ <div>
+ <input
+ id="signature-above-quote-toggle"
+ v-model="signatureAboveQuote"
+ type="checkbox"
+ class="checkbox">
+ <label for="signature-above-quote-toggle">
+ {{ t("mail", "Place signature above quoted text") }}
+ </label>
+ </div>
<Multiselect
v-if="identities.length > 1"
:allow-empty="false"
@@ -72,6 +82,7 @@ export default {
bus: new Vue(),
identity: null,
signature: '',
+ signatureAboveQuote: this.account.signatureAboveQuote,
}
},
computed: {
@@ -93,6 +104,27 @@ export default {
return identities
},
},
+ watch: {
+ async signatureAboveQuote(val, oldVal) {
+ await this.$store
+ .dispatch('patchAccount', {
+ account: this.account,
+ data: {
+ signatureAboveQuote: val,
+ },
+ })
+ .then(() => {
+ logger.debug('signature above quoted updated to ' + val)
+ })
+ .catch((error) => {
+ logger.error('could not update signature above quote', {
+ error,
+ })
+ this.signatureAboveQuote = oldVal
+ throw error
+ })
+ },
+ },
beforeMount() {
this.changeIdentity(this.identities[0])
},
diff --git a/src/components/TextEditor.vue b/src/components/TextEditor.vue
index 72e458d7f..a8bd8a8f2 100644
--- a/src/components/TextEditor.vue
+++ b/src/components/TextEditor.vue
@@ -42,6 +42,7 @@ import LinkPlugin from '@ckeditor/ckeditor5-link/src/link'
import ListStyle from '@ckeditor/ckeditor5-list/src/liststyle'
import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph'
import SignaturePlugin from '../ckeditor/signature/SignaturePlugin'
+import QuotePlugin from '../ckeditor/quote/QuotePlugin'
import { getLanguage } from '@nextcloud/l10n'
import DOMPurify from 'dompurify'
@@ -76,7 +77,7 @@ export default {
},
},
data() {
- const plugins = [EssentialsPlugin, ParagraphPlugin, SignaturePlugin]
+ const plugins = [EssentialsPlugin, ParagraphPlugin, SignaturePlugin, QuotePlugin]
const toolbar = ['undo', 'redo']
if (this.html) {
@@ -202,8 +203,11 @@ export default {
const modelFragment = this.editorInstance.data.toModel(viewFragment)
this.editorInstance.model.insertContent(modelFragment)
},
- onInsertSignature(signature) {
- this.editorInstance.execute('insertSignature', { value: signature })
+ onInsertSignature(signatureParam, signatureAboveQuoteParam) {
+ this.editorInstance.execute('insertSignature', {
+ signature: signatureParam,
+ signatureAboveQuote: signatureAboveQuoteParam,
+ })
},
},
}
diff --git a/src/store/index.js b/src/store/index.js
index 344a40abe..6f8e57343 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -47,6 +47,7 @@ export default new Vuex.Store({
emailAddress: '',
name: '',
showSubscribedOnly: false,
+ signatureAboveQuote: false,
},
},
accountList: [UNIFIED_ACCOUNT_ID],
diff --git a/src/tests/unit/ReplyBuilder.spec.js b/src/tests/unit/ReplyBuilder.spec.js
index 3e9930143..4344b8c99 100644
--- a/src/tests/unit/ReplyBuilder.spec.js
+++ b/src/tests/unit/ReplyBuilder.spec.js
@@ -23,9 +23,9 @@
import {
buildRecipients,
buildReplyBody,
- buildReplySubject
+ buildReplySubject,
} from '../../ReplyBuilder'
-import {html, plain} from '../../util/text'
+import { html, plain } from '../../util/text'
describe('ReplyBuilder', () => {
it('creates a reply body without any sender', () => {
@@ -34,8 +34,8 @@ describe('ReplyBuilder', () => {
const replyBodyTop = buildReplyBody(body)
const replyBodyBottom = buildReplyBody(body, undefined, undefined, false)
- expect(replyBodyTop).to.deep.equal(html('<p></p><p></p><br>&gt; Newsletter<br>&gt; hello<br>&gt; cheers'))
- expect(replyBodyBottom).to.deep.equal(html('<br>&gt; Newsletter<br>&gt; hello<br>&gt; cheers<p></p><p></p>'))
+ expect(replyBodyTop).to.deep.equal(html('<p></p><p></p><div class="quote"><br>&gt; Newsletter<br>&gt; hello<br>&gt; cheers</div>'))
+ expect(replyBodyBottom).to.deep.equal(html('<div class="quote"><br>&gt; Newsletter<br>&gt; hello<br>&gt; cheers</div><p></p><p></p>'))
})
it('creates a reply body', () => {
@@ -59,7 +59,7 @@ describe('ReplyBuilder', () => {
false
)
- expect(replyBodyTop.value.startsWith(html('<p></p><p></p>"Test User" test@user.ru – November 5, 2018 ').value)).to.be.true
+ expect(replyBodyTop.value.startsWith(html('<p></p><p></p><div class="quote">"Test User" test@user.ru – November 5, 2018 ').value)).to.be.true
expect(replyBodyBottom.value.endsWith(html('<p></p><p></p>').value)).to.be.true
})
diff --git a/src/util/text.js b/src/util/text.js
index 1ce1d6733..4b9804e62 100644
--- a/src/util/text.js
+++ b/src/util/text.js
@@ -132,6 +132,7 @@ export const toPlain = (text) => {
.replace(/\n\n\n/g, '\n\n') // remove triple line breaks
.replace(/^[\n\r]+/g, '') // trim line breaks at beginning and end
.replace(/ $/gm, '') // trim white space at end of each line
+ .replace(/^--$/gm, '-- ') // hack to create the correct email signature separator
)
}
diff --git a/tests/Integration/Db/MailAccountTest.php b/tests/Integration/Db/MailAccountTest.php
index fd2ad11e3..8db2729f8 100644
--- a/tests/Integration/Db/MailAccountTest.php
+++ b/tests/Integration/Db/MailAccountTest.php
@@ -70,6 +70,7 @@ class MailAccountTest extends TestCase {
'sentMailboxId' => null,
'trashMailboxId' => null,
'sieveEnabled' => false,
+ 'signatureAboveQuote' => false,
], $a->toJson());
}
@@ -97,6 +98,7 @@ class MailAccountTest extends TestCase {
'sentMailboxId' => null,
'trashMailboxId' => null,
'sieveEnabled' => false,
+ 'signatureAboveQuote' => false,
];
$a = new MailAccount($expected);
// TODO: fix inconsistency