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:
authorChristoph Wurst <ChristophWurst@users.noreply.github.com>2021-05-28 23:50:44 +0300
committerGitHub <noreply@github.com>2021-05-28 23:50:44 +0300
commitea38d17e40d97686695accca2ee0a0fdca76ccba (patch)
treec45677185f146463f644ac77a5a20434a822655f
parent0d8c31751b5fed084511bdd04ee25ee5b9f0fae4 (diff)
parent6c834c1540d8c02d384121b3c1e653ba180ea590 (diff)
Merge pull request #5083 from nextcloud/enhanc/new-personalized-tagsv1.10.0-alpha.4
Add new personalized tags
-rw-r--r--appinfo/routes.php10
-rw-r--r--lib/Controller/TagsController.php140
-rw-r--r--lib/Db/TagMapper.php6
-rw-r--r--lib/Model/IMAPMessage.php52
-rw-r--r--src/components/Envelope.vue1
-rw-r--r--src/components/TagModal.vue98
-rw-r--r--src/service/MessageService.js6
-rw-r--r--src/store/actions.js6
8 files changed, 288 insertions, 31 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 7a1d14649..bf642752e 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -100,6 +100,16 @@ return [
'verb' => 'GET'
],
[
+ 'name' => 'tags#create',
+ 'url' => '/api/tags',
+ 'verb' => 'POST'
+ ],
+ [
+ 'name' => 'tags#update',
+ 'url' => '/api/tags/{id}',
+ 'verb' => 'PUT'
+ ],
+ [
'name' => 'aliases#updateSignature',
'url' => '/api/accounts/{accountId}/aliases/{id}/signature',
'verb' => 'PUT'
diff --git a/lib/Controller/TagsController.php b/lib/Controller/TagsController.php
new file mode 100644
index 000000000..feccf38ed
--- /dev/null
+++ b/lib/Controller/TagsController.php
@@ -0,0 +1,140 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @author Daniel Kesselberg <mail@danielkesselberg.de>
+ *
+ * Mail
+ *
+ * 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/>
+ *
+ */
+
+namespace OCA\Mail\Controller;
+
+use OCA\Mail\AppInfo\Application;
+use OCA\Mail\Db\Tag;
+use OCA\Mail\Db\TagMapper;
+use OCA\Mail\Exception\ClientException;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+
+class TagsController extends Controller {
+
+ /** @var string */
+ private $currentUserId;
+
+ /** @var TagMapper */
+ private $tagMapper;
+
+ /**
+ * TagsController constructor.
+ *
+ * @param IRequest $request
+ * @param $UserId
+ * @param TagMapper $tagMapper
+ */
+ public function __construct(IRequest $request,
+ $UserId,
+ TagMapper $tagMapper
+ ) {
+ parent::__construct(Application::APP_ID, $request);
+ $this->currentUserId = $UserId;
+ $this->tagMapper = $tagMapper;
+ }
+
+ /**
+ * @NoAdminRequired
+ * @TrapError
+ *
+ * @param string $displayName
+ * @param string $color
+ *
+ * @return JSONResponse
+ * @throws ClientException
+ */
+ public function create(string $displayName, string $color): JSONResponse {
+ $this->validateDisplayName($displayName);
+ $this->validateColor($color);
+
+ $imapLabel = str_replace(' ', '_', $displayName);
+ /** @var string|false $imapLabel */
+ $imapLabel = mb_convert_encoding($imapLabel, 'UTF7-IMAP', 'UTF-8');
+ if ($imapLabel === false) {
+ throw new ClientException('Error converting display name to UTF7-IMAP ', 0);
+ }
+ $imapLabel = mb_strcut($imapLabel, 0, 64);
+
+ try {
+ return new JSONResponse($this->tagMapper->getTagByImapLabel($imapLabel, $this->currentUserId));
+ } catch (DoesNotExistException $e) {
+ // it's valid that a tag does not exist.
+ }
+
+ $tag = new Tag();
+ $tag->setUserId($this->currentUserId);
+ $tag->setDisplayName($displayName);
+ $tag->setImapLabel($imapLabel);
+ $tag->setColor($color);
+ $tag->setIsDefaultTag(false);
+
+ $tag = $this->tagMapper->insert($tag);
+ return new JSONResponse($tag);
+ }
+
+ /**
+ * @NoAdminRequired
+ * @TrapError
+ *
+ * @param int $id
+ * @param string $displayName
+ * @param string $color
+ *
+ * @return JSONResponse
+ * @throws ClientException
+ * @throws DoesNotExistException
+ */
+ public function update(int $id, string $displayName, string $color): JSONResponse {
+ $this->validateDisplayName($displayName);
+ $this->validateColor($color);
+
+ $tag = $this->tagMapper->getTagForUser($id, $this->currentUserId);
+
+ $tag->setDisplayName($displayName);
+ $tag->setColor($color);
+
+ $tag = $this->tagMapper->update($tag);
+ return new JSONResponse($tag);
+ }
+
+ /**
+ * @throws ClientException
+ */
+ private function validateDisplayName(string $displayName): void {
+ if (mb_strlen($displayName) > 128) {
+ throw new ClientException('The maximum length for displayName is 128');
+ }
+ }
+
+ /**
+ * @throws ClientException
+ */
+ private function validateColor(string $color): void {
+ if (mb_strlen($color) > 9) {
+ throw new ClientException('The maximum length for color is 9');
+ }
+ }
+}
diff --git a/lib/Db/TagMapper.php b/lib/Db/TagMapper.php
index 3c626b9e3..43794c112 100644
--- a/lib/Db/TagMapper.php
+++ b/lib/Db/TagMapper.php
@@ -28,7 +28,6 @@ namespace OCA\Mail\Db;
use function array_map;
use function array_chunk;
use OCP\AppFramework\Db\DoesNotExistException;
-use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
@@ -51,7 +50,7 @@ class TagMapper extends QBMapper {
/**
* @throws DoesNotExistException
*/
- public function getTagByImapLabel(string $imapLabel, string $userId): Entity {
+ public function getTagByImapLabel(string $imapLabel, string $userId): Tag {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
@@ -65,7 +64,7 @@ class TagMapper extends QBMapper {
/**
* @throws DoesNotExistException
*/
- public function getTagForUser(int $id, string $userId): Entity {
+ public function getTagForUser(int $id, string $userId): Tag {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
@@ -78,7 +77,6 @@ class TagMapper extends QBMapper {
/**
* @return Tag[]
- * @throws DoesNotExistException
*/
public function getAllTagsForUser(string $userId): array {
$qb = $this->db->getQueryBuilder();
diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php
index e1393d8ac..c163ebd0c 100644
--- a/lib/Model/IMAPMessage.php
+++ b/lib/Model/IMAPMessage.php
@@ -29,33 +29,33 @@ declare(strict_types=1);
namespace OCA\Mail\Model;
-use OC;
use Exception;
-use function trim;
-use OCP\Files\File;
-use Horde_Mime_Part;
-use OCA\Mail\Db\Tag;
-use JsonSerializable;
-use function in_array;
use Horde_Imap_Client;
-use Horde_Mime_Headers;
-use OCA\Mail\Db\Message;
-use OCA\Mail\AddressList;
+use Horde_Imap_Client_Data_Envelope;
+use Horde_Imap_Client_Data_Fetch;
+use Horde_Imap_Client_DateTime;
+use Horde_Imap_Client_Fetch_Query;
use Horde_Imap_Client_Ids;
-use OCA\Mail\Service\Html;
-use OCA\Mail\Db\MailAccount;
-use Horde_Imap_Client_Socket;
use Horde_Imap_Client_Mailbox;
-use Horde_Imap_Client_DateTime;
+use Horde_Imap_Client_Socket;
+use Horde_Mime_Headers;
+use Horde_Mime_Headers_MessageId;
+use Horde_Mime_Part;
+use JsonSerializable;
+use OC;
+use OCA\Mail\AddressList;
use OCA\Mail\Db\LocalAttachment;
+use OCA\Mail\Db\MailAccount;
+use OCA\Mail\Db\Message;
+use OCA\Mail\Db\Tag;
+use OCA\Mail\Service\Html;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\Files\File;
+use OCP\Files\SimpleFS\ISimpleFile;
+use function in_array;
use function mb_convert_encoding;
use function mb_strcut;
-use Horde_Imap_Client_Data_Fetch;
-use Horde_Mime_Headers_MessageId;
-use Horde_Imap_Client_Fetch_Query;
-use OCP\Files\SimpleFS\ISimpleFile;
-use Horde_Imap_Client_Data_Envelope;
-use OCP\AppFramework\Db\DoesNotExistException;
+use function trim;
class IMAPMessage implements IMessage, JsonSerializable {
use ConvertAddresses;
@@ -769,9 +769,19 @@ class IMAPMessage implements IMessage, JsonSerializable {
if ($keyword === '$important') {
$keyword = Tag::LABEL_IMPORTANT;
}
+
+ $displayName = str_replace('_', ' ', $keyword);
+ $displayName = strtoupper($displayName);
+ $displayName = mb_convert_encoding($displayName, 'UTF-8', 'UTF7-IMAP');
+ $displayName = strtolower($displayName);
+ $displayName = ucwords($displayName);
+
+ $keyword = mb_strcut($keyword, 0, 64);
+ $displayName = mb_strcut($displayName, 0, 128);
+
$tag = new Tag();
$tag->setImapLabel($keyword);
- $tag->setDisplayName(str_replace('$', '', $keyword));
+ $tag->setDisplayName($displayName);
$tag->setUserId($userId);
$t[] = $tag;
}
diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue
index 439d41e29..265c9b236 100644
--- a/src/components/Envelope.vue
+++ b/src/components/Envelope.vue
@@ -49,7 +49,6 @@
</div>
</template>
<template #subtitle>
- <span v-if="data.flags.answered" class="icon-reply" />
<span v-if="data.flags.hasAttachments === true" class="icon-public icon-attachment" />
<span v-if="draft" class="draft">
<em>{{ t('mail', 'Draft: ') }}</em>
diff --git a/src/components/TagModal.vue b/src/components/TagModal.vue
index 00f885e80..a2989cb0e 100644
--- a/src/components/TagModal.vue
+++ b/src/components/TagModal.vue
@@ -23,7 +23,7 @@
<Modal size="large" @close="onClose">
<div class="modal-content">
<h2 class="tag-title">
- {{ t('mail', 'Add tags') }}
+ {{ t('mail', 'Add default tags') }}
</h2>
<div v-for="tag in tags" :key="tag.id" class="tag-group">
<div class="tag-group__bg button"
@@ -42,20 +42,49 @@
{{ t('mail','Remove') }}
</button>
</div>
+ <h2 class="tag-title">
+ {{ t('mail', 'Add tag') }}
+ </h2>
+ <div class="create-tag">
+ <button v-if="!editing"
+ class="tagButton"
+ @click="addTagInput">
+ {{ t('mail', 'Add tag') }}
+ </button>
+ <ActionInput v-if="editing" icon="icon-tag" @submit="createTag" />
+ <ActionText
+ v-if="showSaving"
+ icon="icon-loading-small">
+ {{ t('mail', 'Saving tag …') }}
+ </ActionText>
+ </div>
</div>
</Modal>
</template>
<script>
import Modal from '@nextcloud/vue/dist/Components/Modal'
+import ActionText from '@nextcloud/vue/dist/Components/ActionText'
+import ActionInput from '@nextcloud/vue/dist/Components/ActionInput'
+import { showError } from '@nextcloud/dialogs'
+
+function randomColor() {
+ let randomHexColor = ((1 << 24) * Math.random() | 0).toString(16)
+ while (randomHexColor.length < 6) {
+ randomHexColor = '0' + randomHexColor
+ }
+ return '#' + randomHexColor
+}
export default {
name: 'TagModal',
components: {
Modal,
+ ActionText,
+ ActionInput,
},
props: {
envelope: {
- // The envelope on which this menu will act
+ // The envelope on which this menu will act
required: true,
type: Object,
},
@@ -63,6 +92,11 @@ export default {
data() {
return {
isAdded: false,
+ editing: false,
+ tagLabel: true,
+ tagInput: false,
+ showSaving: false,
+ color: randomColor(),
}
},
computed: {
@@ -85,14 +119,33 @@ export default {
this.isAdded = false
this.$store.dispatch('removeEnvelopeTag', { envelope: this.envelope, imapLabel })
},
+ addTagInput() {
+ this.editing = true
+ this.showSaving = false
+ },
+ async createTag(event) {
+ this.editing = true
+ const displayName = event.target.querySelector('input[type=text]').value
+
+ try {
+ await this.$store.dispatch('createTag', {
+ displayName,
+ color: randomColor(displayName),
+ })
+ } catch (error) {
+ console.debug(error)
+ showError(this.t('mail', 'An error occurred, unable to create the tag.'))
+ } finally {
+ this.showSaving = false
+ this.tagLabel = true
+ }
+ },
},
}
+
</script>
<style lang="scss" scoped>
-::v-deep .modal-wrapper .modal-container {
- overflow: scroll !important;
-}
::v-deep .modal-content {
padding-left: 20px;
padding-right: 20px;
@@ -134,4 +187,39 @@ export default {
font-weight: bold;
margin: 8px 24px;
}
+.app-navigation-entry-bullet-wrapper {
+ width: 44px;
+ height: 44px;
+ display: inline-block;
+ position: absolute;
+ list-style: none;
+
+ .color0 {
+ width: 30px !important;
+ height: 30px;
+ border-radius: 50%;
+ background-size: 14px;
+ margin-top: -65px;
+ margin-left: 180px;
+ z-index: 2;
+ display: flex;
+ position: relative;
+ }
+}
+.icon-colorpicker {
+ background-image: var(--icon-add-fff);
+}
+.tagButton {
+ display: inline-block;
+ margin-left: 10px;
+}
+.action {
+ list-style: none;
+}
+::v-deep .action-input {
+ margin-left: -31px;
+}
+::v-deep .icon-tag {
+ background-image: none;
+}
</style>
diff --git a/src/service/MessageService.js b/src/service/MessageService.js
index 6e51aa8f0..3c671c2e7 100644
--- a/src/service/MessageService.js
+++ b/src/service/MessageService.js
@@ -116,6 +116,12 @@ export function setEnvelopeFlag(id, flag, value) {
},
})
}
+export async function createEnvelopeTag(displayName, color) {
+ const url = generateUrl('/apps/mail/api/tags')
+
+ const { data } = await axios.post(url, { displayName, color })
+ return data
+}
export async function setEnvelopeTag(id, imapLabel) {
const url = generateUrl('/apps/mail/api/messages/{id}/tags/{imapLabel}', {
diff --git a/src/store/actions.js b/src/store/actions.js
index 8deae447e..ad223710d 100644
--- a/src/store/actions.js
+++ b/src/store/actions.js
@@ -58,6 +58,7 @@ import {
patchMailbox,
} from '../service/MailboxService'
import {
+ createEnvelopeTag,
deleteMessage,
fetchEnvelope,
fetchEnvelopes,
@@ -777,6 +778,11 @@ export default {
throw error
}
},
+ async createTag({ commit }, { displayName, color }) {
+ const tag = await createEnvelopeTag(displayName, color)
+ commit('addTag', { tag })
+
+ },
async addEnvelopeTag({ commit, getters }, { envelope, imapLabel }) {
// TODO: fetch tags indepently of envelopes and only send tag id here
const tag = await setEnvelopeTag(envelope.databaseId, imapLabel)