Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/components/Navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,28 @@
variant="tertiary">{{ t('mail', 'Change password') }}</NcButton>
</span>
</div>
<template v-else-if="!isDisabled(group.account)">
<template v-else-if="!isDisabled(group.account) && !group.account.folded">
<template v-for="item in group.mailboxes" :key="item.databaseId">
<NavigationMailbox
v-show="
!group.isCollapsible
|| !group.account.collapsed
|| !isCollapsed(group.account, item)
"
class="navigation-account-mailboxes__item"
:account="group.account"
:mailbox="item" />
<NavigationMailbox
v-if="!group.account.isUnified && item.specialRole === 'inbox'"
class="navigation-account-mailboxes__item"
:account="group.account"
:mailbox="item"
filter="starred" />
</template>
<NavigationAccountExpandCollapse
v-if="!group.account.isUnified && group.isCollapsible"
:key="'collapse-' + group.account.id"
class="navigation-account-mailboxes__item"
:account="group.account" />
</template>
</template>
Expand Down Expand Up @@ -269,6 +272,16 @@ export default {
}
}

.navigation-account-mailboxes {
&__item {
// The item's root has an explicit width: 100%, so the inset has to come
// off that width too — a margin alone would push the right edge outside
// the list and misalign it with the account header's trailing icons.
width: calc(100% - calc(var(--default-clickable-area) / 2)) !important;
margin-inline-start: calc(var(--default-clickable-area) / 2);
}
}

.mail-settings {
padding: calc(var(--default-grid-baseline, 4px) * 2);
padding-top: 0;
Expand Down
187 changes: 178 additions & 9 deletions src/components/NavigationAccount.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,35 @@
-->

<template>
<NcAppNavigationCaption
<li
v-if="visible"
:id="id"
:key="id"
:name="account.emailAddress">
<!-- Actions -->
<template #actions>
class="navigation-account-header"
:class="{
'navigation-account-header--folded': account.folded,
'navigation-account-header--active': account.folded && isInboxActive,
}">
<h2 :id="id" class="navigation-account-header__name">
<router-link
v-if="inboxRoute"
class="navigation-account-header__link"
:to="inboxRoute"
:title="account.emailAddress">
{{ account.emailAddress }}
</router-link>
<span v-else class="navigation-account-header__link">{{ account.emailAddress }}</span>
</h2>
<template v-if="account.folded && inboxUnread > 0">
<NcCounterBubble
class="navigation-account-header__unread"
:count="inboxUnread"
:title="unreadLabel"
aria-hidden="true" />
<span class="hidden-visually">{{ unreadLabel }}</span>
</template>
<NcActions
class="navigation-account-header__actions"
:aria-label="t('mail', 'Actions for {email}', { email: account.emailAddress })">
<template v-if="isDisabled">
<NcActionText :name="t('mail', 'Provisioned account is disabled')">
<template #icon>
Expand Down Expand Up @@ -92,8 +114,21 @@
{{ t('mail', 'Remove account') }}
</NcActionButton>
</template>
</template>
</NcAppNavigationCaption>
</NcActions>
<NcButton
v-if="!isDisabled"
class="navigation-account-header__toggle"
variant="tertiary"
:aria-label="foldLabel"
:title="foldLabel"
:aria-expanded="account.folded ? 'false' : 'true'"
:disabled="savingFolded"
@click="toggleFolded">
<template #icon>
<IconChevronRight class="navigation-account-header__chevron" :size="20" />
</template>
</NcButton>
</li>
<DelegationModal v-if="showDelegationModal" :account="account" @close="showDelegationModal = false" />
</template>

