Add full width for mailing list

Signed-off-by: greta <gretadoci@gmail.com>
This commit is contained in:
greta 2024-01-25 11:45:27 +01:00 коммит произвёл Christoph Wurst
Родитель c525e13380
Коммит f509bf6fda
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: CC42AC2A7F0E56D8
15 изменённых файлов: 171 добавлений и 59 удалений

Просмотреть файл

@ -206,6 +206,7 @@ class PageController extends Controller {
'attachment-size-limit' => $this->config->getSystemValue('app.mail.attachment-size-limit', 0),
'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'),
'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'),

Просмотреть файл

@ -4,6 +4,32 @@
:name="t('mail', 'Mail settings')"
:show-navigation="true"
:open.sync="showSettings">
<NcAppSettingsSection id="mail-list-view" :name="t('mail', 'Layout')">
<NcCheckboxRadioSwitch value="no-split"
:button-variant="true"
name="mail-layout"
type="radio"
:checked="layoutMode"
button-variant-grouped="vertical"
@update:checked="setLayout">
<template #icon>
<CompactMode :size="20" />
</template>
{{ t('mail', 'List') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch value="vertical-split"
:button-variant="true"
name="mail-layout"
type="radio"
:checked="layoutMode"
button-variant-grouped="vertical"
@update:checked="setLayout">
<template #icon>
<VerticalSplit :size="20" />
</template>
{{ t('mail', 'Vertical split') }}
</NcCheckboxRadioSwitch>
</NcAppSettingsSection>
<NcAppSettingsSection id="account-settings" :name="t('mail', 'Account creation')">
<NcButton v-if="allowNewMailAccounts"
type="primary"
@ -120,22 +146,22 @@
</NcAppSettingsSection>
<NcAppSettingsSection id="sorting-settings" :name="t('mail', 'Sorting')">
<div class="sorting">
<CheckboxRadioSwitch class="sorting__switch"
<NcCheckboxRadioSwitch class="sorting__switch"
:checked="sortOrder"
value="newest"
name="order_radio"
type="radio"
@update:checked="onSortByDate">
{{ t('mail', 'Newest') }}
</CheckboxRadioSwitch>
<CheckboxRadioSwitch class="sorting__switch"
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch class="sorting__switch"
:checked="sortOrder"
value="oldest"
name="order_radio"
type="radio"
@update:checked="onSortByDate">
{{ t('mail', 'Oldest') }}
</CheckboxRadioSwitch>
</NcCheckboxRadioSwitch>
</div>
</NcAppSettingsSection>
<NcAppSettingsSection id="mailvelope-settings" :name="t('mail', 'Mailvelope')">
@ -202,12 +228,14 @@
<script>
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import CompactMode from 'vue-material-design-icons/ReorderHorizontal.vue'
import { NcAppSettingsSection, NcAppSettingsDialog, NcButton, NcLoadingIcon as IconLoading, NcCheckboxRadioSwitch as CheckboxRadioSwitch } from '@nextcloud/vue'
import { NcAppSettingsSection, NcAppSettingsDialog, NcButton, NcLoadingIcon as IconLoading, NcCheckboxRadioSwitch } from '@nextcloud/vue'
import IconAdd from 'vue-material-design-icons/Plus.vue'
import IconEmail from 'vue-material-design-icons/Email.vue'
import IconLock from 'vue-material-design-icons/Lock.vue'
import VerticalSplit from 'vue-material-design-icons/FormatColumns.vue'
import Logger from '../logger.js'
import SmimeCertificateModal from './smime/SmimeCertificateModal.vue'
import TrustedSenders from './TrustedSenders.vue'
@ -222,9 +250,11 @@ export default {
IconLoading,
IconLock,
SmimeCertificateModal,
CheckboxRadioSwitch,
NcCheckboxRadioSwitch,
NcAppSettingsDialog,
NcAppSettingsSection,
CompactMode,
VerticalSplit,
},
props: {
open: {
@ -270,6 +300,9 @@ export default {
allowNewMailAccounts() {
return this.$store.getters.getPreference('allow-new-accounts', true)
},
layoutMode() {
return this.$store.getters.getPreference('layout-mode', 'vertical-split')
},
},
watch: {
showSettings(value) {
@ -287,6 +320,16 @@ export default {
this.sortOrder = this.$store.getters.getPreference('sort-order', 'newest')
},
methods: {
async setLayout(value) {
try {
await this.$store.dispatch('savePreference', {
key: 'layout-mode',
value,
})
} catch (error) {
Logger.error('could not save preferences', { error })
}
},
async onOpen() {
this.showSettings = true
},
@ -465,4 +508,7 @@ p.app-settings {
margin-bottom: 6px;
color: var(--color-text-maxcontrast);
}
.app-settings-section {
list-style: none;
}
</style>

Просмотреть файл

@ -14,6 +14,7 @@
:data-envelope-id="data.databaseId"
:name="addresses"
:details="formatted()"
:one-line="oneLineLayout"
@click="onClick"
@click.ctrl.prevent="toggleSelected"
@update:menuOpen="closeMoreAndSnoozeOptions">
@ -22,16 +23,19 @@
fill-color="#f9cf3d"
:size="18"
class="app-content-list-item-star favorite-icon-style"
:class="{ 'one-line': oneLineLayout, 'favorite-icon-style': !oneLineLayout }"
:data-starred="data.flags.flagged ? 'true' : 'false'"
@click.prevent="hasWriteAcl ? onToggleFlagged() : false" />
<div v-if="isImportant"
class="app-content-list-item-star svg icon-important"
:class="{ 'important-one-line': oneLineLayout, 'icon-important': !oneLineLayout }"
:data-starred="isImportant ? 'true' : 'false'"
@click.prevent="hasWriteAcl ? onToggleImportant() : false"
v-html="importantSvg" />
<JunkIcon v-if="data.flags.$junk"
:size="18"
class="app-content-list-item-star junk-icon-style"
:class="{ 'one-line': oneLineLayout, 'junk-icon-style': !oneLineLayout }"
:data-starred="data.flags.$junk ? 'true' : 'false'"
@click.prevent="hasWriteAcl ? onToggleJunk() : false" />
<div class="app-content-list-item-icon">
@ -48,23 +52,26 @@
</div>
</template>
<template #subname>
<div class="envelope__subtitle">
<Reply v-if="data.flags.answered"
class="seen-icon-style"
:size="18" />
<IconAttachment v-if="data.flags.hasAttachments === true"
class="attachment-icon-style"
:size="18" />
<span v-else-if="draft" class="draft">
<em>{{ t('mail', 'Draft: ') }}</em>
</span>
<span class="envelope__subtitle__subject">
{{ subjectForSubtitle }}
</span>
</div>
<div v-if="data.encrypted || data.previewText"
class="envelope__preview-text">
{{ isEncrypted ? t('mail', 'Encrypted message') : data.previewText.trim() }}
<div class="line-two"
:class="{ 'one-line': oneLineLayout }">
<div class="envelope__subtitle">
<Reply v-if="data.flags.answered"
class="seen-icon-style"
:size="18" />
<IconAttachment v-if="data.flags.hasAttachments === true"
class="attachment-icon-style"
:size="18" />
<span v-else-if="draft" class="draft">
<em>{{ t('mail', 'Draft: ') }}</em>
</span>
<span class="envelope__subtitle__subject">
{{ subjectForSubtitle }}
</span>
</div>
<div v-if="data.encrypted || data.previewText"
class="envelope__preview-text">
{{ isEncrypted ? t('mail', 'Encrypted message') : data.previewText.trim() }}
</div>
</div>
</template>
<template #indicator>
@ -126,7 +133,7 @@
messageLongDate
}}
</ActionText>
<ActionSeparator />
<NcActionSeparator />
<ActionButton v-if="hasWriteAcl"
:close-after-click="true"
@click.prevent="onToggleJunk">
@ -320,7 +327,8 @@ import {
NcListItem as ListItem,
NcActionButton as ActionButton,
NcActionLink as ActionLink,
NcActionSeparator as ActionSeparator,
NcActionSeparator,
NcActionInput,
NcActionText as ActionText,
} from '@nextcloud/vue'
import AlertOctagonIcon from 'vue-material-design-icons/AlertOctagon.vue'
@ -364,8 +372,6 @@ import { generateUrl } from '@nextcloud/router'
import { isPgpText } from '../crypto/pgp.js'
import { mailboxHasRights } from '../util/acl.js'
import DownloadIcon from 'vue-material-design-icons/Download.vue'
import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js'
import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js'
import CalendarClock from 'vue-material-design-icons/CalendarClock.vue'
import AlarmIcon from 'vue-material-design-icons/Alarm.vue'
import moment from '@nextcloud/moment'
@ -403,7 +409,6 @@ export default {
IconBullet,
Reply,
ActionLink,
ActionSeparator,
ActionText,
DownloadIcon,
ClockOutlineIcon,
@ -467,6 +472,9 @@ export default {
messageLongDate() {
return messageDateTime(new Date(this.data.dateInt))
},
oneLineLayout() {
return this.$store.getters.getPreference('layout-mode', 'vertical-split') === 'no-split'
},
hasMultipleRecipients() {
if (!this.account) {
console.error('account is undefined', {
@ -844,7 +852,6 @@ export default {
},
}
</script>
<style lang="scss" scoped>
.mail-message-account-color {
position: absolute;
@ -860,20 +867,24 @@ export default {
}
&__subtitle {
display: flex;
gap: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&__subject {
line-height: 130%;
overflow: hidden;
text-overflow: ellipsis;
&::after {
content: '\00B7';
margin: 12px;
}
}
}
&__preview-text {
color: var(--color-text-maxcontrast);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: initial;
flex: 1 1;
}
}
@ -903,6 +914,9 @@ export default {
}
}
}
.important-one-line.app-content-list-item-star:deep() {
top: 6px !important;
}
.app-content-list-item-select-checkbox {
display: inline-block;
@ -974,7 +988,7 @@ export default {
border: 1px solid transparent;
border-radius: var(--border-radius-pill);
position: relative;
margin: 0 1px;
margin: 9px 1px 0;
overflow: hidden;
left: 4px;
}
@ -986,7 +1000,7 @@ export default {
}
.icon-important.app-content-list-item-star:deep() {
position: absolute;
top: 14px;
top: 12px;
z-index: 1;
}
.app-content-list-item-star.favorite-icon-style {
@ -1008,9 +1022,11 @@ export default {
}
.seen-icon-style {
opacity: .6;
display: inline;
}
.attachment-icon-style {
opacity: .6;
display: inline;
}
:deep(.list-item__anchor) {
margin-top: 6px;
@ -1019,9 +1035,21 @@ export default {
:deep(.list-item) {
flex-wrap: wrap;
}
:deep(.list-item__extra) {
margin-top: 9px;
:deep(.line-two__subtitle) {
display: flex;
flex-basis: 100%;
padding-left: 40px;
width: 450px;
}
:deep(.line-one__title) {
flex-direction: row;
display: flex;
width: 200px;
}
.line-two.one-line {
display: flex;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

Просмотреть файл

@ -320,6 +320,7 @@ export default {
showMoveModal: false,
showTagModal: false,
lastToggledIndex: undefined,
defaultView: false,
}
},
computed: {

Просмотреть файл

@ -2,6 +2,7 @@
- @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
-
- @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
- @author Richard Steinmetz <richard@steinmetz.cloud>
-
- @license AGPL-3.0-or-later
-
@ -111,7 +112,7 @@ export default {
error: false,
refreshing: false,
loadingMore: false,
loadingEnvelopes: true,
loadingEnvelopes: false,
loadingCacheInitialization: false,
loadMailboxInterval: undefined,
expanded: false,
@ -161,11 +162,14 @@ export default {
this.loadMailboxInterval = setInterval(this.loadMailbox, 60000)
},
async mounted() {
this.loadEnvelopes()
.then(() => {
logger.debug(`syncing mailbox ${this.mailbox.databaseId} (${this.searchQuery}) after mount`)
this.sync(false)
})
if (this.$store.getters.hasFetchedInitialEnvelopes) {
return
}
await this.loadEnvelopes()
logger.debug(`syncing mailbox ${this.mailbox.databaseId} (${this.searchQuery}) after mount`)
await this.sync(false)
this.$store.commit('setHasFetchedInitialEnvelopes', true)
},
destroyed() {
this.bus.off('load-more', this.onScroll)

Просмотреть файл

@ -1,7 +1,10 @@
<template>
<AppContent pane-config-key="mail" :show-details="isThreadShown" @update:showDetails="hideMessage">
<AppContent pane-config-key="mail"
:layout="layoutMode"
:show-details="isThreadShown"
@update:showDetails="hideMessage">
<template #list>
<div :class="{ header__button: !showThread || !isMobile }">
<div :class="{ list__wrapper: !showThread || !isMobile }">
<SearchMessages v-if="!showThread || !isMobile"
:mailbox="mailbox"
:account-id="account.accountId"
@ -63,8 +66,9 @@
</AppContentList>
</div>
</template>
<Thread v-if="showThread" @delete="deleteMessage" />
<NoMessageSelected v-else-if="hasEnvelopes && !isMobile" />
<NoMessageSelected v-else-if="hasEnvelopes" />
</AppContent>
</template>
@ -138,9 +142,13 @@ export default {
priorityImportantQuery,
priorityOtherQuery,
startMailboxTimer: undefined,
hasContent: false,
}
},
computed: {
layoutMode() {
return this.$store.getters.getPreference('layout-mode', 'vertical-split')
},
unifiedAccount() {
return this.$store.getters.getAccount(UNIFIED_ACCOUNT_ID)
},
@ -331,11 +339,6 @@ export default {
background-color: var(--color-background-dark);
}
}
:deep(.button-vue--vue-secondary) {
position: sticky;
top:40px;
left: 10px;
}
:deep(.app-content-wrapper) {
overflow: auto;
}
@ -351,10 +354,17 @@ export default {
margin-bottom: 20px;
}
}
.header__button {
.list__wrapper {
display: flex;
flex: 1 0 0;
flex-direction: column;
height: calc(100vh - var(--header-height));
height: 100%;
}
:deep(.app-details-toggle) {
opacity: 1;
}
// temporary fix
:deep(.app-content-list) {
min-height: 0;
}
</style>

Просмотреть файл

@ -109,6 +109,10 @@ store.commit('savePreference', {
key: 'password-is-unavailable',
value: loadState('mail', 'password-is-unavailable', false),
})
store.commit('savePreference', {
key: 'layout-mode',
value: getPreferenceFromPage('layout-mode'),
})
const accountSettings = loadState('mail', 'account-settings')
const accounts = loadState('mail', 'accounts', [])

Просмотреть файл

@ -1499,4 +1499,13 @@ export default {
},
})
},
async setLayout({ commit }, { list }) {
try {
commit('setOneLineLayout', {
list,
})
} catch (error) {
logger.error('Could not set layouts', { error })
}
},
}

Просмотреть файл

@ -155,4 +155,6 @@ export const getters = {
getInbox: (state, getters) => (accountId) => {
return getters.findMailboxBySpecialRole(accountId, 'inbox')
},
isOneLineLayout: (state) => state.list,
hasFetchedInitialEnvelopes: (state) => state.hasFetchedInitialEnvelopes,
}

Просмотреть файл

@ -107,6 +107,7 @@ export default new Store({
sieveScript: {},
calendars: [],
smimeCertificates: [],
hasFetchedInitialEnvelopes: false,
},
getters,
mutations,

Просмотреть файл

@ -506,4 +506,10 @@ export default {
addSmimeCertificate(state, { certificate }) {
state.smimeCertificates = [...state.smimeCertificates, certificate]
},
setOneLineLayout(state, { list }) {
Vue.set(state, 'list', list)
},
setHasFetchedInitialEnvelopes(state, hasFetchedInitialEnvelopes) {
state.hasFetchedInitialEnvelopes = hasFetchedInitialEnvelopes
},
}

Просмотреть файл

@ -52,6 +52,7 @@ describe('Envelope', () => {
getAccount: () => (id) => ({}),
getEnvelopeTags: () => (id) => ([]),
getMailbox: () => (id) => ({}),
getPreference: () => (key, defaultValue) => defaultValue,
}
store = new Vuex.Store({
actions,

Просмотреть файл

@ -175,8 +175,4 @@ export default {
flex: 1 1 100%;
min-width: 70%;
}
// Align the appNavigation toggle with the apps header toolbar
:deep(button.app-navigation-toggle) {
top: 8px;
}
</style>

Просмотреть файл

@ -33,4 +33,5 @@ script('mail', 'mail');
<input type="hidden" id="start-mailbox-id" value="<?php p($_['start-mailbox-id']); ?>">
<input type="hidden" id="tag-classified-messages" value="<?php p($_['tag-classified-messages']); ?>">
<input type="hidden" id="search-priority-body" value="<?php p($_['search-priority-body']); ?>">
<input type="hidden" id="layout-mode" value="<?php p($_['layout-mode']); ?>">

Просмотреть файл

@ -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(8))
$this->preferences->expects($this->exactly(9))
->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, 'tag-classified-messages', 'true', 'true'],
[$this->userId, 'layout-mode', 'vertical-split', 'vertical-split'],
]);
$this->accountService->expects($this->once())
->method('findByUserId')
@ -323,6 +324,7 @@ class PageControllerTest extends TestCase {
'start-mailbox-id' => '123',
'tag-classified-messages' => 'true',
'search-priority-body' => 'false',
'layout-mode' => 'vertical-split',
]);
$csp = new ContentSecurityPolicy();
$csp->addAllowedFrameDomain('\'self\'');