From 4662b981350d3f14b3519f0370aa2c642b2d66ba Mon Sep 17 00:00:00 2001 From: "J.-S. Caux" <J.S.Caux@uva.nl> Date: Fri, 23 Oct 2020 15:19:06 +0200 Subject: [PATCH] Add thread (un)focusing to messages listing --- apimail/api/views.py | 38 ++++++++++++ .../assets/vue/components/MessagesTable.vue | 62 +++++++++++++++++-- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/apimail/api/views.py b/apimail/api/views.py index 0b35d303e..2c628a066 100644 --- a/apimail/api/views.py +++ b/apimail/api/views.py @@ -152,6 +152,44 @@ class StoredMessageFilterBackend(filters.BaseFilterBackend): queryset = StoredMessage.objects.all() queryfilter = Q() + view_option = request.query_params.get('view', None) + if view_option == 'by_thread': + queryset = queryset.filter(data__References__isnull=True) + + thread_of_uuid = request.query_params.get('thread_of_uuid', None) + if thread_of_uuid: + # Identify email thread using data['References'] or data['Message-Id']. + # Since Django ORM does not support hyphenated lookups, use raw SQL. + # First find the message at the root of the thread. which is the message + # with Message-Id first in the list of the message with uuid `thread_of_uuid` + reference_message = get_object_or_404(StoredMessage, uuid=thread_of_uuid) + + # First try the RFC 2822 References MIME header: + head_id = None + try: + head_id = reference_message.data['References'].split()[0] + except KeyError: + # Then try the RFC 2822 In-Reply-To + try: + head_id = reference_message.data['In-Reply-To'].split()[0] + except KeyError: + # This message is head of the thread as far as can be guessed + head_id = reference_message.data['Message-Id'] + + if head_id: + thread_query_raw = ( + "SELECT apimail_storedmessage.id FROM apimail_storedmessage " + "WHERE UPPER((apimail_storedmessage.data ->> %s)::text) LIKE UPPER(%s) " + "OR UPPER((apimail_storedmessage.data ->> %s)::text) LIKE UPPER(%s) " + "ORDER BY apimail_storedmessage.datetimestamp DESC;") + sm_ids = [sm.id for sm in StoredMessage.objects.raw( + thread_query_raw, + ['Message-Id', '%%%s%%' % head_id, 'References', '%%%s%%' % head_id])] + queryset = queryset.filter(pk__in=sm_ids) + else: + queryset = queryset.filter(uuid=thread_of_uuid) + + flow = request.query_params.get('flow', None) if flow == 'in': # Restrict to incoming emails diff --git a/apimail/static/apimail/assets/vue/components/MessagesTable.vue b/apimail/static/apimail/assets/vue/components/MessagesTable.vue index 1ba4a520b..3c5e38bd2 100644 --- a/apimail/static/apimail/assets/vue/components/MessagesTable.vue +++ b/apimail/static/apimail/assets/vue/components/MessagesTable.vue @@ -132,7 +132,25 @@ <h2 class="text-center mb-2">Messages for <strong>{{ accountSelected.email }}</strong></h2> <hr class="my-2"> <b-row class="mb-0"> - <b-col class="col-lg-6"> + <b-col class="col-lg-4"> + <b-form-group + label="View by " + label-cols-sm="6" + label-align-sm="right" + label-size="sm" + > + <b-form-radio-group + v-model="viewFormat" + buttons + button-variant="outline-primary" + size="sm" + :options="viewFormatOptions" + class="float-center" + > + </b-form-radio-group> + </b-form-group> + </b-col> + <b-col md="auto"> <small class="p-2">Last loaded: {{ lastLoaded }}</small> <b-badge class="p-2" @@ -143,9 +161,9 @@ Refresh now </b-badge> </b-col> - <b-col class="col-lg-6"> + <b-col> <b-form-group - label="Auto refresh every: " + label="Refresh interval: " label-cols-sm="6" label-align-sm="right" label-size="sm" @@ -158,7 +176,7 @@ :options="refreshMinutesOptions" class="float-center" > - minutes + mins </b-form-radio-group> </b-form-group> </b-col> @@ -290,6 +308,12 @@ </b-col> </b-row> </b-card> + <div v-if="threadOf" class="m-2"> + <b-button size="sm" variant="info"><strong>Focusing on thread {{ threadOf }}</strong></b-button> + <b-button size="sm" variant="warning" @click="threadOf = null"> + Unfocus this thread + </b-button> + </div> <b-table id="my-table" class="mb-0" @@ -379,7 +403,18 @@ <path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> </svg> </b-button> + <br> </template> + <div v-if="threadOf != message.uuid"> + <b-button class="m-2" variant="primary" @click="threadOf = message.uuid"> + Focus on this thread + </b-button> + </div> + <div v-else> + <b-button class="m-2" variant="warning" @click="threadOf = null"> + Unfocus this thread + </b-button> + </div> <message-content :message="message" :tags="tags" @@ -438,6 +473,12 @@ export default { ], filter: null, filterOn: [], + viewFormat: 'by_message', + viewFormatOptions: [ + { text: 'message', value: 'by_message' }, + { text: 'thread', value: 'by_thread' }, + ], + threadOf: null, timePeriod: 'any', timePeriodOptions: [ { text: 'week', value: 'week' }, @@ -524,6 +565,13 @@ export default { var params = '?account=' + this.accountSelected.email // Our API uses limit/offset pagination params += '&limit=' + ctx.perPage + '&offset=' + ctx.perPage * (ctx.currentPage - 1) + // By message or thread view: + if (this.viewFormat == 'by_thread') { + params += '&view=by_thread' + } + if (this.threadOf) { + params += '&thread_of_uuid=' + this.threadOf + } // Add flow direction if (this.flowDirection) { params += '&flow=' + this.flowDirection @@ -605,6 +653,12 @@ export default { accountSelected: function () { this.$root.$emit('bv::refresh::table', 'my-table') }, + viewFormat: function () { + this.$root.$emit('bv::refresh::table', 'my-table') + }, + threadOf: function () { + this.$root.$emit('bv::refresh::table', 'my-table') + }, timePeriod: function () { this.$root.$emit('bv::refresh::table', 'my-table') }, -- GitLab