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
23 changes: 23 additions & 0 deletions packages/web-app-mail/src/components/MailActionBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template>
<div v-if="hasActions" class="hidden md:flex items-center h-14 -mx-4 px-4 border-b">
<oc-button appearance="raw" :aria-label="$gettext('Edit draft')" @click="$emit('edit-draft')">
<oc-icon name="pencil" fill-type="line" size="medium" />
</oc-button>
</div>
</template>

<script setup lang="ts">
import { computed, unref } from 'vue'
import type { Mail } from '../types'

const props = defineProps<{
mail: Mail
}>()

defineEmits<{
(e: 'edit-draft'): void
}>()

const isDraft = computed(() => Boolean(props.mail?.keywords?.$draft))
const hasActions = computed(() => unref(isDraft))
</script>
12 changes: 12 additions & 0 deletions packages/web-app-mail/src/components/MailDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
>
<oc-icon name="arrow-left" fill-type="line" />
</oc-button>
<MailActionBar :mail="currentMail" @edit-draft="onEditDraft" />
<div class="mail-details-subject font-bold flex justify-between items-center mt-1">
<h3 class="text-lg block truncate" v-text="currentMail.subject" />
<MailIndicators :mail="currentMail" />
Expand Down Expand Up @@ -59,6 +60,8 @@ import MailAttachmentList from './MailAttachmentList.vue'
import MailIndicators from './MailIndicators.vue'
import { AppLoadingSpinner, useGroupwareAccountsStore } from '@opencloud-eu/web-pkg'
import MailAppointmentList from './MailAppointmentList.vue'
import MailActionBar from './MailActionBar.vue'
import { useMailCompose } from '../composables/useMailCompose'
import { useLoadMail } from '../composables/useLoadMail'
import { useMailsStore } from '../composables/piniaStores/mails'
import { storeToRefs } from 'pinia'
Expand All @@ -70,6 +73,7 @@ const mailsStore = useMailsStore()
const { currentAccount } = storeToRefs(accountsStore)
const { currentMail } = storeToRefs(mailsStore)
const { setCurrentMail } = mailsStore
const { openDraftCompose } = useMailCompose()

