mirror of
https://github.com/nextcloud/mail.git
synced 2025-07-23 02:57:02 +00:00
feat: Allow disabling of threaded view
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
This commit is contained in:
@ -345,6 +345,11 @@ return [
|
||||
'url' => '/api/settings/importance-classification-default',
|
||||
'verb' => 'PUT'
|
||||
],
|
||||
[
|
||||
'name' => 'settings#setLayoutMessageView',
|
||||
'url' => '/api/settings/layout-message-view',
|
||||
'verb' => 'PUT'
|
||||
],
|
||||
[
|
||||
'name' => 'trusted_senders#setTrusted',
|
||||
'url' => '/api/trustedsenders/{email}',
|
||||
|
@ -21,6 +21,8 @@ use OCP\IUser;
|
||||
interface IMailSearch {
|
||||
public const ORDER_NEWEST_FIRST = 'DESC';
|
||||
public const ORDER_OLDEST_FIRST = 'ASC';
|
||||
public const VIEW_SINGLETON = 'singleton';
|
||||
public const VIEW_THREADED = 'threaded';
|
||||
/**
|
||||
* @throws DoesNotExistException
|
||||
* @throws ClientException
|
||||
@ -37,6 +39,7 @@ interface IMailSearch {
|
||||
* @param string|null $filter
|
||||
* @param int|null $cursor
|
||||
* @param int|null $limit
|
||||
* @param string|null $view
|
||||
*
|
||||
* @return Message[]
|
||||
*
|
||||
@ -48,7 +51,8 @@ interface IMailSearch {
|
||||
string $sortOrder,
|
||||
?string $filter,
|
||||
?int $cursor,
|
||||
?int $limit): array;
|
||||
?int $limit,
|
||||
?string $view): array;
|
||||
|
||||
/**
|
||||
* Run a search through all mailboxes of a user.
|
||||
|
@ -124,6 +124,7 @@ class MessagesController extends Controller {
|
||||
* @param int $cursor
|
||||
* @param string $filter
|
||||
* @param int|null $limit
|
||||
* @param string $view returns messages in requested view ('singleton' or 'threaded')
|
||||
*
|
||||
* @return JSONResponse
|
||||
*
|
||||
@ -134,7 +135,8 @@ class MessagesController extends Controller {
|
||||
public function index(int $mailboxId,
|
||||
?int $cursor = null,
|
||||
?string $filter = null,
|
||||
?int $limit = null): JSONResponse {
|
||||
?int $limit = null,
|
||||
?string $view = null): JSONResponse {
|
||||
try {
|
||||
$mailbox = $this->mailManager->getMailbox($this->currentUserId, $mailboxId);
|
||||
$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
|
||||
@ -143,17 +145,21 @@ class MessagesController extends Controller {
|
||||
}
|
||||
|
||||
$this->logger->debug("loading messages of mailbox <$mailboxId>");
|
||||
$sort = $this->preferences->getPreference($this->currentUserId, 'sort-order', 'newest') === 'newest' ? IMailSearch::ORDER_NEWEST_FIRST: IMailSearch::ORDER_OLDEST_FIRST;
|
||||
|
||||
$view = $view === 'singleton' ? IMailSearch::VIEW_SINGLETON : IMailSearch::VIEW_THREADED;
|
||||
|
||||
$order = $this->preferences->getPreference($this->currentUserId, 'sort-order', 'newest') === 'newest' ? 'DESC': 'ASC';
|
||||
$messages = $this->mailSearch->findMessages(
|
||||
$account,
|
||||
$mailbox,
|
||||
$sort,
|
||||
$filter === '' ? null : $filter,
|
||||
$cursor,
|
||||
$limit,
|
||||
$view
|
||||
);
|
||||
return new JSONResponse(
|
||||
$this->mailSearch->findMessages(
|
||||
$account,
|
||||
$mailbox,
|
||||
$order,
|
||||
$filter === '' ? null : $filter,
|
||||
$cursor,
|
||||
$limit
|
||||
)
|
||||
$messages
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -211,6 +211,7 @@ class PageController extends Controller {
|
||||
'app-version' => $this->config->getAppValue('mail', 'installed_version'),
|
||||
'external-avatars' => $this->preferences->getPreference($this->currentUserId, 'external-avatars', 'true'),
|
||||
'layout-mode' => $this->preferences->getPreference($this->currentUserId, 'layout-mode', 'vertical-split'),
|
||||
'layout-message-view' => $this->preferences->getPreference($this->currentUserId, 'layout-message-view', $this->config->getAppValue('mail', 'layout_message_view', 'threaded')),
|
||||
'reply-mode' => $this->preferences->getPreference($this->currentUserId, 'reply-mode', 'top'),
|
||||
'collect-data' => $this->preferences->getPreference($this->currentUserId, 'collect-data', 'true'),
|
||||
'search-priority-body' => $this->preferences->getPreference($this->currentUserId, 'search-priority-body', 'false'),
|
||||
|
@ -124,4 +124,9 @@ class SettingsController extends Controller {
|
||||
return new JSONResponse([]);
|
||||
}
|
||||
|
||||
public function setLayoutMessageView(string $value): JSONResponse {
|
||||
$this->config->setAppValue('mail', 'layout_message_view', $value);
|
||||
return new JSONResponse([]);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -808,21 +808,23 @@ class MessageMapper extends QBMapper {
|
||||
} else {
|
||||
$select = $qb->select(['m.id', 'm.sent_at']);
|
||||
}
|
||||
|
||||
$select->from($this->getTableName(), 'm');
|
||||
|
||||
$selfJoin = $select->expr()->andX(
|
||||
$select->expr()->eq('m.mailbox_id', 'm2.mailbox_id', IQueryBuilder::PARAM_INT),
|
||||
$select->expr()->eq('m.thread_root_id', 'm2.thread_root_id', IQueryBuilder::PARAM_INT),
|
||||
$select->expr()->orX(
|
||||
$select->expr()->lt('m.sent_at', 'm2.sent_at', IQueryBuilder::PARAM_INT),
|
||||
$select->expr()->andX(
|
||||
$select->expr()->eq('m.sent_at', 'm2.sent_at', IQueryBuilder::PARAM_INT),
|
||||
$select->expr()->lt('m.message_id', 'm2.message_id', IQueryBuilder::PARAM_STR),
|
||||
if ($query->getThreaded()) {
|
||||
$selfJoin = $select->expr()->andX(
|
||||
$select->expr()->eq('m.mailbox_id', 'm2.mailbox_id', IQueryBuilder::PARAM_INT),
|
||||
$select->expr()->eq('m.thread_root_id', 'm2.thread_root_id', IQueryBuilder::PARAM_INT),
|
||||
$select->expr()->orX(
|
||||
$select->expr()->lt('m.sent_at', 'm2.sent_at', IQueryBuilder::PARAM_INT),
|
||||
$select->expr()->andX(
|
||||
$select->expr()->eq('m.sent_at', 'm2.sent_at', IQueryBuilder::PARAM_INT),
|
||||
$select->expr()->lt('m.message_id', 'm2.message_id', IQueryBuilder::PARAM_STR),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$select->from($this->getTableName(), 'm')
|
||||
->leftJoin('m', $this->getTableName(), 'm2', $selfJoin);
|
||||
);
|
||||
$select->leftJoin('m', $this->getTableName(), 'm2', $selfJoin);
|
||||
}
|
||||
|
||||
if (!empty($query->getFrom())) {
|
||||
$select->innerJoin('m', 'mail_recipients', 'r0', 'm.id = r0.message_id');
|
||||
@ -1008,7 +1010,9 @@ class MessageMapper extends QBMapper {
|
||||
);
|
||||
}
|
||||
|
||||
$select->andWhere($qb->expr()->isNull('m2.id'));
|
||||
if ($query->getThreaded()) {
|
||||
$select->andWhere($qb->expr()->isNull('m2.id'));
|
||||
}
|
||||
|
||||
if ($sortOrder === 'ASC') {
|
||||
$select->orderBy('m.sent_at', $sortOrder);
|
||||
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace OCA\Mail\Listener;
|
||||
|
||||
use Generator;
|
||||
use OCA\Mail\Contracts\IUserPreferences;
|
||||
use OCA\Mail\Db\MessageMapper;
|
||||
use OCA\Mail\Events\SynchronizationEvent;
|
||||
use OCA\Mail\IMAP\Threading\Container;
|
||||
@ -17,6 +18,7 @@ use OCA\Mail\IMAP\Threading\DatabaseMessage;
|
||||
use OCA\Mail\IMAP\Threading\ThreadBuilder;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
|
||||
use function array_chunk;
|
||||
use function gc_collect_cycles;
|
||||
use function iterator_to_array;
|
||||
@ -27,16 +29,11 @@ use function iterator_to_array;
|
||||
class AccountSynchronizedThreadUpdaterListener implements IEventListener {
|
||||
private const WRITE_IDS_CHUNK_SIZE = 500;
|
||||
|
||||
/** @var MessageMapper */
|
||||
private $mapper;
|
||||
|
||||
/** @var ThreadBuilder */
|
||||
private $builder;
|
||||
|
||||
public function __construct(MessageMapper $mapper,
|
||||
ThreadBuilder $builder) {
|
||||
$this->mapper = $mapper;
|
||||
$this->builder = $builder;
|
||||
public function __construct(
|
||||
private IUserPreferences $preferences,
|
||||
private MessageMapper $mapper,
|
||||
private ThreadBuilder $builder,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
@ -45,6 +42,13 @@ class AccountSynchronizedThreadUpdaterListener implements IEventListener {
|
||||
return;
|
||||
}
|
||||
$logger = $event->getLogger();
|
||||
$userId = $event->getAccount()->getUserId();
|
||||
|
||||
if ($this->preferences->getPreference($userId, 'layout-message-view', 'threaded') !== 'threaded') {
|
||||
$event->getLogger()->debug('Skipping threading as the user prefers a flat view');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$event->isRebuildThreads()) {
|
||||
$event->getLogger()->debug('Skipping threading as there were no significant changes');
|
||||
return;
|
||||
|
@ -80,6 +80,7 @@ class MailSearch implements IMailSearch {
|
||||
* @param string|null $filter
|
||||
* @param int|null $cursor
|
||||
* @param int|null $limit
|
||||
* @param string|null $view
|
||||
*
|
||||
* @return Message[]
|
||||
*
|
||||
@ -91,7 +92,8 @@ class MailSearch implements IMailSearch {
|
||||
string $sortOrder,
|
||||
?string $filter,
|
||||
?int $cursor,
|
||||
?int $limit): array {
|
||||
?int $limit,
|
||||
?string $view): array {
|
||||
if ($mailbox->hasLocks($this->timeFactory->getTime())) {
|
||||
throw MailboxLockedException::from($mailbox);
|
||||
}
|
||||
@ -103,6 +105,9 @@ class MailSearch implements IMailSearch {
|
||||
if ($cursor !== null) {
|
||||
$query->setCursor($cursor);
|
||||
}
|
||||
if ($view !== null) {
|
||||
$query->setThreaded($view === self::VIEW_THREADED);
|
||||
}
|
||||
// In flagged we don't want anything but flagged messages
|
||||
if ($mailbox->isSpecialUse(Horde_Imap_Client::SPECIALUSE_FLAGGED)) {
|
||||
$query->addFlag(Flag::is(Flag::FLAGGED));
|
||||
|
@ -12,6 +12,8 @@ namespace OCA\Mail\Service\Search;
|
||||
class SearchQuery {
|
||||
/** @var int|null */
|
||||
private $cursor;
|
||||
|
||||
private bool $threaded = true;
|
||||
|
||||
/** @var Flag[] */
|
||||
private $flags = [];
|
||||
@ -72,6 +74,14 @@ class SearchQuery {
|
||||
$this->cursor = $cursor;
|
||||
}
|
||||
|
||||
public function getThreaded(): bool {
|
||||
return $this->threaded;
|
||||
}
|
||||
|
||||
public function setThreaded(bool $threaded): void {
|
||||
$this->threaded = $threaded;
|
||||
}
|
||||
|
||||
public function getMatch(): string {
|
||||
return $this->match;
|
||||
}
|
||||
|
@ -80,6 +80,12 @@ class AdminSettings implements ISettings {
|
||||
$this->config->getAppValue('mail', 'allow_new_mail_accounts', 'yes') === 'yes'
|
||||
);
|
||||
|
||||
$this->initialStateService->provideInitialState(
|
||||
Application::APP_ID,
|
||||
'layout_message_view',
|
||||
$this->config->getAppValue('mail', 'layout_message_view', 'threaded')
|
||||
);
|
||||
|
||||
$this->initialStateService->provideInitialState(
|
||||
Application::APP_ID,
|
||||
'llm_processing',
|
||||
|
@ -72,6 +72,24 @@
|
||||
{{ t('mail', 'Horizontal split') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<h6>{{ t('mail', 'Message View Mode') }}</h6>
|
||||
<div class="sorting">
|
||||
<NcCheckboxRadioSwitch type="radio"
|
||||
name="message_view_mode_radio"
|
||||
value="threaded"
|
||||
:checked="layoutMessageView"
|
||||
@update:checked="setLayoutMessageView('threaded')">
|
||||
{{ t('mail', 'Show all messages in thread') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<NcCheckboxRadioSwitch type="radio"
|
||||
name="message_view_mode_radio"
|
||||
value="singleton"
|
||||
:checked="layoutMessageView"
|
||||
@update:checked="setLayoutMessageView('singleton')">
|
||||
{{ t('mail', 'Show only the selected message') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
|
||||
<h6>{{ t('mail', 'Sorting') }}</h6>
|
||||
<div class="sorting">
|
||||
<NcCheckboxRadioSwitch class="sorting__switch"
|
||||
@ -398,6 +416,9 @@ export default {
|
||||
layoutMode() {
|
||||
return this.mainStore.getPreference('layout-mode', 'vertical-split')
|
||||
},
|
||||
layoutMessageView() {
|
||||
return this.mainStore.getPreference('layout-message-view')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showSettings(value) {
|
||||
@ -439,6 +460,16 @@ export default {
|
||||
Logger.error('Could not save preferences', { error })
|
||||
}
|
||||
},
|
||||
async setLayoutMessageView(value) {
|
||||
try {
|
||||
await this.mainStore.savePreference({
|
||||
key: 'layout-message-view',
|
||||
value,
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error('Could not save preferences', { error })
|
||||
}
|
||||
},
|
||||
async onOpen() {
|
||||
this.showSettings = true
|
||||
},
|
||||
|
@ -190,7 +190,12 @@
|
||||
<template #icon>
|
||||
<OpenInNewIcon :size="16" />
|
||||
</template>
|
||||
{{ t('mail', 'Move thread') }}
|
||||
<template v-if="layoutMessageViewThreaded">
|
||||
{{ t('mail', 'Move thread') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('mail', 'Move Message') }}
|
||||
</template>
|
||||
</ActionButton>
|
||||
<ActionButton v-if="showArchiveButton && hasArchiveAcl"
|
||||
:close-after-click="true"
|
||||
@ -199,7 +204,12 @@
|
||||
<template #icon>
|
||||
<ArchiveIcon :size="16" />
|
||||
</template>
|
||||
{{ t('mail', 'Archive thread') }}
|
||||
<template v-if="layoutMessageViewThreaded">
|
||||
{{ t('mail', 'Archive thread') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('mail', 'Archive message') }}
|
||||
</template>
|
||||
</ActionButton>
|
||||
<ActionButton v-if="hasDeleteAcl"
|
||||
:close-after-click="true"
|
||||
@ -207,7 +217,12 @@
|
||||
<template #icon>
|
||||
<DeleteIcon :size="16" />
|
||||
</template>
|
||||
{{ t('mail', 'Delete thread') }}
|
||||
<template v-if="layoutMessageViewThreaded">
|
||||
{{ t('mail', 'Delete thread') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('mail', 'Delete message') }}
|
||||
</template>
|
||||
</ActionButton>
|
||||
<ActionButton :close-after-click="false"
|
||||
@click="showMoreActionOptions">
|
||||
@ -311,7 +326,7 @@
|
||||
<MoveModal v-if="showMoveModal"
|
||||
:account="account"
|
||||
:envelopes="[data]"
|
||||
:move-thread="true"
|
||||
:move-thread="listViewThreaded"
|
||||
@move="onMove"
|
||||
@close="onCloseMoveModal" />
|
||||
<EventModal v-if="showEventModal"
|
||||
@ -493,6 +508,9 @@ export default {
|
||||
oneLineLayout() {
|
||||
return this.overwriteOneLineMobile ? false : this.mainStore.getPreference('layout-mode', 'vertical-split') === 'no-split'
|
||||
},
|
||||
layoutMessageViewThreaded() {
|
||||
return this.mainStore.getPreference('layout-message-view', 'threaded') === 'threaded'
|
||||
},
|
||||
hasMultipleRecipients() {
|
||||
if (!this.account) {
|
||||
console.error('account is undefined', {
|
||||
@ -773,9 +791,15 @@ export default {
|
||||
this.$emit('delete', this.data.databaseId)
|
||||
|
||||
try {
|
||||
await this.mainStore.deleteThread({
|
||||
envelope: this.data,
|
||||
})
|
||||
if (this.layoutMessageViewThreaded) {
|
||||
await this.mainStore.deleteThread({
|
||||
envelope: this.data,
|
||||
})
|
||||
} else {
|
||||
await this.mainStore.deleteMessage({
|
||||
id: this.data.databaseId,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showError(await matchError(error, {
|
||||
[NoTrashMailboxConfiguredError.getName()]() {
|
||||
@ -807,10 +831,17 @@ export default {
|
||||
this.$emit('archive', this.data.databaseId)
|
||||
|
||||
try {
|
||||
await this.mainStore.moveThread({
|
||||
envelope: this.data,
|
||||
destMailboxId: this.account.archiveMailboxId,
|
||||
})
|
||||
if (this.layoutMessageViewThreaded) {
|
||||
await this.mainStore.moveThread({
|
||||
envelope: this.data,
|
||||
destMailboxId: this.account.archiveMailboxId,
|
||||
})
|
||||
} else {
|
||||
await this.mainStore.moveMessage({
|
||||
id: this.data.databaseId,
|
||||
destMailboxId: this.account.archiveMailboxId,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('could not archive message', error)
|
||||
showError(t('mail', 'Could not archive message'))
|
||||
@ -825,11 +856,19 @@ export default {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.mainStore.snoozeThread({
|
||||
envelope: this.data,
|
||||
unixTimestamp: timestamp / 1000,
|
||||
destMailboxId: this.account.snoozeMailboxId,
|
||||
})
|
||||
if (this.layoutMessageViewThreaded) {
|
||||
await this.mainStore.snoozeThread({
|
||||
envelope: this.data,
|
||||
unixTimestamp: timestamp / 1000,
|
||||
destMailboxId: this.account.snoozeMailboxId,
|
||||
})
|
||||
} else {
|
||||
await this.mainStore.snoozeMessage({
|
||||
id: this.data.databaseId,
|
||||
unixTimestamp: timestamp / 1000,
|
||||
destMailboxId: this.account.snoozeMailboxId,
|
||||
})
|
||||
}
|
||||
showSuccess(t('mail', 'Thread was snoozed'))
|
||||
} catch (error) {
|
||||
logger.error('could not snooze thread', error)
|
||||
@ -841,9 +880,15 @@ export default {
|
||||
this.setSelected(false)
|
||||
|
||||
try {
|
||||
await this.mainStore.unSnoozeThread({
|
||||
envelope: this.data,
|
||||
})
|
||||
if (this.layoutMessageViewThreaded) {
|
||||
await this.mainStore.unSnoozeThread({
|
||||
envelope: this.data,
|
||||
})
|
||||
} else {
|
||||
await this.mainStore.unSnoozeMessage({
|
||||
id: this.data.databaseId,
|
||||
})
|
||||
}
|
||||
showSuccess(t('mail', 'Thread was unsnoozed'))
|
||||
} catch (error) {
|
||||
logger.error('Could not unsnooze thread', error)
|
||||
|
@ -8,6 +8,7 @@
|
||||
:id="genId(mailbox)"
|
||||
:key="genId(mailbox)"
|
||||
v-droppable-mailbox="{
|
||||
mainStore: mainStore,
|
||||
mailboxId: mailbox.databaseId,
|
||||
accountId: mailbox.accountId,
|
||||
isValidDropTarget,
|
||||
|
@ -126,6 +126,10 @@ export default {
|
||||
return []
|
||||
}
|
||||
|
||||
if (this.mainStore.getPreference('layout-message-view', 'threaded') === 'singleton') {
|
||||
return [envelope]
|
||||
}
|
||||
|
||||
const envelopes = this.mainStore.getEnvelopesByThreadRootId(envelope.accountId, envelope.threadRootId)
|
||||
if (envelopes.length === 0) {
|
||||
return []
|
||||
@ -281,11 +285,12 @@ export default {
|
||||
this.expandedThreads = [this.threadId]
|
||||
this.errorMessage = ''
|
||||
this.error = undefined
|
||||
await this.fetchThread()
|
||||
if (this.mainStore.getPreference('layout-message-view', 'threaded') === 'threaded') {
|
||||
await this.fetchThread()
|
||||
}
|
||||
this.updateParticipantsToDisplay()
|
||||
this.updateSummary()
|
||||
this.loadedThreads = 0
|
||||
|
||||
},
|
||||
async fetchThread() {
|
||||
this.loading = true
|
||||
|
@ -253,6 +253,36 @@
|
||||
</article>
|
||||
<MicrosoftAdminOauthSettings :tenant-id="microsoftOauthTenantId" :client-id="microsoftOauthClientId" />
|
||||
</div>
|
||||
<div class="app-description">
|
||||
<h3>{{ t('mail', 'User Interface Preference Defaults') }}</h3>
|
||||
<article>
|
||||
<p>
|
||||
{{ t('mail', 'These settings are used to pre-configure the user interface preferences they can be overridden by the user in the mail settings') }}
|
||||
</p>
|
||||
</article>
|
||||
<br>
|
||||
<article>
|
||||
<p>
|
||||
{{ t('mail', 'Message View Mode') }}
|
||||
</p>
|
||||
<p>
|
||||
<NcCheckboxRadioSwitch type="radio"
|
||||
name="message_view_mode_radio"
|
||||
value="threaded"
|
||||
:checked.sync="layoutMessageView"
|
||||
@update:checked="setLayoutMessageView('threaded')">
|
||||
{{ t('mail', 'Show all messages in thread') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<NcCheckboxRadioSwitch type="radio"
|
||||
name="message_view_mode_radio"
|
||||
value="singleton"
|
||||
:checked.sync="layoutMessageView"
|
||||
@update:checked="setLayoutMessageView('singleton')">
|
||||
{{ t('mail', 'Show only the selected message') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</template>
|
||||
|
||||
@ -278,6 +308,7 @@ import {
|
||||
updateLlmEnabled,
|
||||
updateEnabledSmartReply,
|
||||
setImportanceClassificationEnabledByDefault,
|
||||
setLayoutMessageView,
|
||||
} from '../../service/SettingsService.js'
|
||||
|
||||
const googleOauthClientId = loadState('mail', 'google_oauth_client_id', null) ?? undefined
|
||||
@ -342,6 +373,7 @@ export default {
|
||||
isLlmFreePromptConfigured: loadState('mail', 'enabled_llm_free_prompt_backend'),
|
||||
isClassificationEnabledByDefault: loadState('mail', 'llm_processing', true),
|
||||
isImportanceClassificationEnabledByDefault: loadState('mail', 'importance_classification_default', true),
|
||||
layoutMessageView: loadState('mail', 'layout_message_view'),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -410,6 +442,9 @@ export default {
|
||||
logger.error('Could not save default classification setting', { error })
|
||||
}
|
||||
},
|
||||
async setLayoutMessageView(value) {
|
||||
await setLayoutMessageView(value)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -2,20 +2,15 @@
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import useMainStore from '../../../store/mainStore.js'
|
||||
import logger from '../../../logger.js'
|
||||
import dragEventBus from '../util/dragEventBus.js'
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
|
||||
setActivePinia(createPinia())
|
||||
const mainStore = useMainStore()
|
||||
|
||||
export class DroppableMailbox {
|
||||
|
||||
constructor(el, componentInstance, options) {
|
||||
this.el = el
|
||||
this.options = options
|
||||
this.mainStore = options.mainStore
|
||||
this.registerListeners.bind(this)(el)
|
||||
this.setInitialAttributes()
|
||||
}
|
||||
@ -149,10 +144,17 @@ export class DroppableMailbox {
|
||||
item.setAttribute('draggable-envelope', 'pending')
|
||||
|
||||
try {
|
||||
await mainStore.moveThread({
|
||||
envelope,
|
||||
destMailboxId: this.options.mailboxId,
|
||||
})
|
||||
if (this.mainStore.getPreference('layout-message-view') === 'threaded') {
|
||||
await this.mainStore.moveThread({
|
||||
envelope,
|
||||
destMailboxId: this.options.mailboxId,
|
||||
})
|
||||
} else {
|
||||
await this.mainStore.moveMessage({
|
||||
id: envelope.databaseId,
|
||||
destMailboxId: this.options.mailboxId,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
item.removeAttribute('draggable-envelope')
|
||||
logger.error('could not move messages', error)
|
||||
|
@ -70,6 +70,10 @@ export default function initAfterAppCreation() {
|
||||
key: 'layout-mode',
|
||||
value: preferences['layout-mode'],
|
||||
})
|
||||
mainStore.savePreferenceMutation({
|
||||
key: 'layout-message-view',
|
||||
value: preferences['layout-message-view'],
|
||||
})
|
||||
mainStore.savePreferenceMutation({
|
||||
key: 'follow-up-reminders',
|
||||
value: preferences['follow-up-reminders'],
|
||||
|
@ -32,7 +32,7 @@ export function fetchEnvelope(accountId, id) {
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchEnvelopes(accountId, mailboxId, query, cursor, limit) {
|
||||
export function fetchEnvelopes(accountId, mailboxId, query, cursor, limit, sort, view) {
|
||||
const url = generateUrl('/apps/mail/api/messages')
|
||||
const params = {
|
||||
mailboxId,
|
||||
@ -47,6 +47,12 @@ export function fetchEnvelopes(accountId, mailboxId, query, cursor, limit) {
|
||||
if (cursor) {
|
||||
params.cursor = cursor
|
||||
}
|
||||
if (sort) {
|
||||
params.sort = sort
|
||||
}
|
||||
if (view) {
|
||||
params.view = view
|
||||
}
|
||||
|
||||
return axios
|
||||
.get(url, {
|
||||
|
@ -89,3 +89,16 @@ export const setImportanceClassificationEnabledByDefault = async (enabledByDefau
|
||||
enabledByDefault,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export const setLayoutMessageView = async (value) => {
|
||||
const url = generateUrl('/apps/mail/api/settings/layout-message-view')
|
||||
const data = {
|
||||
value,
|
||||
}
|
||||
const resp = await axios.put(url, data)
|
||||
return resp.data
|
||||
}
|
||||
|
@ -667,6 +667,8 @@ export default function mainStoreActions() {
|
||||
mailboxId: mb.databaseId,
|
||||
query,
|
||||
addToUnifiedMailboxes: false,
|
||||
sort: this.getPreference('sort-order'),
|
||||
view: this.getPreference('layout-message-view'),
|
||||
}),
|
||||
),
|
||||
Promise.all.bind(Promise),
|
||||
@ -701,7 +703,7 @@ export default function mainStoreActions() {
|
||||
}),
|
||||
),
|
||||
),
|
||||
)(mailbox.accountId, mailboxId, query, undefined, PAGE_SIZE, this.getPreference('sort-order'))
|
||||
)(mailbox.accountId, mailboxId, query, undefined, PAGE_SIZE, this.getPreference('sort-order'), this.getPreference('layout-message-view'))
|
||||
})
|
||||
},
|
||||
async fetchNextEnvelopePage({
|
||||
@ -1819,12 +1821,18 @@ export default function mainStoreActions() {
|
||||
* @return {Array} list of envelope ids
|
||||
*/
|
||||
appendOrReplaceEnvelopeId(existing, envelope) {
|
||||
const index = existing.findIndex((id) => this.envelopes[id].threadRootId === envelope.threadRootId)
|
||||
if (index === -1) {
|
||||
|
||||
if (this.getPreference('layout-message-view') === 'singleton') {
|
||||
existing.push(envelope.databaseId)
|
||||
} else {
|
||||
existing[index] = envelope.databaseId
|
||||
const index = existing.findIndex((id) => this.envelopes[id].threadRootId === envelope.threadRootId)
|
||||
if (index === -1) {
|
||||
existing.push(envelope.databaseId)
|
||||
} else {
|
||||
existing[index] = envelope.databaseId
|
||||
}
|
||||
}
|
||||
|
||||
return existing
|
||||
},
|
||||
savePreferenceMutation({
|
||||
|
@ -168,7 +168,7 @@ class PageControllerTest extends TestCase {
|
||||
$account1 = $this->createMock(Account::class);
|
||||
$account2 = $this->createMock(Account::class);
|
||||
$mailbox = $this->createMock(Mailbox::class);
|
||||
$this->preferences->expects($this->exactly(10))
|
||||
$this->preferences->expects($this->exactly(11))
|
||||
->method('getPreference')
|
||||
->willReturnMap([
|
||||
[$this->userId, 'account-settings', '[]', json_encode([])],
|
||||
@ -179,6 +179,7 @@ class PageControllerTest extends TestCase {
|
||||
[$this->userId, 'search-priority-body', 'false', 'false'],
|
||||
[$this->userId, 'start-mailbox-id', null, '123'],
|
||||
[$this->userId, 'layout-mode', 'vertical-split', 'vertical-split'],
|
||||
[$this->userId, 'layout-message-view', 'threaded', 'threaded'],
|
||||
[$this->userId, 'follow-up-reminders', 'true', 'true'],
|
||||
[$this->userId, 'internal-addresses', 'false', 'false'],
|
||||
]);
|
||||
@ -257,10 +258,11 @@ class PageControllerTest extends TestCase {
|
||||
['version', '0.0.0', '26.0.0'],
|
||||
['app.mail.attachment-size-limit', 0, 123],
|
||||
]);
|
||||
$this->config->expects($this->exactly(6))
|
||||
$this->config->expects($this->exactly(7))
|
||||
->method('getAppValue')
|
||||
->withConsecutive(
|
||||
[ 'mail', 'installed_version' ],
|
||||
['mail', 'layout_message_view' ],
|
||||
['mail', 'google_oauth_client_id' ],
|
||||
['mail', 'microsoft_oauth_client_id' ],
|
||||
['mail', 'microsoft_oauth_tenant_id' ],
|
||||
@ -268,6 +270,7 @@ class PageControllerTest extends TestCase {
|
||||
['mail', 'allow_new_mail_accounts', 'yes'],
|
||||
)->willReturnOnConsecutiveCalls(
|
||||
$this->returnValue('1.2.3'),
|
||||
$this->returnValue('threaded'),
|
||||
$this->returnValue(''),
|
||||
$this->returnValue(''),
|
||||
$this->returnValue(''),
|
||||
@ -321,6 +324,7 @@ class PageControllerTest extends TestCase {
|
||||
'tag-classified-messages' => 'false',
|
||||
'search-priority-body' => 'false',
|
||||
'layout-mode' => 'vertical-split',
|
||||
'layout-message-view' => 'threaded',
|
||||
'follow-up-reminders' => 'true',
|
||||
]],
|
||||
['prefill_displayName', 'Jane Doe'],
|
||||
|
@ -81,6 +81,7 @@ class MailSearchTest extends TestCase {
|
||||
'DESC',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
@ -97,6 +98,7 @@ class MailSearchTest extends TestCase {
|
||||
'DESC',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
@ -117,6 +119,7 @@ class MailSearchTest extends TestCase {
|
||||
'DESC',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
@ -156,6 +159,7 @@ class MailSearchTest extends TestCase {
|
||||
'DESC',
|
||||
'my search',
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
@ -198,6 +202,7 @@ class MailSearchTest extends TestCase {
|
||||
'DESC',
|
||||
'my search',
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
|
@ -37,7 +37,7 @@ class AdminSettingsTest extends TestCase {
|
||||
}
|
||||
|
||||
public function testGetForm() {
|
||||
$this->serviceMock->getParameter('initialStateService')->expects($this->exactly(12))
|
||||
$this->serviceMock->getParameter('initialStateService')->expects($this->exactly(13))
|
||||
->method('provideInitialState')
|
||||
->withConsecutive(
|
||||
[
|
||||
@ -55,6 +55,11 @@ class AdminSettingsTest extends TestCase {
|
||||
'allow_new_mail_accounts',
|
||||
$this->anything()
|
||||
],
|
||||
[
|
||||
Application::APP_ID,
|
||||
'layout_message_view',
|
||||
$this->anything()
|
||||
],
|
||||
[
|
||||
Application::APP_ID,
|
||||
'llm_processing',
|
||||
|
Reference in New Issue
Block a user