Expand All @@ -106,11 +141,14 @@ import { defineAsyncComponent } from 'vue'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcActionCheckbox from '@nextcloud/vue/components/NcActionCheckbox'
import NcActionInput from '@nextcloud/vue/components/NcActionInput'
import NcActions from '@nextcloud/vue/components/NcActions'
import NcActionText from '@nextcloud/vue/components/NcActionText'
import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCaption'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcCounterBubble from '@nextcloud/vue/components/NcCounterBubble'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import MenuDown from 'vue-material-design-icons/ChevronDown.vue'
import IconChevronRight from 'vue-material-design-icons/ChevronRight.vue'
import MenuUp from 'vue-material-design-icons/ChevronUp.vue'
import IconSettings from 'vue-material-design-icons/CogOutline.vue'
import IconFolderAdd from 'vue-material-design-icons/FolderOutline.vue'
Expand All @@ -124,7 +162,9 @@ import IconDelegation from './../../img/delegation.svg'
export default {
name: 'NavigationAccount',
components: {
NcAppNavigationCaption,
NcActions,
NcButton,
NcCounterBubble,
NcActionButton,
NcActionCheckbox,
NcActionInput,
Expand All @@ -134,6 +174,7 @@ export default {
IconSettings,
NcIconSvgWrapper,
IconFolderAdd,
IconChevronRight,
MenuDown,
MenuUp,
IconDelete,
Expand Down Expand Up @@ -175,6 +216,7 @@ export default {
},

savingShowOnlySubscribed: false,
savingFolded: false,
quota: undefined,
editing: false,
showSaving: false,
Expand All @@ -201,6 +243,43 @@ export default {
return 'account-' + this.account.id
},

foldLabel() {
return this.account.folded
? t('mail', 'Expand account {email}', { email: this.account.emailAddress })
: t('mail', 'Collapse account {email}', { email: this.account.emailAddress })
},

inbox() {
return this.mainStore.getMailboxes(this.account.id)
.find((mailbox) => mailbox.specialRole === 'inbox')
},

inboxRoute() {
if (this.isDisabled || !this.inbox) {
return null
}
return {
name: 'mailbox',
params: {
mailboxId: this.inbox.databaseId,
},
}
},

isInboxActive() {
return this.inbox !== undefined
&& !this.$route?.params.filter
&& String(this.$route?.params.mailboxId) === String(this.inbox.databaseId)
},

inboxUnread() {
return this.inbox?.unread ?? 0
},

unreadLabel() {
return n('mail', '%n unread message in the inbox', '%n unread messages in the inbox', this.inboxUnread)
},

quotaText() {
if (this.quota) {
return t('mail', 'Used quota: {quota}% ({limit})', {
Expand Down Expand Up @@ -282,6 +361,18 @@ export default {
await dialog.show()
},

async toggleFolded() {
this.savingFolded = true
try {
await this.mainStore.toggleAccountFolded(this.account.id)
} catch (error) {
logger.error('could not save folded state of account', { error })
showError(t('mail', 'Could not save whether the account is collapsed'))
} finally {
this.savingFolded = false
}
},

changeAccountOrderUp() {
this.mainStore.moveAccount({ account: this.account, up: true })
.catch((error) => logger.error('could not move account up', { error }))
Expand Down Expand Up @@ -332,6 +423,84 @@ export default {
}
</script>

<style lang="scss" scoped>
.navigation-account-header {
display: flex;
align-items: center;
gap: calc(var(--default-grid-baseline) / 2);

&:not(:first-child) {
margin-top: calc(var(--default-clickable-area) / 2);
}

& + & {
margin-top: 0;
}

&__toggle {
flex-shrink: 0;
}

&__chevron {
transform: rotate(90deg);
transition: transform var(--animation-quick);
}

&--folded &__chevron {
transform: rotate(0deg);
}

&__name {
flex: 1 1 auto;
min-width: 0;
margin: 0;
font-size: var(--default-font-size);
font-weight: var(--font-weight-heading, bold);
line-height: var(--default-clickable-area);
}

&__link {
display: block;
overflow: hidden;
padding-inline: calc(var(--default-grid-baseline) * 2);
border-radius: var(--border-radius-element);
color: var(--color-main-text);
text-overflow: ellipsis;
white-space: nowrap;
}

a#{&}__link:hover,
a#{&}__link:focus-visible {
background-color: var(--color-background-hover);
}

// Same highlight as an active NcAppNavigationItem
&--active {
position: relative;
border-radius: var(--border-radius-element);
background-color: color-mix(in srgb, var(--color-primary-element) 16%, transparent);

&::before {
content: '';
position: absolute;
inset-block: calc(var(--default-grid-baseline) * 2);
inset-inline-start: 0;
width: calc(var(--default-grid-baseline) * 3 / 4);
border-radius: var(--border-radius-pill);
background-color: var(--color-primary-element);
}
}

&__unread {
flex-shrink: 0;
}

&__actions {
flex: 0 0 var(--default-clickable-area);
}
}
</style>

<style lang="scss">
// Unscoped because DialogBuilder mounts outside this component; wraps the long "Remove {email}" label
.nc-generic-dialog .dialog__actions {
Expand Down
51 changes: 50 additions & 1 deletion src/store/mainStore/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ function transformMailboxName(account, mailbox) {
}

export default function mainStoreActions() {
let accountSettingsSave = Promise.resolve()
const foldedVersions = {}

return {
updateSyncTimestamp() {
this.syncTimestamp = Date.now()
Expand Down Expand Up @@ -313,7 +316,13 @@ export default function mainStoreActions() {
key,
value,
})
return await savePreference('account-settings', JSON.stringify(this.allAccountSettings))
// Every save sends the whole settings list, so queue them to keep an
// older snapshot from overwriting a newer one
const save = accountSettingsSave
.catch(() => {})
.then(() => savePreference('account-settings', JSON.stringify(this.allAccountSettings)))
accountSettingsSave = save
return await save
})
},
async deleteAccount(account) {
Expand Down Expand Up @@ -359,9 +368,45 @@ export default function mainStoreActions() {
key: 'collapsed',
value: false,
})
if (account.folded) {
foldedVersions[account.id] = (foldedVersions[account.id] ?? 0) + 1
this.toggleAccountFoldedMutation(account.id)
try {
await this.setAccountSetting({
accountId: account.id,
key: 'folded',
value: false,
})
} catch (error) {
logger.error(`could not save unfolded state of account ${account.id}`, { error })
}
}
return mailbox
})
},
async toggleAccountFolded(accountId) {
const previous = this.accountsUnmapped[accountId].folded
const version = (foldedVersions[accountId] ?? 0) + 1
foldedVersions[accountId] = version
this.toggleAccountFoldedMutation(accountId)
try {
await this.setAccountSetting({
accountId,
key: 'folded',
value: !previous,
})
} catch (error) {
if (foldedVersions[accountId] === version) {
this.toggleAccountFoldedMutation(accountId)
this.setAccountSettingMutation({
accountId,
key: 'folded',
value: previous,
})
}
throw error
}
},
async moveAccount({
account,
up,
Expand Down Expand Up @@ -1906,6 +1951,7 @@ export default function mainStoreActions() {
},
addAccountMutation(account) {
account.collapsed = account.collapsed ?? true
account.folded = account.folded ?? false

this.accountsUnmapped[account.id] = account

Expand Down Expand Up @@ -1942,6 +1988,9 @@ export default function mainStoreActions() {
toggleAccountCollapsedMutation(accountId) {
this.accountsUnmapped[accountId].collapsed = !this.accountsUnmapped[accountId].collapsed
},
toggleAccountFoldedMutation(accountId) {
this.accountsUnmapped[accountId].folded = !this.accountsUnmapped[accountId].folded
},
expandAccountMutation(accountId) {
this.accountsUnmapped[accountId].collapsed = false
},
Expand Down
Loading
Loading