const fromEmail = computed(() => {
return unref(currentMail)?.from[0]?.email || unref(currentMail)?.sender[0]?.email
Expand Down Expand Up @@ -98,6 +102,14 @@ const appointments = computed(() => {
return unref(currentMail).attachments?.filter((attachment) => attachment.type === 'text/calendar')
})

const onEditDraft = () => {
if (!unref(currentMail)) {
return
}

openDraftCompose(unref(currentMail))
}

const onNavigateBack = () => {
setCurrentMail(null)
}
Expand Down
8 changes: 8 additions & 0 deletions packages/web-app-mail/src/components/MailList.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<template>
<app-loading-spinner v-if="isLoading" />
<template v-else>
<oc-floating-action-button
class="md:hidden"
mode="action"
:aria-label="$gettext('Write new Email')"
@click="openNewCompose"
/>
<no-content-message v-if="!currentMailbox" icon="folder" icon-fill-type="line">
<template #message>
<span v-text="$gettext('No mailbox selected')" />
Expand Down Expand Up @@ -69,12 +75,14 @@ import { useMailboxesStore } from '../composables/piniaStores/mailboxes'
import { storeToRefs } from 'pinia'
import { useLoadMail } from '../composables/useLoadMail'
import { unref } from 'vue'
import { useMailCompose } from '../composables/useMailCompose'

const mailsStore = useMailsStore()
const mailboxesStore = useMailboxesStore()
const accountsStore = useGroupwareAccountsStore()
const { loadMail } = useLoadMail()
const { isLoading } = useLoadMails()
const { openNewCompose } = useMailCompose()

const { currentAccount } = storeToRefs(accountsStore)
const { currentMail, mails } = storeToRefs(mailsStore)
Expand Down
84 changes: 81 additions & 3 deletions packages/web-app-mail/src/components/MailWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
<oc-icon name="text" fill-type="none" />
</oc-button>
<div class="ml-auto flex items-center min-w-0">
<MailSavedHint :show="showSavedHint" />
<MailSavedHint v-if="showSavedHint" />
</div>
</div>
</div>
Expand All @@ -127,7 +127,7 @@
</template>

<script setup lang="ts">
import { ref, computed, unref, onUnmounted } from 'vue'
import { ref, computed, unref, watch, onUnmounted } from 'vue'
import { useGettext } from 'vue3-gettext'
import { storeToRefs } from 'pinia'
import { useGroupwareAccountsStore, useModals } from '@opencloud-eu/web-pkg'
Expand All @@ -142,16 +142,21 @@ import { useAutoSaveDraft } from '../composables/useAutoSaveDraft'
import { useComposeDirtyTracking } from '../composables/useComposeDirtyTracking'
import { plainTextFromHtml } from '../helpers/mailComposeText'
import isEmpty from 'lodash-es/isEmpty'
import type { Mailbox, MailAddress } from '../types'
import type { Mail, Mailbox, MailAddress } from '../types'

type ComposeAttachment = ComposeFormState['attachments'][number]

const { $gettext } = useGettext()
const { dispatchModal } = useModals()
const appliedDraftId = ref<string | null>(null)

const SAVED_HINT_DURATION_MS = 2000
const AUTO_SAVE_INTERVAL_MS = 120000 // 2(min) * 60 * 1000

const props = defineProps<{
draftMail?: Mail | null
}>()

const emit = defineEmits<{
(e: 'close'): void
}>()
Expand Down Expand Up @@ -190,6 +195,49 @@ const createEmptyComposeState = (): ComposeFormState => ({

const composeState = ref<ComposeFormState>(createEmptyComposeState())

const recipientsToInput = (recipients: MailAddress[] = []) => {
return recipients.map((recipient) => recipient.email).join(', ')
}

const getDraftBody = (mail: Mail) => {
const htmlPartId = mail.htmlBody?.[0]?.partId
if (htmlPartId) {
return mail.bodyValues?.[htmlPartId]?.value ?? ''
}

const textPartId = mail.textBody?.[0]?.partId
if (textPartId) {
return mail.bodyValues?.[textPartId]?.value ?? ''
}

return ''
}

const getDraftAttachments = (mail: Mail) => {
return (mail.attachments ?? [])
.filter((attachment) => attachment.blobId)
.map((attachment) => ({
id: attachment.blobId,
blobId: attachment.blobId,
name: attachment.name,
type: attachment.type,
disposition: 'attachment' as const,
size: attachment.size ?? 0
}))
}

const createComposeStateFromDraft = (mail: Mail): ComposeFormState => {
return {
from: undefined,
to: recipientsToInput(mail.to),
cc: recipientsToInput(mail.cc),
bcc: recipientsToInput(mail.bcc),
subject: mail.subject ?? '',
body: getDraftBody(mail),
attachments: getDraftAttachments(mail)
}
}

const toggleCollapseExpand = () => {
isExpanded.value = !isExpanded.value
}
Expand Down Expand Up @@ -266,6 +314,15 @@ const resetCompose = async () => {
})
}

const applyDraftMail = async (mail: Mail) => {
await runWithResetGuard(() => {
composeState.value = createComposeStateFromDraft(mail)
clearSavedHint()
resetDraft(mail.id)
appliedDraftId.value = mail.id
})
}

const doClose = () => {
isExpanded.value = false
emit('close')
Expand Down Expand Up @@ -326,6 +383,27 @@ useAutoSaveDraft({
onUnmounted(() => {
resetCompose()
})

watch(
() => props.draftMail?.id,
async (draftId) => {
if (!draftId || !props.draftMail) {
return
}

if (draftId === appliedDraftId.value) {
return
}

if (unref(isDirty) || unref(hasMeaningfulChanges)) {
return
}

await applyDraftMail(props.draftMail)
appliedDraftId.value = props.draftMail.id
},
{ immediate: true }
)
</script>

<style>
Expand Down
30 changes: 30 additions & 0 deletions packages/web-app-mail/src/composables/useMailCompose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ref } from 'vue'
import { Mail } from '../types'

const isOpen = ref(false)
const draftMail = ref<Mail | null>(null)

export const useMailCompose = () => {
const openNewCompose = () => {
draftMail.value = null
isOpen.value = true
}

const openDraftCompose = (mail: Mail) => {
draftMail.value = mail
isOpen.value = true
}

const closeCompose = () => {
isOpen.value = false
draftMail.value = null
}

return {
isOpen,
draftMail,
openNewCompose,
openDraftCompose,
closeCompose
}
}
6 changes: 3 additions & 3 deletions packages/web-app-mail/src/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ import {
ApplicationInformation,
useCapabilityStore,
useUserStore,
useEventBus,
Extension
} from '@opencloud-eu/web-pkg'
import { $gettext } from '@opencloud-eu/web-pkg/src/router/utils'
import { computed, unref } from 'vue'
import MailboxTree from './components/MailboxTree.vue'
import { storeToRefs } from 'pinia'
import { useMailCompose } from './composables/useMailCompose'

export const extensions = (appInfo: ApplicationInformation) => {
const capabilityStore = useCapabilityStore()
const userStore = useUserStore()
const eventBus = useEventBus()
const { user } = storeToRefs(userStore)
const { openNewCompose } = useMailCompose()

const menuItemExtension: AppMenuItemExtension = {
id: `app.${appInfo.id}.menuItem`,
Expand All @@ -39,7 +39,7 @@ export const extensions = (appInfo: ApplicationInformation) => {
label: () => $gettext('New'),
mode: () => 'handler',
handler: () => {
eventBus.publish('app.mail.show-compose-mail')
openNewCompose()
}
}

Expand Down
33 changes: 7 additions & 26 deletions packages/web-app-mail/src/views/Inbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
'hidden md:block': currentMail || !currentMailbox
}"
>
<MailList ref="mailListRef" />
<MailList />
</div>
<div
class="overflow-y-auto min-w-0 w-full md:w-3/4 px-4 pt-4 md:pt-0"
Expand All @@ -19,21 +19,16 @@
<MailDetails :key="currentMail?.id" />
</div>
</div>
<MailWidget v-if="showCompose" @close="onCloseCompose" />
<MailWidget v-if="showCompose" :draft-mail="draftMail" @close="closeCompose" />
</template>
</template>

<script setup lang="ts">
import { ref, unref, onMounted, onUnmounted } from 'vue'
import { ref, unref, onMounted } from 'vue'
import MailList from '../components/MailList.vue'
import MailDetails from '../components/MailDetails.vue'
import MailWidget from '../components/MailWidget.vue'
import {
AppLoadingSpinner,
queryItemAsString,
useClientService,
useEventBus
} from '@opencloud-eu/web-pkg'
import { AppLoadingSpinner, queryItemAsString, useClientService } from '@opencloud-eu/web-pkg'
import { useRouteQuery } from '@opencloud-eu/web-pkg'
import { useMailsStore } from '../composables/piniaStores/mails'
import { useGroupwareAccountsStore } from '@opencloud-eu/web-pkg'
Expand All @@ -42,12 +37,12 @@ import { useMailboxesStore } from '../composables/piniaStores/mailboxes'
import { useLoadMailboxes } from '../composables/useLoadMailboxes'
import { useLoadMails } from '../composables/useLoadMails'
import { useLoadMail } from '../composables/useLoadMail'
import { useMailCompose } from '../composables/useMailCompose'
import { Mailbox } from '../types'

const accountsStore = useGroupwareAccountsStore()
const mailboxesStore = useMailboxesStore()
const mailsStore = useMailsStore()
const eventBus = useEventBus()
const { httpAuthenticated } = useClientService()

const { currentAccount } = storeToRefs(accountsStore)
Expand All @@ -61,23 +56,14 @@ const { loadMails } = useLoadMails()
const { loadMail } = useLoadMail()
const { setCurrentMail } = mailsStore

const { isOpen: showCompose, draftMail, closeCompose } = useMailCompose()

const isLoading = ref<boolean>(true)

const currentAccountIdQuery = useRouteQuery('accountId')
const currentMailboxIdQuery = useRouteQuery('mailboxId')
const currentMailIdQuery = useRouteQuery('mailId')

let mailComposeEventToken: string
const showCompose = ref(false)

const onComposeMail = () => {
showCompose.value = true
}

const onCloseCompose = () => {
showCompose.value = false
}

const loadMailboxesAndMails = async () => {
isLoading.value = true
setCurrentMail(null)
Expand Down Expand Up @@ -110,14 +96,9 @@ accountsStore.$onAction(({ after, name }) => {
})

onMounted(() => {
mailComposeEventToken = eventBus.subscribe('app.mail.show-compose-mail', onComposeMail)
loadCurrentAccount({
client: httpAuthenticated,
query: queryItemAsString(unref(currentAccountIdQuery))
})
})

onUnmounted(() => {
eventBus.unsubscribe('app.mail.show-compose-mail', mailComposeEventToken)
})
</script>