diff options
author | Christoph Wurst <ChristophWurst@users.noreply.github.com> | 2019-03-06 20:36:04 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-03-06 20:36:04 +0300 |
commit | 5ab96c39d64f803bb1a977b90a805a1895ba9618 (patch) | |
tree | 87324f3fa96241c98595fb04b375d88ef1a63f50 /src | |
parent | cf3e7e374a17a84a98c116e780dde234096f8a7f (diff) | |
parent | f1bafd430b0f0e2c7a52afc00bf6bd7ea732c478 (diff) |
Merge pull request #1587 from nextcloud/lint/components
Apply lint:autofix on all components
Diffstat (limited to 'src')
-rw-r--r-- | src/components/AccountForm.vue | 471 | ||||
-rw-r--r-- | src/components/Address.vue | 52 | ||||
-rw-r--r-- | src/components/AddressList.vue | 28 | ||||
-rw-r--r-- | src/components/AppSettingsMenu.vue | 78 | ||||
-rw-r--r-- | src/components/Avatar.vue | 73 | ||||
-rw-r--r-- | src/components/Composer.vue | 791 | ||||
-rw-r--r-- | src/components/ComposerAttachments.vue | 201 | ||||
-rw-r--r-- | src/components/EmptyFolder.vue | 6 | ||||
-rw-r--r-- | src/components/Envelope.vue | 128 | ||||
-rw-r--r-- | src/components/EnvelopeList.vue | 428 | ||||
-rw-r--r-- | src/components/Error.vue | 48 | ||||
-rw-r--r-- | src/components/FolderContent.vue | 228 | ||||
-rw-r--r-- | src/components/Loading.vue | 19 | ||||
-rw-r--r-- | src/components/Message.vue | 419 | ||||
-rw-r--r-- | src/components/MessageAttachment.vue | 412 | ||||
-rw-r--r-- | src/components/MessageAttachments.vue | 146 | ||||
-rw-r--r-- | src/components/MessageHTMLBody.vue | 113 | ||||
-rw-r--r-- | src/components/MessagePlainTextBody.vue | 36 | ||||
-rw-r--r-- | src/components/Moment.vue | 50 | ||||
-rw-r--r-- | src/components/Navigation.vue | 256 | ||||
-rw-r--r-- | src/components/NewMessageDetail.vue | 353 |
21 files changed, 2146 insertions, 2190 deletions
diff --git a/src/components/AccountForm.vue b/src/components/AccountForm.vue index f556c6b4d..745a35772 100644 --- a/src/components/AccountForm.vue +++ b/src/components/AccountForm.vue @@ -1,242 +1,246 @@ <template> - <div id="account-form"> - <tabs - :options="{ useUrlFragment: false, defaultTabHash: settingsPage ? 'manual' : 'auto' }" - @changed="onModeChanged" - cache-lifetime="0" - > - <tab :name="t('mail', 'Auto')" id="auto" key="auto"> - <label for="auto-name">{{ t('mail', 'Name') }}</label> - <input - type="text" - id="auto-name" - :placeholder="t('mail', 'Name')" - v-model="autoConfig.accountName" - :disabled="loading" - autofocus - > - <label for="auto-address">{{ t('mail', 'Mail Address') }}</label> - <input - type="email" - id="auto-address" - :placeholder="t('mail', 'Mail Address')" - v-model="autoConfig.emailAddress" - :disabled="loading" - required - > - <label for="auto-password">{{ t('mail', 'Password') }}</label> - <input - type="password" - id="auto-password" - :placeholder="t('mail', 'Password')" - v-model="autoConfig.password" - :disabled="loading" - required - > - </tab> - <tab :name="t('mail', 'Manual')" id="manual" key="manual"> - <label for="man-name">{{ t('mail', 'Name') }}</label> - <input - type="text" - id="man-name" - :placeholder="t('mail', 'Name')" - v-model="manualConfig.accountName" - :disabled="loading" - autofocus - > - <label for="man-address">{{ t('mail', 'Mail Address') }}</label> - <input - type="email" - id="man-address" - :placeholder="t('mail', 'Mail Address')" - v-model="manualConfig.emailAddress" - :disabled="loading" - required - > + <div id="account-form"> + <tabs + :options="{useUrlFragment: false, defaultTabHash: settingsPage ? 'manual' : 'auto'}" + cache-lifetime="0" + @changed="onModeChanged" + > + <tab id="auto" key="auto" :name="t('mail', 'Auto')"> + <label for="auto-name">{{ t('mail', 'Name') }}</label> + <input + id="auto-name" + v-model="autoConfig.accountName" + type="text" + :placeholder="t('mail', 'Name')" + :disabled="loading" + autofocus + /> + <label for="auto-address">{{ t('mail', 'Mail Address') }}</label> + <input + id="auto-address" + v-model="autoConfig.emailAddress" + type="email" + :placeholder="t('mail', 'Mail Address')" + :disabled="loading" + required + /> + <label for="auto-password">{{ t('mail', 'Password') }}</label> + <input + id="auto-password" + v-model="autoConfig.password" + type="password" + :placeholder="t('mail', 'Password')" + :disabled="loading" + required + /> + </tab> + <tab id="manual" key="manual" :name="t('mail', 'Manual')"> + <label for="man-name">{{ t('mail', 'Name') }}</label> + <input + id="man-name" + v-model="manualConfig.accountName" + type="text" + :placeholder="t('mail', 'Name')" + :disabled="loading" + autofocus + /> + <label for="man-address">{{ t('mail', 'Mail Address') }}</label> + <input + id="man-address" + v-model="manualConfig.emailAddress" + type="email" + :placeholder="t('mail', 'Mail Address')" + :disabled="loading" + required + /> - <h3>{{ t('mail', 'IMAP Settings') }}</h3> - <label for="man-imap-host">{{ t('mail', 'IMAP Host') }}</label> - <input - type="text" - id="man-imap-host" - :placeholder="t('mail', 'IMAP Host')" - v-model="manualConfig.imapHost" - :disabled="loading" - required - > - <h4>{{ t('mail', 'IMAP Security') }}</h4> - <div class="flex-row"> - <input - type="radio" - id="man-imap-sec-none" - name="man-imap-sec" - v-model="manualConfig.imapSslMode" - :disabled="loading" - @change="onImapSslModeChange" - value="none" - > - <label - class="button" - for="man-imap-sec-none" - :class="{primary: manualConfig.imapSslMode === 'none' }" - >{{ t('mail', 'None') }}</label> - <input - type="radio" - id="man-imap-sec-ssl" - name="man-imap-sec" - v-model="manualConfig.imapSslMode" - :disabled="loading" - @change="onImapSslModeChange" - value="ssl" - > - <label - class="button" - for="man-imap-sec-ssl" - :class="{primary: manualConfig.imapSslMode === 'ssl' }" - >{{ t('mail', 'SSL/TLS') }}</label> - <input - type="radio" - id="man-imap-sec-tls" - name="man-imap-sec" - v-model="manualConfig.imapSslMode" - :disabled="loading" - @change="onImapSslModeChange" - value="tls" - > - <label - class="button" - for="man-imap-sec-tls" - :class="{primary: manualConfig.imapSslMode === 'tls' }" - >{{ t('mail', 'STARTTLS') }}</label> - </div> - <label for="man-imap-port">{{ t('mail', 'IMAP Port') }}</label> - <input - type="number" - id="man-imap-port" - :placeholder="t('mail', 'IMAP Port')" - v-model="manualConfig.imapPort" - :disabled="loading" - required - > - <label for="man-imap-user">{{ t('mail', 'IMAP User') }}</label> - <input - type="text" - id="man-imap-user" - :placeholder="t('mail', 'IMAP User')" - v-model="manualConfig.imapUser" - :disabled="loading" - required - > - <label for="man-imap-password">{{ t('mail', 'IMAP Password') }}</label> - <input - type="password" - id="man-imap-password" - :placeholder="t('mail', 'IMAP Password')" - v-model="manualConfig.imapPassword" - :disabled="loading" - required - > + <h3>{{ t('mail', 'IMAP Settings') }}</h3> + <label for="man-imap-host">{{ t('mail', 'IMAP Host') }}</label> + <input + id="man-imap-host" + v-model="manualConfig.imapHost" + type="text" + :placeholder="t('mail', 'IMAP Host')" + :disabled="loading" + required + /> + <h4>{{ t('mail', 'IMAP Security') }}</h4> + <div class="flex-row"> + <input + id="man-imap-sec-none" + v-model="manualConfig.imapSslMode" + type="radio" + name="man-imap-sec" + :disabled="loading" + value="none" + @change="onImapSslModeChange" + /> + <label + class="button" + for="man-imap-sec-none" + :class="{primary: manualConfig.imapSslMode === 'none'}" + >{{ t('mail', 'None') }}</label + > + <input + id="man-imap-sec-ssl" + v-model="manualConfig.imapSslMode" + type="radio" + name="man-imap-sec" + :disabled="loading" + value="ssl" + @change="onImapSslModeChange" + /> + <label + class="button" + for="man-imap-sec-ssl" + :class="{primary: manualConfig.imapSslMode === 'ssl'}" + >{{ t('mail', 'SSL/TLS') }}</label + > + <input + id="man-imap-sec-tls" + v-model="manualConfig.imapSslMode" + type="radio" + name="man-imap-sec" + :disabled="loading" + value="tls" + @change="onImapSslModeChange" + /> + <label + class="button" + for="man-imap-sec-tls" + :class="{primary: manualConfig.imapSslMode === 'tls'}" + >{{ t('mail', 'STARTTLS') }}</label + > + </div> + <label for="man-imap-port">{{ t('mail', 'IMAP Port') }}</label> + <input + id="man-imap-port" + v-model="manualConfig.imapPort" + type="number" + :placeholder="t('mail', 'IMAP Port')" + :disabled="loading" + required + /> + <label for="man-imap-user">{{ t('mail', 'IMAP User') }}</label> + <input + id="man-imap-user" + v-model="manualConfig.imapUser" + type="text" + :placeholder="t('mail', 'IMAP User')" + :disabled="loading" + required + /> + <label for="man-imap-password">{{ t('mail', 'IMAP Password') }}</label> + <input + id="man-imap-password" + v-model="manualConfig.imapPassword" + type="password" + :placeholder="t('mail', 'IMAP Password')" + :disabled="loading" + required + /> - <h3>{{ t('mail', 'SMTP Settings') }}</h3> - <input - type="text" - ref="smtpHost" - name="smtp-host" - :placeholder="t('mail', 'SMTP Host')" - v-model="manualConfig.smtpHost" - :disabled="loading" - required - > - <h4>{{ t('mail', 'SMTP Security') }}</h4> - <div class="flex-row"> - <input - type="radio" - id="man-smtp-sec-none" - name="man-smtp-sec" - v-model="manualConfig.smtpSslMode" - :disabled="loading" - @change="onSmtpSslModeChange" - value="none" - > - <label - class="button" - for="man-smtp-sec-none" - :class="{primary: manualConfig.smtpSslMode === 'none' }" - >{{ t('mail', 'None') }}</label> - <input - type="radio" - id="man-smtp-sec-ssl" - name="man-smtp-sec" - v-model="manualConfig.smtpSslMode" - :disabled="loading" - @change="onSmtpSslModeChange" - value="ssl" - > - <label - class="button" - for="man-smtp-sec-ssl" - :class="{primary: manualConfig.smtpSslMode === 'ssl' }" - >{{ t('mail', 'SSL/TLS') }}</label> - <input - type="radio" - id="man-smtp-sec-tls" - name="man-smtp-sec" - v-model="manualConfig.smtpSslMode" - :disabled="loading" - @change="onSmtpSslModeChange" - value="tls" - > - <label - class="button" - for="man-smtp-sec-tls" - :class="{primary: manualConfig.smtpSslMode === 'tls' }" - >{{ t('mail', 'STARTTLS') }}</label> - </div> - <label for="man-smtp-port">{{ t('mail', 'SMTP Port') }}</label> - <input - type="number" - id="man-smtp-port" - :placeholder="t('mail', 'SMTP Port')" - v-model="manualConfig.smtpPort" - :disabled="loading" - required - > - <label for="man-smtp-user">{{ t('mail', 'SMTP User') }}</label> - <input - type="text" - id="man-smtp-user" - :placeholder="t('mail', 'SMTP User')" - v-model="manualConfig.smtpUser" - :disabled="loading" - required - > - <label for="man-smtp-password">{{ t('mail', 'SMTP Password') }}</label> - <input - type="password" - id="man-smtp-password" - :placeholder="t('mail', 'SMTP Password')" - v-model="manualConfig.smtpPassword" - :disabled="loading" - required - > - </tab> - </tabs> - <input - type="submit" - class="primary" - v-on:click="onSubmit" - :disabled="loading" - :value="submitButtonText" - > - </div> + <h3>{{ t('mail', 'SMTP Settings') }}</h3> + <input + ref="smtpHost" + v-model="manualConfig.smtpHost" + type="text" + name="smtp-host" + :placeholder="t('mail', 'SMTP Host')" + :disabled="loading" + required + /> + <h4>{{ t('mail', 'SMTP Security') }}</h4> + <div class="flex-row"> + <input + id="man-smtp-sec-none" + v-model="manualConfig.smtpSslMode" + type="radio" + name="man-smtp-sec" + :disabled="loading" + value="none" + @change="onSmtpSslModeChange" + /> + <label + class="button" + for="man-smtp-sec-none" + :class="{primary: manualConfig.smtpSslMode === 'none'}" + >{{ t('mail', 'None') }}</label + > + <input + id="man-smtp-sec-ssl" + v-model="manualConfig.smtpSslMode" + type="radio" + name="man-smtp-sec" + :disabled="loading" + value="ssl" + @change="onSmtpSslModeChange" + /> + <label + class="button" + for="man-smtp-sec-ssl" + :class="{primary: manualConfig.smtpSslMode === 'ssl'}" + >{{ t('mail', 'SSL/TLS') }}</label + > + <input + id="man-smtp-sec-tls" + v-model="manualConfig.smtpSslMode" + type="radio" + name="man-smtp-sec" + :disabled="loading" + value="tls" + @change="onSmtpSslModeChange" + /> + <label + class="button" + for="man-smtp-sec-tls" + :class="{primary: manualConfig.smtpSslMode === 'tls'}" + >{{ t('mail', 'STARTTLS') }}</label + > + </div> + <label for="man-smtp-port">{{ t('mail', 'SMTP Port') }}</label> + <input + id="man-smtp-port" + v-model="manualConfig.smtpPort" + type="number" + :placeholder="t('mail', 'SMTP Port')" + :disabled="loading" + required + /> + <label for="man-smtp-user">{{ t('mail', 'SMTP User') }}</label> + <input + id="man-smtp-user" + v-model="manualConfig.smtpUser" + type="text" + :placeholder="t('mail', 'SMTP User')" + :disabled="loading" + required + /> + <label for="man-smtp-password">{{ t('mail', 'SMTP Password') }}</label> + <input + id="man-smtp-password" + v-model="manualConfig.smtpPassword" + type="password" + :placeholder="t('mail', 'SMTP Password')" + :disabled="loading" + required + /> + </tab> + </tabs> + <input type="submit" class="primary" :disabled="loading" :value="submitButtonText" @click="onSubmit" /> + </div> </template> <script> import _ from 'underscore' -import { Tab, Tabs } from 'vue-tabs-component' +import {Tab, Tabs} from 'vue-tabs-component' export default { name: 'AccountForm', + components: { + Tab, + Tabs, + }, props: { displayName: { type: String, @@ -253,12 +257,9 @@ export default { account: { type: Object, required: false, + default: () => undefined, }, }, - components: { - Tab, - Tabs, - }, data() { const fromAccountOr = (prop, def) => { if (!_.isUndefined(this.account)) { @@ -290,9 +291,7 @@ export default { smtpUser: fromAccountOr('smtpUser', ''), smtpPassword: '', }, - submitButtonText: this.account - ? t('mail', 'Save') - : t('mail', 'Connect'), + submitButtonText: this.account ? t('mail', 'Save') : t('mail', 'Connect'), } }, computed: { diff --git a/src/components/Address.vue b/src/components/Address.vue index 13f149ace..641817d0a 100644 --- a/src/components/Address.vue +++ b/src/components/Address.vue @@ -1,30 +1,34 @@ <template> - <router-link :to="newMessageRoute" - exact - v-tooltip.bottom="email">{{ label }} - </router-link> + <router-link v-tooltip.bottom="email" :to="newMessageRoute" exact>{{ label }} </router-link> </template> <script> - export default { - name: "Address", - props: [ - 'email', - 'label', - ], - computed: { - newMessageRoute () { - return { - name: 'message', - params: { - accountId: this.$route.params.accountId, - folderId: this.$route.params.folderId, - messageUid: 'new' - }, query: { - to: this.email - } - } +export default { + name: 'Address', + props: { + email: { + type: String, + required: true, + }, + label: { + type: String, + required: true, + }, + }, + computed: { + newMessageRoute() { + return { + name: 'message', + params: { + accountId: this.$route.params.accountId, + folderId: this.$route.params.folderId, + messageUid: 'new', + }, + query: { + to: this.email, + }, } - } - } + }, + }, +} </script> diff --git a/src/components/AddressList.vue b/src/components/AddressList.vue index 05634dc87..d0d08fe84 100644 --- a/src/components/AddressList.vue +++ b/src/components/AddressList.vue @@ -1,23 +1,25 @@ <template> <span> <template v-for="(entry, idx) in entries"> - <Address :key="entry.email" - :email="entry.email" - :label="entry.label"/><!-- - --><span - v-if="idx+1 < entries.length">, </span> + <Address :key="entry.email" :email="entry.email" :label="entry.label" /><!-- + --><span v-if="idx + 1 < entries.length" :key="entry.email">, </span> </template> </span> </template> <script> - import Address from "./Address"; +import Address from './Address' - export default { - name: "AddressList", - components: {Address}, - props: [ - 'entries' - ] - } +export default { + name: 'AddressList', + components: { + Address, + }, + props: { + entries: { + type: Array, + required: true, + }, + }, +} </script> diff --git a/src/components/AppSettingsMenu.vue b/src/components/AppSettingsMenu.vue index 067a3903a..a49774530 100644 --- a/src/components/AppSettingsMenu.vue +++ b/src/components/AppSettingsMenu.vue @@ -1,25 +1,24 @@ <template> <div> - <router-link to="/setup" - class="button new-button">{{ t('mail', 'Add mail account') }} - </router-link> + <router-link to="/setup" class="button new-button">{{ t('mail', 'Add mail account') }} </router-link> - <p v-if="loadingAvatarSettings" - class="avatar-settings"> + <p v-if="loadingAvatarSettings" class="avatar-settings"> <span class="icon-loading-small"></span> {{ t('mail', 'Use Gravatar and favicon avatars') }} </p> <p v-else> - <input class="checkbox" - id="gravatar-enabled" - type="checkbox" - :checked="useExternalAvatars" - @change="onToggleExternalAvatars"> + <input + id="gravatar-enabled" + class="checkbox" + type="checkbox" + :checked="useExternalAvatars" + @change="onToggleExternalAvatars" + /> <label for="gravatar-enabled">{{ t('mail', 'Use Gravatar and favicon avatars') }}</label> </p> <p class="app-settings-hint"> - <router-link :to="{ name: 'keyboardShortcuts' }"> + <router-link :to="{name: 'keyboardShortcuts'}"> {{ t('mail', 'Keyboard shortcuts') }} </router-link> </p> @@ -27,39 +26,40 @@ </template> <script> - export default { - name: "AppSettingsMenu", - data () { - return { - loadingAvatarSettings: false, - } - }, - computed: { - useExternalAvatars () { - return this.$store.getters.getPreference('external-avatars', 'true') === 'true' - } +export default { + name: 'AppSettingsMenu', + data() { + return { + loadingAvatarSettings: false, + } + }, + computed: { + useExternalAvatars() { + return this.$store.getters.getPreference('external-avatars', 'true') === 'true' }, - methods: { - onToggleExternalAvatars (e) { - this.loadingAvatarSettings = true + }, + methods: { + onToggleExternalAvatars(e) { + this.loadingAvatarSettings = true - this.$store.dispatch('savePreference', { + this.$store + .dispatch('savePreference', { key: 'external-avatars', value: e.target.checked ? 'true' : 'false', }) - .catch(console.error.bind(this)) - .then(() => { - this.loadingAvatarSettings = false - }) - } - } - } + .catch(console.error.bind(this)) + .then(() => { + this.loadingAvatarSettings = false + }) + }, + }, +} </script> <style scoped> - p.avatar-settings span.icon-loading-small { - display: inline-block; - vertical-align: middle; - padding: 5px 0; - } -</style>
\ No newline at end of file +p.avatar-settings span.icon-loading-small { + display: inline-block; + vertical-align: middle; + padding: 5px 0; +} +</style> diff --git a/src/components/Avatar.vue b/src/components/Avatar.vue index cd705b501..5521f4fe8 100644 --- a/src/components/Avatar.vue +++ b/src/components/Avatar.vue @@ -20,50 +20,47 @@ --> <template> - <BaseAvatar v-if="loading || !hasAvatar" - :displayName="displayName"/> - <BaseAvatar v-else - :displayName="displayName" - :url="avatarUrl"/> + <BaseAvatar v-if="loading || !hasAvatar" :display-name="displayName" /> + <BaseAvatar v-else :display-name="displayName" :url="avatarUrl" /> </template> <script> - import _ from 'lodash' - import {Avatar as BaseAvatar} from 'nextcloud-vue' +import _ from 'lodash' +import {Avatar as BaseAvatar} from 'nextcloud-vue' - import {fetchAvatarUrlMemoized} from '../service/AvatarService' +import {fetchAvatarUrlMemoized} from '../service/AvatarService' - export default { - name: 'Avatar', - props: { - displayName: { - type: String, - required: true, - }, - email: { - type: String, - } +export default { + name: 'Avatar', + components: { + BaseAvatar, + }, + props: { + displayName: { + type: String, + required: true, }, - data() { - return { - loading: true, - avatarUrl: undefined, - } + email: { + type: String, + required: true, }, - computed: { - hasAvatar() { - return !_.isUndefined(this.avatarUrl) - } - }, - components: { - BaseAvatar - }, - mounted () { - fetchAvatarUrlMemoized(this.email) - .then(url => { - this.avatarUrl = url - this.loading = false - }) + }, + data() { + return { + loading: true, + avatarUrl: undefined, } - } + }, + computed: { + hasAvatar() { + return !_.isUndefined(this.avatarUrl) + }, + }, + mounted() { + fetchAvatarUrlMemoized(this.email).then(url => { + this.avatarUrl = url + this.loading = false + }) + }, +} </script> diff --git a/src/components/Composer.vue b/src/components/Composer.vue index 0a7a8e68b..8f8ca9a46 100644 --- a/src/components/Composer.vue +++ b/src/components/Composer.vue @@ -1,477 +1,470 @@ <template> - <div v-if="state === STATES.EDITING" - class="message-composer"> + <div v-if="state === STATES.EDITING" class="message-composer"> <div class="composer-fields mail-account"> <label class="to-label transparency" for="from"> {{ t('mail', 'from') }} </label> - <Multiselect :options="aliases" - id="from" - v-model="selectedAlias" - @keyup="onInputChanged" - label="name" track-by="id" - :customLabel="formatAliases" /> + <Multiselect + id="from" + v-model="selectedAlias" + :options="aliases" + label="name" + track-by="id" + :custom-label="formatAliases" + @keyup="onInputChanged" + /> </div> <div class="composer-fields"> <label class="to-label transparency" for="to"> {{ t('mail', 'to') }} </label> - <Multiselect :options="selectableRecipients" - id="to" - @keyup="onInputChanged" - :taggable="true" - @tag="onNewToAddr" - @search-change="onAutocomplete" - v-model="selectTo" - label="label" - track-by="email" - :multiple="true" /> - <a v-if="!showCC" - href="#" - @click.prevent="showCC = true"> - {{ t ('mail', '+ cc/bcc') }} + <Multiselect + id="to" + v-model="selectTo" + :options="selectableRecipients" + :taggable="true" + label="label" + track-by="email" + :multiple="true" + @keyup="onInputChanged" + @tag="onNewToAddr" + @search-change="onAutocomplete" + /> + <a v-if="!showCC" href="#" @click.prevent="showCC = true"> + {{ t('mail', '+ cc/bcc') }} </a> </div> - <div class="composer-fields" - v-if="showCC"> + <div v-if="showCC" class="composer-fields"> <label for="cc" class="cc-label transparency"> {{ t('mail', 'cc') }} </label> - <Multiselect :options="selectableRecipients" - id="cc" - @keyup="onInputChanged" - :taggable="true" - @tag="onNewCcAddr" - @search-change="onAutocomplete" - v-model="selectCc" - label="label" track-by="email" - :multiple="true" /> + <Multiselect + id="cc" + v-model="selectCc" + :options="selectableRecipients" + :taggable="true" + label="label" + track-by="email" + :multiple="true" + @keyup="onInputChanged" + @tag="onNewCcAddr" + @search-change="onAutocomplete" + /> </div> - <div class="composer-fields" - v-if="showCC"> + <div v-if="showCC" class="composer-fields"> <label for="bcc" class="bcc-label transparency"> {{ t('mail', 'bcc') }} </label> - <Multiselect :options="selectableRecipients" - id="bcc" - @keyup="onInputChanged" - :taggable="true" - @tag="onNewBccAddr" - @search-change="onAutocomplete" - v-model="selectBcc" - label="label" track-by="email" - :multiple="true" /> + <Multiselect + id="bcc" + v-model="selectBcc" + :options="selectableRecipients" + :taggable="true" + label="label" + track-by="email" + :multiple="true" + @keyup="onInputChanged" + @tag="onNewBccAddr" + @search-change="onAutocomplete" + /> </div> <div class="composer-fields"> <label for="subject" class="subject-label transparency"> {{ t('mail', 'Subject') }} </label> - <input type="text" - id="subject" - name="subject" - v-model="subjectVal" - v-on:keyup="onInputChanged" - class="subject" autocomplete="off" - :placeholder="t('mail', 'Subject')"/> + <input + id="subject" + v-model="subjectVal" + type="text" + name="subject" + class="subject" + autocomplete="off" + :placeholder="t('mail', 'Subject')" + @keyup="onInputChanged" + /> </div> - <div v-if="noReply" - class="warning noreply-box"> + <div v-if="noReply" class="warning noreply-box"> {{ t('mail', 'Note that the mail came from a noreply address so your reply will probably not be read.') }} </div> <div class="composer-fields"> - <textarea name="body" - class="message-body" - v-autosize - v-model="bodyVal" - @keyup="onInputChanged" - @keypress="onBodyKeyPress" - :placeholder="t('mail', 'Message …')">{{message}}</textarea> + <textarea + v-model="bodyVal" + v-autosize + name="body" + class="message-body" + :placeholder="t('mail', 'Message …')" + @keyup="onInputChanged" + @keypress="onBodyKeyPress" + ></textarea> </div> <div class="submit-message-wrapper"> - <input class="submit-message send primary" - type="submit" - :value="submitButtonTitle" - v-on:click="onSend"> + <input class="submit-message send primary" type="submit" :value="submitButtonTitle" @click="onSend" /> </div> - <ComposerAttachments v-model="attachments" - @upload="onAttachmentsUploading"/> - <span id="draft-status" v-if="savingDraft === true">{{ t('mail', 'Saving draft …') }}</span> - <span id="draft-status" v-else-if="savingDraft === false">{{ t('mail', 'Draft saved') }}</span> + <ComposerAttachments v-model="attachments" @upload="onAttachmentsUploading" /> + <span v-if="savingDraft === true" id="draft-status">{{ t('mail', 'Saving draft …') }}</span> + <span v-else-if="savingDraft === false" id="draft-status">{{ t('mail', 'Draft saved') }}</span> </div> - <Loading v-else-if="state === STATES.UPLOADING" - :hint="t('mail', 'Uploading attachments …')" /> - <Loading v-else-if="state === STATES.SENDING" - :hint="t('mail', 'Sending …')" /> - <div v-else-if="state === STATES.ERROR" - class="emptycontent"> + <Loading v-else-if="state === STATES.UPLOADING" :hint="t('mail', 'Uploading attachments …')" /> + <Loading v-else-if="state === STATES.SENDING" :hint="t('mail', 'Sending …')" /> + <div v-else-if="state === STATES.ERROR" class="emptycontent"> <h2>{{ t('mail', 'Error sending your message') }}</h2> <p v-if="errorText">{{ errorText }}</p> - <button v-on:click="state = STATES.EDITING" - class="button">{{ t('mail', 'Go back') }}</button> - <button v-on:click="onSend" - class="button primary">{{ t('mail', 'Retry') }}</button> + <button class="button" @click="state = STATES.EDITING">{{ t('mail', 'Go back') }}</button> + <button class="button primary" @click="onSend">{{ t('mail', 'Retry') }}</button> </div> - <div v-else - class="emptycontent"> + <div v-else class="emptycontent"> <h2 v-if="!isReply">{{ t('mail', 'Message sent!') }}</h2> <h2 v-else>{{ t('mail', 'Reply sent!') }}</h2> - <button v-on:click="reset" - v-if="!isReply" - class="button primary">{{ t('mail', 'Write another message') }}</button> + <button v-if="!isReply" class="button primary" @click="reset"> + {{ t('mail', 'Write another message') }} + </button> </div> </template> <script> - import _ from 'lodash' - import Autosize from 'vue-autosize' - import debouncePromise from 'debounce-promise' - import {Multiselect} from 'nextcloud-vue' - import Vue from 'vue' +import _ from 'lodash' +import Autosize from 'vue-autosize' +import debouncePromise from 'debounce-promise' +import {Multiselect} from 'nextcloud-vue' +import Vue from 'vue' - import {findRecipient} from '../service/AutocompleteService' - import Loading from './Loading' - import ComposerAttachments from './ComposerAttachments' +import {findRecipient} from '../service/AutocompleteService' +import Loading from './Loading' +import ComposerAttachments from './ComposerAttachments' - const debouncedSearch = debouncePromise(findRecipient, 500) +const debouncedSearch = debouncePromise(findRecipient, 500) - Vue.use(Autosize) +Vue.use(Autosize) - const STATES = Object.seal({ - EDITING: 0, - UPLOADING: 1, - SENDING: 2, - ERROR: 3, - FINISHED: 4, - }) +const STATES = Object.seal({ + EDITING: 0, + UPLOADING: 1, + SENDING: 2, + ERROR: 3, + FINISHED: 4, +}) - export default { - name: 'Composer', - components: { - ComposerAttachments, - Loading, - Multiselect, +export default { + name: 'Composer', + components: { + ComposerAttachments, + Loading, + Multiselect, + }, + props: { + replyTo: { + type: Object, + default: () => undefined, }, - props: { - replyTo: { - type: Object, - }, - fromAccount: { - type: Number, - }, - to: { - type: Array, - default: () => [], - }, - cc: { - type: Array, - default: () => [], - }, - bcc: { - type: Array, - default: () => [], - }, - subject: { - type: String, - default: '', - }, - body: { - type: String, - default: '' - }, - draft: { - type: Function, - required: true, - }, - send: { - type: Function, - required: true, + fromAccount: { + type: Number, + default: () => undefined, + }, + to: { + type: Array, + default: () => [], + }, + cc: { + type: Array, + default: () => [], + }, + bcc: { + type: Array, + default: () => [], + }, + subject: { + type: String, + default: '', + }, + body: { + type: String, + default: '', + }, + draft: { + type: Function, + required: true, + }, + send: { + type: Function, + required: true, + }, + }, + data() { + return { + showCC: this.cc.length > 0, + selectedAlias: -1, // Fixed in `beforeMount` + autocompleteRecipients: this.to.concat(this.cc).concat(this.bcc), + newRecipients: [], + subjectVal: this.subject, + bodyVal: this.body, + attachments: [], + noReply: this.to.some(to => to.email.startsWith('noreply@') || to.email.startsWith('no-reply@')), + submitButtonTitle: t('mail', 'Send'), + draftsPromise: Promise.resolve(), + attachmentsPromise: Promise.resolve(), + savingDraft: undefined, + saveDraftDebounced: _.debounce(this.saveDraft, 700), + state: STATES.EDITING, + errorText: undefined, + STATES, + selectTo: this.to, + selectCc: this.cc, + selectBcc: this.bcc, + } + }, + computed: { + aliases() { + return this.$store.getters.getAccounts().filter(a => !a.isUnified) + }, + selectableRecipients() { + return this.newRecipients.concat(this.autocompleteRecipients) + }, + isReply() { + return !_.isUndefined(this.replyTo) + }, + }, + beforeMount() { + if (this.fromAccount) { + this.selectedAlias = this.aliases.find(alias => alias.id === this.fromAccount) + } + this.selectedAlias = this.aliases[0] + }, + methods: { + recipientToRfc822(recipient) { + if (recipient.email === recipient.label) { + // From mailto or sender without proper label + return recipient.email + } else if (recipient.label === '') { + // Invalid label + return recipient.email + } else if (recipient.email.search(/^[a-zA-Z]+:[a-zA-Z]+$/) === 0) { + // Group integration + return recipient.email + } else { + // Proper layout with label + return `"${recipient.label}" <${recipient.email}>` } }, - data () { - return { - showCC: this.cc.length > 0, - selectedAlias: -1, // Fixed in `beforeMount` - autocompleteRecipients: this.to.concat(this.cc).concat(this.bcc), - newRecipients: [], - subjectVal: this.subject, - bodyVal: this.body, - attachments: [], - noReply: this.to.some(to => - to.email.startsWith('noreply@') || to.email.startsWith('no-reply@') - ), - message: '', - submitButtonTitle: t('mail', 'Send'), - draftsPromise: Promise.resolve(), - attachmentsPromise: Promise.resolve(), - savingDraft: undefined, - saveDraftDebounced: _.debounce(this.saveDraft, 700), - state: STATES.EDITING, - errorText: undefined, - STATES, - selectTo: this.to, - selectCc: this.cc, - selectBcc: this.bcc, + getMessageData() { + return uid => { + return { + account: this.selectedAlias.id, + to: this.selectTo.map(this.recipientToRfc822).join(', '), + cc: this.selectCc.map(this.recipientToRfc822).join(', '), + bcc: this.selectBcc.map(this.recipientToRfc822).join(', '), + draftUID: uid, + subject: this.subjectVal, + body: this.bodyVal, + attachments: this.attachments, + folderId: this.replyTo ? this.replyTo.folderId : undefined, + messageId: this.replyTo ? this.replyTo.messageId : undefined, + } } }, - beforeMount () { - if (this.fromAccount) { - this.selectedAlias = this.aliases.find(alias => alias.id === this.fromAccount) + saveDraft(data) { + this.savingDraft = true + this.draftsPromise = this.draftsPromise + .then(uid => this.draft(data(uid))) + .catch(console.error.bind(this)) + .then(uid => { + this.savingDraft = false + return uid + }) + }, + onInputChanged() { + this.saveDraftDebounced(this.getMessageData()) + }, + onAutocomplete(term) { + if (_.isUndefined(term) || term === '') { + return } - this.selectedAlias = this.aliases[0] + debouncedSearch(term).then(results => { + this.autocompleteRecipients = _.uniqBy(this.autocompleteRecipients.concat(results), 'email') + }) }, - computed: { - aliases () { - return this.$store.getters.getAccounts() - .filter(a => !a.isUnified) - }, - selectableRecipients () { - return this.newRecipients.concat(this.autocompleteRecipients) - }, - isReply () { - return !_.isUndefined(this.replyTo) + onAttachmentsUploading(uploaded) { + this.attachmentsPromise = this.attachmentsPromise + .then(() => uploaded) + .catch(console.error.bind(this)) + .then(() => console.debug('attachments uploaded')) + }, + onBodyKeyPress(event) { + // CTRL+Enter sends the message + if (event.keyCode === 13 && event.ctrlKey) { + return this.onSend() } }, - methods: { - recipientToRfc822 (recipient) { - if (recipient.email === recipient.label) { - // From mailto or sender without proper label - return recipient.email - } else if (recipient.label === '') { - // Invalid label - return recipient.email - } else if (recipient.email.search(/^[a-zA-Z]+:[a-zA-Z]+$/) === 0) { - // Group integration - return recipient.email - } else { - // Proper layout with label - return `"${recipient.label}" <${recipient.email}>` - } - }, - getMessageData () { - return uid => { - return { - account: this.selectedAlias.id, - to: this.selectTo.map(this.recipientToRfc822).join(', '), - cc: this.selectCc.map(this.recipientToRfc822).join(', '), - bcc: this.selectBcc.map(this.recipientToRfc822).join(', '), - draftUID: uid, - subject: this.subjectVal, - body: this.bodyVal, - attachments: this.attachments, - folderId: this.replyTo ? this.replyTo.folderId : undefined, - messageId: this.replyTo ? this.replyTo.messageId : undefined, - } - } - }, - saveDraft (data) { - this.savingDraft = true - this.draftsPromise = this.draftsPromise - .then(uid => this.draft(data(uid))) - .catch(console.error.bind(this)) - .then(uid => { - this.savingDraft = false - return uid - }) - }, - onInputChanged () { - this.saveDraftDebounced(this.getMessageData()) - }, - onAutocomplete (term) { - if (_.isUndefined(term) || term === '') { - return - } - debouncedSearch(term) - .then(results => { - this.autocompleteRecipients = _.uniqBy( - this.autocompleteRecipients.concat(results), - 'email', - ) - }) - }, - onAttachmentsUploading (uploaded) { - this.attachmentsPromise = this.attachmentsPromise - .then(() => uploaded) - .catch(console.error.bind(this)) - .then(() => console.debug('attachments uploaded')) - }, - onBodyKeyPress (event) { - // CTRL+Enter sends the message - if (event.keyCode === 13 && event.ctrlKey) { - return this.onSend() - } - }, - onNewToAddr (addr) { - this.onNewAddr(addr, this.selectTo) - }, - onNewCcAddr (addr) { - this.onNewAddr(addr, this.selectCc) - }, - onNewBccAddr (addr) { - this.onNewAddr(addr, this.selectBcc) - }, - onNewAddr (addr, list) { - const res = { - label: addr, // TODO: parse if possible - email: addr, // TODO: parse if possible - } - this.newRecipients.push(res) - list.push(res) - }, - onSend () { - this.state = STATES.UPLOADING - - return this.attachmentsPromise - .then(() => this.state = STATES.SENDING) - .then(() => this.draftsPromise) - .then(this.getMessageData()) - .then(data => this.send(data)) - .then(() => console.info('message sent')) - .then(() => this.state = STATES.FINISHED) - .catch(e => { - console.error('could not send message', e) - if (e && e.toString) { - this.errorText = e.toString() - } - this.state = STATES.ERROR - }) - }, - reset () { - this.selectTo = [] - this.selectCc = [] - this.selectBcc = [] - this.subjectVal = '' - this.bodyVal = '' - this.attachments = [] - this.errorText = undefined - this.state = STATES.EDITING - }, - /** - * Format aliases for the Multiselect - * @returns {string} - */ - formatAliases(alias) { - return `${alias.name} <${alias.emailAddress}>` + onNewToAddr(addr) { + this.onNewAddr(addr, this.selectTo) + }, + onNewCcAddr(addr) { + this.onNewAddr(addr, this.selectCc) + }, + onNewBccAddr(addr) { + this.onNewAddr(addr, this.selectBcc) + }, + onNewAddr(addr, list) { + const res = { + label: addr, // TODO: parse if possible + email: addr, // TODO: parse if possible } - } - } + this.newRecipients.push(res) + list.push(res) + }, + onSend() { + this.state = STATES.UPLOADING + + return this.attachmentsPromise + .then(() => (this.state = STATES.SENDING)) + .then(() => this.draftsPromise) + .then(this.getMessageData()) + .then(data => this.send(data)) + .then(() => console.info('message sent')) + .then(() => (this.state = STATES.FINISHED)) + .catch(e => { + console.error('could not send message', e) + if (e && e.toString) { + this.errorText = e.toString() + } + this.state = STATES.ERROR + }) + }, + reset() { + this.selectTo = [] + this.selectCc = [] + this.selectBcc = [] + this.subjectVal = '' + this.bodyVal = '' + this.attachments = [] + this.errorText = undefined + this.state = STATES.EDITING + }, + /** + * Format aliases for the Multiselect + * @returns {string} + */ + formatAliases(alias) { + return `${alias.name} <${alias.emailAddress}>` + }, + }, +} </script> <style scoped> - .message-composer { - margin: 0; - margin-bottom: 10px; /* line up with the send button */ - z-index: 100; - } - - #reply-composer .message-composer { - margin: 0; - } +.message-composer { + margin: 0; + margin-bottom: 10px; /* line up with the send button */ + z-index: 100; +} - .composer-fields.mail-account > .multiselect { - max-width: none; - min-height: auto; - width: 350px; - } +#reply-composer .message-composer { + margin: 0; +} - .composer-fields { - display: flex; - align-items: center; - border-top: 1px solid var(--color-border); - padding-right: 30px; - } - .composer-fields .multiselect, - .composer-fields input, - .composer-fields textarea { - flex-grow: 1; - max-width: none; - border: none; - border-radius: 0px; - } - .noreply-box { - margin-top: 0; - background: #fdffc3; - padding-left: 64px; - } +.composer-fields.mail-account > .multiselect { + max-width: none; + min-height: auto; + width: 350px; +} - #to, - #cc, - #bcc, - input.subject, - textarea.message-body { - padding: 12px; - margin: 0; - } +.composer-fields { + display: flex; + align-items: center; + border-top: 1px solid var(--color-border); + padding-right: 30px; +} +.composer-fields .multiselect, +.composer-fields input, +.composer-fields textarea { + flex-grow: 1; + max-width: none; + border: none; + border-radius: 0px; +} +.noreply-box { + margin-top: 0; + background: #fdffc3; + padding-left: 64px; +} - #to { - padding-right: 60px; /* for cc-bcc-toggle */ - } +#to, +#cc, +#bcc, +input.subject, +textarea.message-body { + padding: 12px; + margin: 0; +} - input.cc, - input.bcc, - input.subject, - textarea.message-body { - border-top: none; - } +#to { + padding-right: 60px; /* for cc-bcc-toggle */ +} - input.subject { - font-size: 20px; - font-weight: 300; - } +input.cc, +input.bcc, +input.subject, +textarea.message-body { + border-top: none; +} - textarea.message-body { - min-height: 300px; - resize: none; - padding-right: 25%; - } +input.subject { + font-size: 20px; + font-weight: 300; +} - #draft-status { - padding: 5px; - opacity: 0.5; - font-size: small; - } +textarea.message-body { + min-height: 300px; + resize: none; + padding-right: 25%; +} - label.to-label, - label.cc-label, - label.bcc-label, - label.subject-label { - padding: 12px; - padding-left: 30px; - cursor: text; - opacity: .5; - width: 90px; - text-align: right; - } +#draft-status { + padding: 5px; + opacity: 0.5; + font-size: small; +} - label.bcc-label { - top: initial; - bottom: 0; - } +label.to-label, +label.cc-label, +label.bcc-label, +label.subject-label { + padding: 12px; + padding-left: 30px; + cursor: text; + opacity: 0.5; + width: 90px; + text-align: right; +} - textarea.reply { - min-height: 100px; - } +label.bcc-label { + top: initial; + bottom: 0; +} - input.submit-message, - .submit-message-wrapper { - position: fixed; - bottom: 10px; - right: 15px; - } +textarea.reply { + min-height: 100px; +} - .submit-message-wrapper { - position: fixed; - height: 36px; - width: 60px; - } +input.submit-message, +.submit-message-wrapper { + position: fixed; + bottom: 10px; + right: 15px; +} - .submit-message.send { - padding: 12px; - } +.submit-message-wrapper { + position: fixed; + height: 36px; + width: 60px; +} +.submit-message.send { + padding: 12px; +} </style> <style> - .multiselect .multiselect__tags { - border: none !important; - } +.multiselect .multiselect__tags { + border: none !important; +} </style> diff --git a/src/components/ComposerAttachments.vue b/src/components/ComposerAttachments.vue index a4b1be033..b674ccd40 100644 --- a/src/components/ComposerAttachments.vue +++ b/src/components/ComposerAttachments.vue @@ -22,143 +22,126 @@ <template> <div class="new-message-attachments"> <ul> - <li v-for="attachment in value"> + <li v-for="attachment in value" :key="attachment.id"> <div class="new-message-attachment-name"> - {{attachment.displayName}} + {{ attachment.displayName }} </div> - <div class="new-message-attachments-action svg icon-delete" - v-on:click="onDelete(attachment)"></div> + <div class="new-message-attachments-action svg icon-delete" @click="onDelete(attachment)"></div> </li> </ul> - <button class="button" - :disabled="uploading" - v-on:click="onAddLocalAttachment"> - <span :class="{ 'icon-upload' : !uploading, 'icon-loading-small': uploading }"/> - {{ uploading ? - t('mail', 'Uploading …') : - t('mail', 'Upload attachment') - }} + <button class="button" :disabled="uploading" @click="onAddLocalAttachment"> + <span :class="{'icon-upload': !uploading, 'icon-loading-small': uploading}"></span> + {{ uploading ? t('mail', 'Uploading …') : t('mail', 'Upload attachment') }} </button> - <button class="button" - v-on:click="onAddCloudAttachment"> - <span class="icon-folder"/> + <button class="button" @click="onAddCloudAttachment"> + <span class="icon-folder" /> {{ t('mail', 'Add attachment from Files') }} </button> - <input type="file" - ref="localAttachments" - v-on:change="onLocalAttachmentSelected" - multiple - style="display: none;"> + <input ref="localAttachments" type="file" multiple style="display: none;" @change="onLocalAttachmentSelected" /> </div> </template> <script> - import _ from 'lodash' - import {translate as t} from 'nextcloud-server/dist/l10n' - import {pickFileOrDirectory} from 'nextcloud-server/dist/files' +import _ from 'lodash' +import {translate as t} from 'nextcloud-server/dist/l10n' +import {pickFileOrDirectory} from 'nextcloud-server/dist/files' - import {uploadLocalAttachment} from '../service/AttachmentService' +import {uploadLocalAttachment} from '../service/AttachmentService' - export default { - name: 'ComposerAttachments', - data () { +export default { + name: 'ComposerAttachments', + props: { + value: { + type: Array, + required: true, + }, + }, + data() { + return { + uploading: false, + } + }, + methods: { + onAddLocalAttachment() { + this.$refs.localAttachments.click() + }, + fileNameToAttachment(name, id) { return { - uploading: false, + fileName: name, + displayName: _.trimStart(name, '/'), + id, + isLocal: !_.isUndefined(id), } }, - props: { - value: { - type: Array, - required: true, - } + emitNewAttachment(attachment) { + this.$emit('input', this.value.concat([attachment])) }, - methods: { - onAddLocalAttachment () { - this.$refs.localAttachments.click() - }, - fileNameToAttachment (name, id) { - return { - fileName: name, - displayName: _.trimStart(name, '/'), - id, - isLocal: !_.isUndefined(id) - } - }, - emitNewAttachment (attachment) { - this.$emit('input', this.value.concat([attachment])) - }, - onLocalAttachmentSelected (e) { - this.uploading = true + onLocalAttachmentSelected(e) { + this.uploading = true - const done = Promise.all( - _.map( - e.target.files, - file => uploadLocalAttachment(file) - .then(({file, id}) => { - console.info('uploaded') - return this.emitNewAttachment( - this.fileNameToAttachment(file.name, id) - ) - }) - ) + const done = Promise.all( + _.map(e.target.files, file => + uploadLocalAttachment(file).then(({file, id}) => { + console.info('uploaded') + return this.emitNewAttachment(this.fileNameToAttachment(file.name, id)) + }) ) - .catch(console.error.bind(this)) - .then(() => this.uploading = false) + ) + .catch(console.error.bind(this)) + .then(() => (this.uploading = false)) - this.$emit('upload', done) + this.$emit('upload', done) - return done - }, - onAddCloudAttachment () { - return pickFileOrDirectory(t('mail', 'Choose a file to add as attachment')) - .then(path => this.emitNewAttachment( - this.fileNameToAttachment(path) - )) - .catch(console.error.bind(this)) - }, - onDelete (attachment) { - this.$emit('input', this.value.filter(a => a !== attachment)) - } - } - } + return done + }, + onAddCloudAttachment() { + return pickFileOrDirectory(t('mail', 'Choose a file to add as attachment')) + .then(path => this.emitNewAttachment(this.fileNameToAttachment(path))) + .catch(console.error.bind(this)) + }, + onDelete(attachment) { + this.$emit('input', this.value.filter(a => a !== attachment)) + }, + }, +} </script> <style scoped> - button { - /* TODO: remove for Nextcloud 15+ */ - /* https://github.com/nextcloud/server/pull/12138 */ - display: inline-block; - } +button { + /* TODO: remove for Nextcloud 15+ */ + /* https://github.com/nextcloud/server/pull/12138 */ + display: inline-block; +} - .new-message-attachments li { - padding: 10px; - } +.new-message-attachments li { + padding: 10px; +} - .new-message-attachments-action { - display: inline-block; - vertical-align: middle; - padding: 22px; - opacity: .5; - } +.new-message-attachments-action { + display: inline-block; + vertical-align: middle; + padding: 22px; + opacity: 0.5; +} - /* attachment filenames */ - .new-message-attachment-name { - display: inline-block; - } +/* attachment filenames */ +.new-message-attachment-name { + display: inline-block; +} - /* Colour the filename with a different color during attachment upload */ - .new-message-attachment-name.upload-ongoing { - color: #0082c9; - } +/* Colour the filename with a different color during attachment upload */ +.new-message-attachment-name.upload-ongoing { + color: #0082c9; +} - /* Colour the filename in red if the attachment upload failed */ - .new-message-attachment-name.upload-warning { - color: #d2322d; - } +/* Colour the filename in red if the attachment upload failed */ +.new-message-attachment-name.upload-warning { + color: #d2322d; +} - /* Red ProgressBar for failed attachment uploads */ - .new-message-attachment-name.upload-warning .ui-progressbar-value { - border: 1px solid #e9322d; - background: #e9322d; - } +/* Red ProgressBar for failed attachment uploads */ +.new-message-attachment-name.upload-warning .ui-progressbar-value { + border: 1px solid #e9322d; + background: #e9322d; +} </style> diff --git a/src/components/EmptyFolder.vue b/src/components/EmptyFolder.vue index 6126ee039..cf8d7f868 100644 --- a/src/components/EmptyFolder.vue +++ b/src/components/EmptyFolder.vue @@ -6,7 +6,7 @@ </template> <script> - export default { - name: 'EmptyFolder' - }; +export default { + name: 'EmptyFolder', +} </script> diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue index dba07db5c..50f076aca 100644 --- a/src/components/Envelope.vue +++ b/src/components/Envelope.vue @@ -1,58 +1,38 @@ <template> - <router-link - class="app-content-list-item" - :class="{ unseen: data.flags.unseen, draft }" - :to="link" - > - <div class="mail-message-account-color" - v-if="showAccountColor" - :style="{ 'background-color': accountColor }"> - </div> - <div - class="app-content-list-item-star icon-starred" - :data-starred="data.flags.flagged ? 'true':'false'" - @click.prevent="toggleFlagged" - /> - <div class="app-content-list-item-icon"> - <Avatar :displayName="sender" - :email="senderEmail"/> - </div> - <div - class="app-content-list-item-line-one" - :title="sender" - > - {{ sender }} - </div> - <div - class="app-content-list-item-line-two" - :title="data.subject" - > - <span - v-if="data.flags.answered" - class="icon-reply" - /> - <span - v-if="data.flags.hasAttachments" - class="icon-public icon-attachment" - /> - <span - v-if="draft" - class="draft" - > - <em>{{ t('mail', 'Draft: ') }}</em> - </span> - {{ data.subject }} - </div> - <div class="app-content-list-item-details date"> - <Moment :timestamp="data.dateInt" /> - </div> - <Action class="app-content-list-item-menu" - :actions="actions" /> - </router-link> + <router-link class="app-content-list-item" :class="{unseen: data.flags.unseen, draft}" :to="link"> + <div + v-if="showAccountColor" + class="mail-message-account-color" + :style="{'background-color': accountColor}" + ></div> + <div + class="app-content-list-item-star icon-starred" + :data-starred="data.flags.flagged ? 'true' : 'false'" + @click.prevent="toggleFlagged" + /> + <div class="app-content-list-item-icon"> + <Avatar :display-name="sender" :email="senderEmail" /> + </div> + <div class="app-content-list-item-line-one" :title="sender"> + {{ sender }} + </div> + <div class="app-content-list-item-line-two" :title="data.subject"> + <span v-if="data.flags.answered" class="icon-reply" /> + <span v-if="data.flags.hasAttachments" class="icon-public icon-attachment" /> + <span v-if="draft" class="draft"> + <em>{{ t('mail', 'Draft: ') }}</em> + </span> + {{ data.subject }} + </div> + <div class="app-content-list-item-details date"> + <Moment :timestamp="data.dateInt" /> + </div> + <Action class="app-content-list-item-menu" :actions="actions" /> + </router-link> </template> <script> -import { Action } from 'nextcloud-vue' +import {Action} from 'nextcloud-vue' import Moment from './Moment' import Avatar from './Avatar' @@ -68,27 +48,25 @@ export default { props: { data: { type: Object, - required: true, - }, - showAccountColor: { + required: true, + }, + showAccountColor: { type: Boolean, - default: false, - } + default: false, + }, }, computed: { accountColor() { - return calculateAccountColor( - this.$store.getters.getAccount(this.data.accountId).emailAddress - ) - }, - draft() { + return calculateAccountColor(this.$store.getters.getAccount(this.data.accountId).emailAddress) + }, + draft() { return this.data.flags.draft - }, - link() { + }, + link() { if (this.draft) { // TODO: does not work with a unified drafts folder - // the query should also contain the account and folder - // id for that to work + // the query should also contain the account and folder + // id for that to work return { name: 'message', params: { @@ -97,7 +75,7 @@ export default { messageUid: 'new', draftUid: this.data.uid, }, - exact: true + exact: true, } } else { return { @@ -105,12 +83,12 @@ export default { params: { accountId: this.$route.params.accountId, folderId: this.$route.params.folderId, - messageUid: this.data.uid + messageUid: this.data.uid, }, - exact: true + exact: true, } - } - }, + } + }, sender() { if (this.data.from.length === 0) { // No sender @@ -120,11 +98,13 @@ export default { const first = this.data.from[0] return first.label || first.email }, - senderEmail() { + senderEmail() { if (this.data.from.length > 0) { return this.data.from[0].email + } else { + return undefined } - }, + }, actions() { return [ { @@ -140,8 +120,8 @@ export default { action: () => { this.$emit('delete', this.data) this.$store.dispatch('deleteMessage', this.data) - } - } + }, + }, ] }, }, diff --git a/src/components/EnvelopeList.vue b/src/components/EnvelopeList.vue index 7ea81467a..5a4938c99 100644 --- a/src/components/EnvelopeList.vue +++ b/src/components/EnvelopeList.vue @@ -1,256 +1,264 @@ <template> - <transition-group name="list" - tag="div" - class="app-content-list" - v-infinite-scroll="loadMore" - infinite-scroll-disabled="loading" - infinite-scroll-distance="30" - v-scroll="onScroll" - v-shortkey.once="shortkeys" - @shortkey.native="handleShortcut"> - <div id="list-refreshing" - key="loading" - class="icon-loading-small" - :class="{refreshing: refreshing}"/> - <EmptyFolder v-if="envelopes.length === 0" - key="empty"/> - <Envelope v-else - v-for="env in envelopes" - :key="env.uid" - :data="env" - :show-account-color="folder.isUnified" - @delete="onEnvelopeDeleted"/> - <div id="load-more-mail-messages" - key="loadingMore" - :class="{'icon-loading-small': loadingMore}"/> + <transition-group + v-infinite-scroll="loadMore" + v-scroll="onScroll" + v-shortkey.once="shortkeys" + name="list" + tag="div" + class="app-content-list" + infinite-scroll-disabled="loading" + infinite-scroll-distance="30" + @shortkey.native="handleShortcut" + > + <div id="list-refreshing" key="loading" class="icon-loading-small" :class="{refreshing: refreshing}" /> + <EmptyFolder v-if="envelopes.length === 0" key="empty" /> + <Envelope + v-for="env in envelopes" + v-else + :key="env.uid" + :data="env" + :show-account-color="folder.isUnified" + @delete="onEnvelopeDeleted" + /> + <div id="load-more-mail-messages" key="loadingMore" :class="{'icon-loading-small': loadingMore}" /> </transition-group> </template> <script> - import _ from 'lodash' - import infiniteScroll from 'vue-infinite-scroll' - import vuescroll from 'vue-scroll' - import Vue from 'vue' +import _ from 'lodash' +import infiniteScroll from 'vue-infinite-scroll' +import vuescroll from 'vue-scroll' +import Vue from 'vue' - import EmptyFolder from './EmptyFolder' - import Envelope from './Envelope' +import EmptyFolder from './EmptyFolder' +import Envelope from './Envelope' - Vue.use(vuescroll, {throttle: 600}) +Vue.use(vuescroll, {throttle: 600}) - export default { - name: "EnvelopeList", - props: { - account: { - type: Object, - required: true, - }, - folder: { - type: Object, - required: true, - }, - envelopes: { - type: Array, - required: true, - }, - searchQuery: { - type: String, - required: false, - default: undefined, - } +export default { + name: 'EnvelopeList', + components: { + Envelope, + EmptyFolder, + }, + directives: { + infiniteScroll, + }, + props: { + account: { + type: Object, + required: true, }, - components: { - Envelope, - EmptyFolder, + folder: { + type: Object, + required: true, }, - directives: { - infiniteScroll, + envelopes: { + type: Array, + required: true, }, - data () { - return { - loadingMore: false, - refreshing: false, - shortkeys: { - del: ['del'], - flag: ['s'], - next: ['arrowright'], - prev: ['arrowleft'], - refresh: ['r'], - unseen: ['u'] - } - } + searchQuery: { + type: String, + required: false, + default: undefined, }, - computed: { - isSearch () { - return !_.isUndefined(this.searchQuery) - } + }, + data() { + return { + loadingMore: false, + refreshing: false, + shortkeys: { + del: ['del'], + flag: ['s'], + next: ['arrowright'], + prev: ['arrowleft'], + refresh: ['r'], + unseen: ['u'], + }, + } + }, + computed: { + isSearch() { + return !_.isUndefined(this.searchQuery) }, - methods: { - loadMore () { - this.loadingMore = true + }, + methods: { + loadMore() { + this.loadingMore = true - this.$store.dispatch('fetchNextEnvelopePage', { + this.$store + .dispatch('fetchNextEnvelopePage', { accountId: this.$route.params.accountId, folderId: this.$route.params.folderId, query: this.searchQuery, - }).catch(console.error.bind(this)).then(() => { + }) + .catch(console.error.bind(this)) + .then(() => { this.loadingMore = false }) - }, - sync () { - this.refreshing = true + }, + sync() { + this.refreshing = true - this.$store.dispatch('syncEnvelopes', { + this.$store + .dispatch('syncEnvelopes', { accountId: this.$route.params.accountId, folderId: this.$route.params.folderId, - }).catch(console.error.bind(this)).then(() => { + }) + .catch(console.error.bind(this)) + .then(() => { this.refreshing = false }) - }, - onEnvelopeDeleted (envelope) { - const envelopes = this.envelopes - const idx = this.envelopes.indexOf(envelope) - if (idx === -1) { - console.debug('envelope to delete does not exist in envelope list') - return - } + }, + onEnvelopeDeleted(envelope) { + const envelopes = this.envelopes + const idx = this.envelopes.indexOf(envelope) + if (idx === -1) { + console.debug('envelope to delete does not exist in envelope list') + return + } - let next - if (idx === 0) { - next = envelopes[idx + 1] - } else { - next = envelopes[idx - 1] - } + let next + if (idx === 0) { + next = envelopes[idx + 1] + } else { + next = envelopes[idx - 1] + } - if (!next) { - console.debug('no next/previous envelope, not navigating') - return - } + if (!next) { + console.debug('no next/previous envelope, not navigating') + return + } - // Keep the selected account-folder combination, but navigate to a different message - // (it's not a bug that we don't use next.accountId and next.folderId here) - this.$router.push({ - name: 'message', - params: { - accountId: this.$route.params.accountId, - folderId: this.$route.params.folderId, - messageUid: next.uid, - } - }); - }, - onScroll (e, p) { - if (p.scrollTop === 0 && !this.refreshing) { - return this.sync() - } - }, - handleShortcut (e) { - const envelopes = this.envelopes - const currentUid = this.$route.params.messageUid + // Keep the selected account-folder combination, but navigate to a different message + // (it's not a bug that we don't use next.accountId and next.folderId here) + this.$router.push({ + name: 'message', + params: { + accountId: this.$route.params.accountId, + folderId: this.$route.params.folderId, + messageUid: next.uid, + }, + }) + }, + onScroll(e, p) { + if (p.scrollTop === 0 && !this.refreshing) { + return this.sync() + } + }, + handleShortcut(e) { + const envelopes = this.envelopes + const currentUid = this.$route.params.messageUid - if (!currentUid) { - console.debug('ignoring shortcut: no envelope selected') - return - } + if (!currentUid) { + console.debug('ignoring shortcut: no envelope selected') + return + } - const current = envelopes.filter(e => e.uid == currentUid) - if (current.length === 0) { - console.debug('ignoring shortcut: currently displayed messages is not in current envelope list') - return - } + const current = envelopes.filter(e => e.uid == currentUid) + if (current.length === 0) { + console.debug('ignoring shortcut: currently displayed messages is not in current envelope list') + return + } - const env = current[0] - const idx = envelopes.indexOf(env) + const env = current[0] + const idx = envelopes.indexOf(env) - switch (e.srcKey) { - case 'next': - case 'prev': - let next - if (e.srcKey === 'next') { - next = envelopes[idx + 1] - } else { - next = envelopes[idx - 1] - } + switch (e.srcKey) { + case 'next': + case 'prev': + let next + if (e.srcKey === 'next') { + next = envelopes[idx + 1] + } else { + next = envelopes[idx - 1] + } - if (!next) { - console.debug('ignoring shortcut: head or tail of envelope list reached', envelopes, idx, e.srcKey) - return - } + if (!next) { + console.debug( + 'ignoring shortcut: head or tail of envelope list reached', + envelopes, + idx, + e.srcKey + ) + return + } - // Keep the selected account-folder combination, but navigate to a different message - // (it's not a bug that we don't use next.accountId and next.folderId here) - this.$router.push({ - name: 'message', - params: { - accountId: this.$route.params.accountId, - folderId: this.$route.params.folderId, - messageUid: next.uid, - } - }) - break - case 'del': - console.debug('deleting', env) - this.$store.dispatch('deleteMessage', env) - .catch(console.error.bind(this)) + // Keep the selected account-folder combination, but navigate to a different message + // (it's not a bug that we don't use next.accountId and next.folderId here) + this.$router.push({ + name: 'message', + params: { + accountId: this.$route.params.accountId, + folderId: this.$route.params.folderId, + messageUid: next.uid, + }, + }) + break + case 'del': + console.debug('deleting', env) + this.$store.dispatch('deleteMessage', env).catch(console.error.bind(this)) - break - case 'flag': - console.debug('flagging envelope via shortkey', env) - this.$store.dispatch('toggleEnvelopeFlagged', env) - .catch(console.error.bind(this)) - break - case 'refresh': - console.debug('syncing envelopes via shortkey') - if (!this.refreshing) { - this.sync() - } + break + case 'flag': + console.debug('flagging envelope via shortkey', env) + this.$store.dispatch('toggleEnvelopeFlagged', env).catch(console.error.bind(this)) + break + case 'refresh': + console.debug('syncing envelopes via shortkey') + if (!this.refreshing) { + this.sync() + } - break - case 'unseen': - console.debug('marking message as seen/unseen via shortkey', env) - this.$store.dispatch('toggleEnvelopeSeen', env) - .catch(console.error.bind(this)) - break - default: - console.warn('shortcut ' + e.srcKey + ' is unknown. ignoring.') - } + break + case 'unseen': + console.debug('marking message as seen/unseen via shortkey', env) + this.$store.dispatch('toggleEnvelopeSeen', env).catch(console.error.bind(this)) + break + default: + console.warn('shortcut ' + e.srcKey + ' is unknown. ignoring.') } - } - } + }, + }, +} </script> <style scoped> - #load-more-mail-messages { - margin: 10px auto; - padding: 10px; - margin-top: 50px; - margin-bottom: 200px; - } +#load-more-mail-messages { + margin: 10px auto; + padding: 10px; + margin-top: 50px; + margin-bottom: 200px; +} - /* TODO: put this in core icons.css as general rule for buttons with icons */ - #load-more-mail-messages.icon-loading-small { - padding-left: 32px; - background-position: 9px center; - } +/* TODO: put this in core icons.css as general rule for buttons with icons */ +#load-more-mail-messages.icon-loading-small { + padding-left: 32px; + background-position: 9px center; +} - #list-refreshing { - overflow-y: hidden; - min-height: 0; +#list-refreshing { + overflow-y: hidden; + min-height: 0; - transition-property: all; - transition-duration: .5s; - transition-timing-function: ease-in; - } + transition-property: all; + transition-duration: 0.5s; + transition-timing-function: ease-in; +} - #list-refreshing.refreshing { - min-height: 32px; - } +#list-refreshing.refreshing { + min-height: 32px; +} - .list-enter-active, .list-leave-active { - transition: all 1s; - } +.list-enter-active, +.list-leave-active { + transition: all 1s; +} - .list-enter, .list-leave-to { - opacity: 0; - height: 0px; - transform: scaleY(0); - } +.list-enter, +.list-leave-to { + opacity: 0; + height: 0px; + transform: scaleY(0); +} </style> diff --git a/src/components/Error.vue b/src/components/Error.vue index 7ef8645ee..e603b5d8a 100644 --- a/src/components/Error.vue +++ b/src/components/Error.vue @@ -24,36 +24,34 @@ <h2>{{ error }}</h2> <p>{{ message }}</p> <p v-if="data && data.debug"> - <a class="button" - :href="reportUrl" - target="_blank" - rel="noopener">{{ t('mail', 'Report this bug') }}</a> + <a class="button" :href="reportUrl" target="_blank" rel="noopener">{{ t('mail', 'Report this bug') }}</a> </p> </div> </template> <script> - import {getReportUrl} from '../util/CrashReport' +import {getReportUrl} from '../util/CrashReport' - export default { - name: 'Error', - props: { - error: { - type: String, - required: true, - }, - message: { - type: String, - required: true, - }, - data: { - type: Object, - } +export default { + name: 'Error', + props: { + error: { + type: String, + required: true, }, - computed: { - reportUrl() { - return getReportUrl(this.data) - } - } - } + message: { + type: String, + required: true, + }, + data: { + type: Object, + default: () => undefined, + }, + }, + computed: { + reportUrl() { + return getReportUrl(this.data) + }, + }, +} </script> diff --git a/src/components/FolderContent.vue b/src/components/FolderContent.vue index 27a09129e..ee5b57136 100644 --- a/src/components/FolderContent.vue +++ b/src/components/FolderContent.vue @@ -1,145 +1,131 @@ <template> <div id="app-content-wrapper"> - <Loading v-if="loading" - :hint="t('mail', 'Loading messages')"/> + <Loading v-if="loading" :hint="t('mail', 'Loading messages')" /> <template v-else> - <EnvelopeList :account="account" - :folder="folder" - :envelopes="envelopes" - :searchQuery="searchQuery"/> - <NewMessageDetail v-if="newMessage"/> - <Message v-else-if="hasMessages"/> + <EnvelopeList :account="account" :folder="folder" :envelopes="envelopes" :search-query="searchQuery" /> + <NewMessageDetail v-if="newMessage" /> + <Message v-else-if="hasMessages" /> </template> </div> </template> <script> - import _ from 'lodash'; +import _ from 'lodash' - import Message from "./Message"; - import EnvelopeList from "./EnvelopeList"; - import NewMessageDetail from "./NewMessageDetail"; - import Loading from "./Loading"; +import Message from './Message' +import EnvelopeList from './EnvelopeList' +import NewMessageDetail from './NewMessageDetail' +import Loading from './Loading' - export default { - name: "FolderContent", - components: { - Loading, - NewMessageDetail, - Message, - EnvelopeList, +export default { + name: 'FolderContent', + components: { + Loading, + NewMessageDetail, + Message, + EnvelopeList, + }, + props: { + account: { + type: Object, + required: true, }, - props: { - account: { - type: Object, - required: true, - }, - folder: { - type: Object, - required: true, - }, + folder: { + type: Object, + required: true, }, - data () { - return { - loading: true, - searchQuery: undefined, - alive: false, + }, + data() { + return { + loading: true, + searchQuery: undefined, + alive: false, + } + }, + computed: { + hasMessages() { + return this.$store.getters.getEnvelopes(this.account.id, this.folder.id).length > 0 + }, + newMessage() { + return this.$route.params.messageUid === 'new' + }, + envelopes() { + if (_.isUndefined(this.searchQuery)) { + return this.$store.getters.getEnvelopes(this.account.id, this.folder.id) + } else { + return this.$store.getters.getSearchEnvelopes(this.account.id, this.folder.id) } }, - computed: { - hasMessages () { - return this.$store.getters.getEnvelopes( - this.account.id, - this.folder.id, - ).length > 0 - }, - newMessage () { - return this.$route.params.messageUid === 'new' - }, - envelopes () { - if (_.isUndefined(this.searchQuery)) { - return this.$store.getters.getEnvelopes( - this.account.id, - this.folder.id, - ) - } else { - return this.$store.getters.getSearchEnvelopes( - this.account.id, - this.folder.id, - ) - } - + }, + watch: { + $route(to, from) { + if (to.name === 'folder') { + // Navigate (back) to the folder view -> (re)fetch data + this.fetchData() } }, - created () { - this.alive = true + }, + created() { + this.alive = true - new OCA.Search(this.searchProxy, this.clearSearchProxy) + new OCA.Search(this.searchProxy, this.clearSearchProxy) - this.fetchData() - }, - beforeDestroy () { - this.alive = false - }, - watch: { - '$route' (to, from) { - if (to.name === 'folder') { - // Navigate (back) to the folder view -> (re)fetch data - this.fetchData() - } - } - }, - methods: { - fetchData () { - this.loading = true + this.fetchData() + }, + beforeDestroy() { + this.alive = false + }, + methods: { + fetchData() { + this.loading = true - this.$store.dispatch( - 'fetchEnvelopes', { - accountId: this.account.id, - folderId: this.folder.id, - query: this.searchQuery, - }) - .then(() => { - const envelopes = this.envelopes - console.debug('envelopes fetched', envelopes) + this.$store + .dispatch('fetchEnvelopes', { + accountId: this.account.id, + folderId: this.folder.id, + query: this.searchQuery, + }) + .then(() => { + const envelopes = this.envelopes + console.debug('envelopes fetched', envelopes) - this.loading = false + this.loading = false - if (this.$route.name !== 'message' && envelopes.length > 0) { - // Show first message - let first = envelopes[0]; + if (this.$route.name !== 'message' && envelopes.length > 0) { + // Show first message + let first = envelopes[0] - // Keep the selected account-folder combination, but navigate to the message - // (it's not a bug that we don't use first.accountId and first.folderId here) - this.$router.replace({ - name: 'message', - params: { - accountId: this.account.id, - folderId: this.folder.id, - messageUid: first.uid, - } - }) - } - }); - }, - searchProxy (query) { - if (this.alive) { - this.search(query) - } - }, - clearSearchProxy () { - if (this.alive) { - this.clearSearch() - } - }, - search (query) { - this.searchQuery = query + // Keep the selected account-folder combination, but navigate to the message + // (it's not a bug that we don't use first.accountId and first.folderId here) + this.$router.replace({ + name: 'message', + params: { + accountId: this.account.id, + folderId: this.folder.id, + messageUid: first.uid, + }, + }) + } + }) + }, + searchProxy(query) { + if (this.alive) { + this.search(query) + } + }, + clearSearchProxy() { + if (this.alive) { + this.clearSearch() + } + }, + search(query) { + this.searchQuery = query - this.fetchData() - }, - clearSearch () { - this.searchQuery = undefined - }, - } - } + this.fetchData() + }, + clearSearch() { + this.searchQuery = undefined + }, + }, +} </script> diff --git a/src/components/Loading.vue b/src/components/Loading.vue index 82f1a8a7f..4547301f3 100644 --- a/src/components/Loading.vue +++ b/src/components/Loading.vue @@ -7,14 +7,15 @@ </template> <script> - export default { - name: "Loading", - props: { - hint: String - } - } +export default { + name: 'Loading', + props: { + hint: { + type: String, + default: () => undefined, + }, + }, +} </script> -<style scoped> - -</style>
\ No newline at end of file +<style scoped></style> diff --git a/src/components/Message.vue b/src/components/Message.vue index d2a313f58..742cba2da 100644 --- a/src/components/Message.vue +++ b/src/components/Message.vue @@ -1,265 +1,256 @@ <template> <div class="app-content-details"> - <Loading v-if="loading"/> - <Error v-else-if="!message" - :error="error && error.message ? error.message : t('mail', 'Not found')" - :message="errorMessage" - :data="error"> + <Loading v-if="loading" /> + <Error + v-else-if="!message" + :error="error && error.message ? error.message : t('mail', 'Not found')" + :message="errorMessage" + :data="error" + > </Error> <template v-else> <div id="mail-message-header" class="section"> - <h2 :title="message.subject">{{message.subject}}</h2> + <h2 :title="message.subject">{{ message.subject }}</h2> <p class="transparency"> - <AddressList :entries="message.from"/> - to <!-- TODO: translate --> - <AddressList :entries="message.to"/> + <AddressList :entries="message.from" /> + to + <!-- TODO: translate --> + <AddressList :entries="message.to" /> <template v-if="message.cc.length"> - (cc <!-- TODO: translate --> - <AddressList :entries="message.cc"/><!-- + (cc + <!-- TODO: translate --> + <AddressList :entries="message.cc" /><!-- -->) </template> </p> </div> <div class="mail-message-body"> - <MessageHTMLBody v-if="message.hasHtmlBody" - :url="htmlUrl" - @loaded="onHtmlBodyLoaded"/> - <MessagePlainTextBody v-else - :body="message.body" - :signature="message.signature"/> + <MessageHTMLBody v-if="message.hasHtmlBody" :url="htmlUrl" @loaded="onHtmlBodyLoaded" /> + <MessagePlainTextBody v-else :body="message.body" :signature="message.signature" /> <MessageAttachments :attachments="message.attachments" /> <div id="reply-composer"></div> - <input type="button" - id="forward-button" - value="Forward" - @click="forwardMessage"> + <input id="forward-button" type="button" value="Forward" @click="forwardMessage" /> </div> - <Composer v-if="!message.hasHtmlBody || htmlBodyLoaded" - :fromAccount="message.accountId" - :to="replyRecipient.to" - :cc="replyRecipient.cc" - :subject="replySubject" - :body="replyBody" - :replyTo="replyTo" - :send="sendReply" - :draft="saveReplyDraft"/> + <Composer + v-if="!message.hasHtmlBody || htmlBodyLoaded" + :from-account="message.accountId" + :to="replyRecipient.to" + :cc="replyRecipient.cc" + :subject="replySubject" + :body="replyBody" + :reply-to="replyTo" + :send="sendReply" + :draft="saveReplyDraft" + /> </template> </div> </template> <script> - import { generateUrl } from 'nextcloud-server/dist/router' +import {generateUrl} from 'nextcloud-server/dist/router' - import AddressList from './AddressList' - import { - buildReplyBody, - buildRecipients as buildReplyRecipients, - buildReplySubject, - } from '../ReplyBuilder' - import Composer from './Composer' - import Error from './Error' - import {getRandomMessageErrorMessage} from '../util/ErrorMessageFactory' - import {htmlToText} from '../util/HtmlHelper' - import MessageHTMLBody from './MessageHTMLBody' - import MessagePlainTextBody from './MessagePlainTextBody' - import Loading from './Loading' - import MessageAttachments from './MessageAttachments' - import {saveDraft, sendMessage} from '../service/MessageService' +import AddressList from './AddressList' +import {buildReplyBody, buildRecipients as buildReplyRecipients, buildReplySubject} from '../ReplyBuilder' +import Composer from './Composer' +import Error from './Error' +import {getRandomMessageErrorMessage} from '../util/ErrorMessageFactory' +import {htmlToText} from '../util/HtmlHelper' +import MessageHTMLBody from './MessageHTMLBody' +import MessagePlainTextBody from './MessagePlainTextBody' +import Loading from './Loading' +import MessageAttachments from './MessageAttachments' +import {saveDraft, sendMessage} from '../service/MessageService' - export default { - name: 'Message', - components: { - AddressList, - Composer, - Error, - Loading, - MessageAttachments, - MessageHTMLBody, - MessagePlainTextBody, +export default { + name: 'Message', + components: { + AddressList, + Composer, + Error, + Loading, + MessageAttachments, + MessageHTMLBody, + MessagePlainTextBody, + }, + data() { + return { + loading: true, + message: undefined, + errorMessage: '', + error: undefined, + htmlBodyLoaded: false, + replyRecipient: {}, + replySubject: '', + replyBody: '', + } + }, + computed: { + htmlUrl() { + return generateUrl('/apps/mail/api/accounts/{accountId}/folders/{folderId}/messages/{id}/html', { + accountId: this.message.accountId, + folderId: this.message.folderId, + id: this.message.id, + }) }, - data () { + replyTo() { return { - loading: true, - message: undefined, - errorMessage: '', - error: undefined, - htmlBodyLoaded: false, - replyRecipient: {}, - replySubject: '', - replyBody: '', - } - }, - computed: { - htmlUrl () { - return generateUrl('/apps/mail/api/accounts/{accountId}/folders/{folderId}/messages/{id}/html', { - accountId: this.message.accountId, - folderId: this.message.folderId, - id: this.message.id - }) - }, - replyTo () { - return { - accountId: this.message.accountId, - folderId: this.message.folderId, - messageId: this.message.id, - } + accountId: this.message.accountId, + folderId: this.message.folderId, + messageId: this.message.id, } }, - created () { + }, + watch: { + $route(to, from) { this.fetchMessage() }, - watch: { - '$route' (to, from) { - this.fetchMessage() - } - }, - methods: { - fetchMessage () { - this.loading = true - this.message = undefined - this.errorMessage = '' - this.error = undefined - this.replyRecipient = {} - this.replySubject = '' - this.replyBody = '' - this.htmlBodyLoaded = false + }, + created() { + this.fetchMessage() + }, + methods: { + fetchMessage() { + this.loading = true + this.message = undefined + this.errorMessage = '' + this.error = undefined + this.replyRecipient = {} + this.replySubject = '' + this.replyBody = '' + this.htmlBodyLoaded = false - const messageUid = this.$route.params.messageUid + const messageUid = this.$route.params.messageUid - this.$store.dispatch('fetchMessage', messageUid) - .then(message => { - // TODO: add timeout so that message isn't flagged when only viewed - // for a few seconds - if (message.uid !== this.$route.params.messageUid) { - console.debug('User navigated away, loaded message won\'t be shown nor flagged as seen') - return - } - - this.message = message + this.$store + .dispatch('fetchMessage', messageUid) + .then(message => { + // TODO: add timeout so that message isn't flagged when only viewed + // for a few seconds + if (message.uid !== this.$route.params.messageUid) { + console.debug("User navigated away, loaded message won't be shown nor flagged as seen") + return + } - if (_.isUndefined(this.message)) { - console.info('message could not be found', messageUid) - this.errorMessage = getRandomMessageErrorMessage() - this.loading = false - return - } + this.message = message - const account = this.$store.getters.getAccount(message.accountId) - this.replyRecipient = buildReplyRecipients(message, { - label: account.name, - email: account.emailAddress, - }) - this.replySubject = buildReplySubject(message.subject) + if (_.isUndefined(this.message)) { + console.info('message could not be found', messageUid) + this.errorMessage = getRandomMessageErrorMessage() + this.loading = false + return + } - if (!message.hasHtmlBody) { - this.setReplyText(message.body) - } + const account = this.$store.getters.getAccount(message.accountId) + this.replyRecipient = buildReplyRecipients(message, { + label: account.name, + email: account.emailAddress, + }) + this.replySubject = buildReplySubject(message.subject) - this.loading = false + if (!message.hasHtmlBody) { + this.setReplyText(message.body) + } - const envelope = this.$store.getters.getEnvelope(message.accountId, message.folderId, message.id); - if (!envelope.flags.unseen) { - // Already seen -> no change necessary - return - } + this.loading = false - return this.$store.dispatch('toggleEnvelopeSeen', envelope) - }) - .catch(err => { - console.error('could not load message ', messageUid, err) - if (err.isError) { - this.errorMessage = t('mail', 'Could not load your message') - this.error = err - this.loading = false - } - }) - }, - setReplyText (text) { - const bodyText = htmlToText(text) + const envelope = this.$store.getters.getEnvelope(message.accountId, message.folderId, message.id) + if (!envelope.flags.unseen) { + // Already seen -> no change necessary + return + } - this.$store.commit('setMessageBodyText', { - uid: this.message.uid, - bodyText, + return this.$store.dispatch('toggleEnvelopeSeen', envelope) }) - - this.replyBody = buildReplyBody( - this.message.bodyText, - this.message.from[0], - this.message.dateInt, - ) - }, - onHtmlBodyLoaded (bodyString) { - this.setReplyText(bodyString) - this.htmlBodyLoaded = true - }, - saveReplyDraft (data) { - return saveDraft(data.account, data) - .then(({uid}) => uid) - }, - sendReply (data) { - return sendMessage(data.account, data) - }, - forwardMessage () { - this.$router.push({ - name: 'message', - params: { - accountId: this.$route.params.accountId, - folderId: this.$route.params.folderId, - messageUid: 'new', - }, - query: { - uid: this.message.uid, + .catch(err => { + console.error('could not load message ', messageUid, err) + if (err.isError) { + this.errorMessage = t('mail', 'Could not load your message') + this.error = err + this.loading = false } - }); - } - } - } + }) + }, + setReplyText(text) { + const bodyText = htmlToText(text) + + this.$store.commit('setMessageBodyText', { + uid: this.message.uid, + bodyText, + }) + + this.replyBody = buildReplyBody(this.message.bodyText, this.message.from[0], this.message.dateInt) + }, + onHtmlBodyLoaded(bodyString) { + this.setReplyText(bodyString) + this.htmlBodyLoaded = true + }, + saveReplyDraft(data) { + return saveDraft(data.account, data).then(({uid}) => uid) + }, + sendReply(data) { + return sendMessage(data.account, data) + }, + forwardMessage() { + this.$router.push({ + name: 'message', + params: { + accountId: this.$route.params.accountId, + folderId: this.$route.params.folderId, + messageUid: 'new', + }, + query: { + uid: this.message.uid, + }, + }) + }, + }, +} </script> <style> - .mail-message-body { - margin-bottom: 100px; - } +.mail-message-body { + margin-bottom: 100px; +} - #mail-message-header h2, - #mail-message-header p { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding-bottom: 7px; - margin-bottom: 0; - } +#mail-message-header h2, +#mail-message-header p { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-bottom: 7px; + margin-bottom: 0; +} - #mail-content, - .mail-message-attachments { - margin: 10px 10px 50px 30px; - } +#mail-content, +.mail-message-attachments { + margin: 10px 10px 50px 30px; +} - .mail-message-attachments { - margin-top: 10px; - } +.mail-message-attachments { + margin-top: 10px; +} - #mail-content iframe { - width: 100%; - } +#mail-content iframe { + width: 100%; +} - #show-images-text { - display: none; - } +#show-images-text { + display: none; +} - #mail-content a, - .mail-signature a { - color: #07d; - border-bottom: 1px dotted #07d; - text-decoration: none; - word-wrap: break-word; - } +#mail-content a, +.mail-signature a { + color: #07d; + border-bottom: 1px dotted #07d; + text-decoration: none; + word-wrap: break-word; +} - #mail-message-header .transparency { - opacity: .6; - } +#mail-message-header .transparency { + opacity: 0.6; +} - #mail-message-header .transparency a { - font-weight: bold; - } +#mail-message-header .transparency a { + font-weight: bold; +} </style> diff --git a/src/components/MessageAttachment.vue b/src/components/MessageAttachment.vue index 03c357937..227a771b4 100644 --- a/src/components/MessageAttachment.vue +++ b/src/components/MessageAttachment.vue @@ -20,241 +20,251 @@ --> <template> - <div class="attachment" - v-on:click="download"> - <img v-if="isImage" - class="mail-attached-image" - :src="url"> - <img class="attachment-icon" - :src="mimeUrl"/> - <span class="attachment-name" - :title="label">{{fileName}} - <span class="attachment-size">({{humanReadable(size)}})</span> + <div class="attachment" @click="download"> + <img v-if="isImage" class="mail-attached-image" :src="url" /> + <img class="attachment-icon" :src="mimeUrl" /> + <span class="attachment-name" :title="label" + >{{ fileName }} + <span class="attachment-size">({{ humanReadable(size) }})</span> </span> - <button v-if="isCalendarEvent" - class="button attachment-import calendar" - :class="{'icon-add' : !loadingCalendars, 'icon-loading-small': loadingCalendars}" - :disabled="loadingCalendars" - v-on:click.stop="loadCalendars" - :title="t('mail', 'Import into calendar')"></button> - <button class="button icon-download attachment-download" - :title="t('mail', 'Download attachment')"></button> - <button class="attachment-save-to-cloud" - :class="{'icon-folder' : !savingToCloud, 'icon-loading-small': savingToCloud}" - :disabled="savingToCloud" - v-on:click.stop="saveToCloud" - :title="t('mail', 'Save to Files')"></button> - <div class="popovermenu bubble attachment-import-popover hidden" - :class="{open: showCalendarPopover}" - v-on-click-outside="closeCalendarPopover"> - <PopoverMenu :menu="calendarMenuEntries"/> + <button + v-if="isCalendarEvent" + class="button attachment-import calendar" + :class="{'icon-add': !loadingCalendars, 'icon-loading-small': loadingCalendars}" + :disabled="loadingCalendars" + :title="t('mail', 'Import into calendar')" + @click.stop="loadCalendars" + ></button> + <button class="button icon-download attachment-download" :title="t('mail', 'Download attachment')"></button> + <button + class="attachment-save-to-cloud" + :class="{'icon-folder': !savingToCloud, 'icon-loading-small': savingToCloud}" + :disabled="savingToCloud" + :title="t('mail', 'Save to Files')" + @click.stop="saveToCloud" + ></button> + <div + v-on-click-outside="closeCalendarPopover" + class="popovermenu bubble attachment-import-popover hidden" + :class="{open: showCalendarPopover}" + > + <PopoverMenu :menu="calendarMenuEntries" /> </div> </div> </template> <script> - import {formatFileSize} from 'nextcloud-server/dist/format' - import {mixin as onClickOutside} from 'vue-on-click-outside' - import {pickFileOrDirectory} from 'nextcloud-server/dist/files' - import {PopoverMenu} from 'nextcloud-vue' +import {formatFileSize} from 'nextcloud-server/dist/format' +import {mixin as onClickOutside} from 'vue-on-click-outside' +import {pickFileOrDirectory} from 'nextcloud-server/dist/files' +import {PopoverMenu} from 'nextcloud-vue' - import {parseUid} from '../util/EnvelopeUidParser' +import {parseUid} from '../util/EnvelopeUidParser' - import { - downloadAttachment, - saveAttachmentToFiles - } from '../service/AttachmentService' - import {getUserCalendars, importCalendarEvent} from '../service/DAVService' +import {downloadAttachment, saveAttachmentToFiles} from '../service/AttachmentService' +import {getUserCalendars, importCalendarEvent} from '../service/DAVService' - export default { - name: "MessageAttachment", - components: { - PopoverMenu +export default { + name: 'MessageAttachment', + components: { + PopoverMenu, + }, + mixins: [onClickOutside], + props: { + id: { + type: Number, + required: true, }, - mixins: [ - onClickOutside - ], - props: { - id: Number, - fileName: String, - url: String, - size: Number, - mimeUrl: String, - isImage: Boolean, - isCalendarEvent: Boolean, + fileName: { + type: String, + required: true, }, - data () { - return { - savingToCloud: false, - loadingCalendars: false, - calendars: [], - showCalendarPopover: false, - } + url: { + type: String, + required: true, }, - computed: { - label () { - return this.fileName + " (" + formatFileSize(this.size) + ")" - }, - calendarMenuEntries () { - return this.calendars.map(cal => { - return { - icon: 'icon-add', - text: cal.displayname, - action: this.importCalendar(cal.url), - } - }) - } + size: { + type: Number, + required: true, + }, + mimeUrl: { + type: String, + required: true, + }, + isImage: { + type: Boolean, + default: false, }, - methods: { - humanReadable (size) { - return formatFileSize(size) - }, - saveToCloud () { - const saveAttachment = (accountId, folderId, messageId, attachmentId) => directory => { - return saveAttachmentToFiles( - accountId, - folderId, - messageId, - attachmentId, - directory - ) + isCalendarEvent: { + type: Boolean, + default: false, + }, + }, + data() { + return { + savingToCloud: false, + loadingCalendars: false, + calendars: [], + showCalendarPopover: false, + } + }, + computed: { + label() { + return this.fileName + ' (' + formatFileSize(this.size) + ')' + }, + calendarMenuEntries() { + return this.calendars.map(cal => { + return { + icon: 'icon-add', + text: cal.displayname, + action: this.importCalendar(cal.url), } - const {accountId, folderId, id} = parseUid(this.$route.params.messageUid) + }) + }, + }, + methods: { + humanReadable(size) { + return formatFileSize(size) + }, + saveToCloud() { + const saveAttachment = (accountId, folderId, messageId, attachmentId) => directory => { + return saveAttachmentToFiles(accountId, folderId, messageId, attachmentId, directory) + } + const {accountId, folderId, id} = parseUid(this.$route.params.messageUid) - return pickFileOrDirectory( - t('mail', 'Choose a folder to store the attachment in'), - false, - ['httpd/unix-directory'], - true - ) - .then(dest => { - this.savingToCloud = true - return dest - }) - .then(saveAttachment(accountId, folderId, id, this.id)) - .then(() => console.info('saved')) - .catch(e => console.error('not saved', e)) - .then(() => this.savingToCloud = false) - }, - download () { - window.open(this.url) - window.focus(); - }, - loadCalendars () { - this.loadingCalendars = true - getUserCalendars() - .then(calendars => { - this.calendars = calendars - this.showCalendarPopover = true - this.loadingCalendars = false - }) - }, - closeCalendarPopover () { - this.showCalendarPopover = false - }, - importCalendar (url) { - return () => { - downloadAttachment(this.url) - .then(importCalendarEvent(url)) - .then(() => console.info('calendar imported')) - .catch(e => console.error('import error', e)) - .then(() => this.showCalendarPopover = false) - } + return pickFileOrDirectory( + t('mail', 'Choose a folder to store the attachment in'), + false, + ['httpd/unix-directory'], + true + ) + .then(dest => { + this.savingToCloud = true + return dest + }) + .then(saveAttachment(accountId, folderId, id, this.id)) + .then(() => console.info('saved')) + .catch(e => console.error('not saved', e)) + .then(() => (this.savingToCloud = false)) + }, + download() { + window.open(this.url) + window.focus() + }, + loadCalendars() { + this.loadingCalendars = true + getUserCalendars().then(calendars => { + this.calendars = calendars + this.showCalendarPopover = true + this.loadingCalendars = false + }) + }, + closeCalendarPopover() { + this.showCalendarPopover = false + }, + importCalendar(url) { + return () => { + downloadAttachment(this.url) + .then(importCalendarEvent(url)) + .then(() => console.info('calendar imported')) + .catch(e => console.error('import error', e)) + .then(() => (this.showCalendarPopover = false)) } - } - } + }, + }, +} </script> <style scoped> - .attachment { - position: relative; - display: inline-block; - border: 1px solid var(--color-border); - border-radius: 3px; - margin: 0 10px 10px 0; - padding: 5px; - } +.attachment { + position: relative; + display: inline-block; + border: 1px solid var(--color-border); + border-radius: 3px; + margin: 0 10px 10px 0; + padding: 5px; +} - .attachment:hover, - .attachment span:hover { - background-color: var(--color-background-dark); - cursor: pointer; - } +.attachment:hover, +.attachment span:hover { + background-color: var(--color-background-dark); + cursor: pointer; +} - @media only screen and (max-width: 768px) { - .attachment { - width: calc(100% - 5px); - } +@media only screen and (max-width: 768px) { + .attachment { + width: calc(100% - 5px); } +} - @media only screen and (min-width: 769px) and (max-width: 1400px) { - .attachment { - width: calc(50% - 10px); - } +@media only screen and (min-width: 769px) and (max-width: 1400px) { + .attachment { + width: calc(50% - 10px); } +} - @media only screen and (min-width: 1401px) { - .attachment { - width: calc(33% - 12px); - } +@media only screen and (min-width: 1401px) { + .attachment { + width: calc(33% - 12px); } +} - .mail-attached-image { - display: block; - max-width: 100%; - max-height: 120px; - } +.mail-attached-image { + display: block; + max-width: 100%; + max-height: 120px; +} - .attachment-save-to-cloud, - .attachment-download, - .attachment-import { - position: absolute; - height: 32px; - width: 32px; - margin: 0; - bottom: 0; - background-color: transparent; - border-color: transparent; - } +.attachment-save-to-cloud, +.attachment-download, +.attachment-import { + position: absolute; + height: 32px; + width: 32px; + margin: 0; + bottom: 0; + background-color: transparent; + border-color: transparent; +} - .attachment-save-to-cloud { - right: 0; - } +.attachment-save-to-cloud { + right: 0; +} - .attachment-download { - right: 32px; - opacity: .6; - } +.attachment-download { + right: 32px; + opacity: 0.6; +} - .attachment-import { - right: 64px; - } +.attachment-import { + right: 64px; +} - .attachment-import-popover { - right: 32px; - top: 42px; - } +.attachment-import-popover { + right: 32px; + top: 42px; +} - .attachment-import-popover::after { - right: 32px; - } +.attachment-import-popover::after { + right: 32px; +} - .attachment-name { - display: inline-block; - width: calc(100% - 108px); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: middle; - } +.attachment-name { + display: inline-block; + width: calc(100% - 108px); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; +} - /* show attachment size less prominent */ - .attachment-size { - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; - opacity: .5; - } +/* show attachment size less prominent */ +.attachment-size { + -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)'; + opacity: 0.5; +} - .attachment-icon { - vertical-align: middle; - } +.attachment-icon { + vertical-align: middle; +} </style> diff --git a/src/components/MessageAttachments.vue b/src/components/MessageAttachments.vue index eefb00c4f..248a605b3 100644 --- a/src/components/MessageAttachments.vue +++ b/src/components/MessageAttachments.vue @@ -22,21 +22,25 @@ <template> <div class="mail-message-attachments"> <div class="attachments"> - <MessageAttachment v-for="attachment in attachments" - :key="attachment.id" - :id="attachment.id" - :fileName="attachment.fileName" - :size="attachment.size" - :url="attachment.downloadUrl" - :isImage="attachment.isImage" - :isCalendarEvent="attachment.isCalendarEvent" - :mimeUrl="attachment.mimeUrl"/> + <MessageAttachment + v-for="attachment in attachments" + :id="attachment.id" + :key="attachment.id" + :file-name="attachment.fileName" + :size="attachment.size" + :url="attachment.downloadUrl" + :is-image="attachment.isImage" + :is-calendar-event="attachment.isCalendarEvent" + :mime-url="attachment.mimeUrl" + /> </div> <p v-if="moreThanOne"> - <button class="attachments-save-to-cloud" - :class="{'icon-folder' : !savingToCloud, 'icon-loading-small' : savingToCloud}" - :disabled="savingToCloud" - v-on:click="saveAll"> + <button + class="attachments-save-to-cloud" + :class="{'icon-folder': !savingToCloud, 'icon-loading-small': savingToCloud}" + :disabled="savingToCloud" + @click="saveAll" + > {{ t('mail', 'Save all to Files') }} </button> </p> @@ -44,74 +48,72 @@ </template> <script> - import MessageAttachment from './MessageAttachment' - import {parseUid} from '../util/EnvelopeUidParser' - import {saveAttachmentsToFiles} from '../service/AttachmentService' +import MessageAttachment from './MessageAttachment' +import {parseUid} from '../util/EnvelopeUidParser' +import {saveAttachmentsToFiles} from '../service/AttachmentService' - export default { - name: "MessageAttachments", - components: { - MessageAttachment +export default { + name: 'MessageAttachments', + components: { + MessageAttachment, + }, + props: { + attachments: { + type: Array, + required: true, }, - props: { - attachments: Array, + }, + data() { + return { + savingToCloud: false, + } + }, + computed: { + moreThanOne() { + return this.attachments.length > 1 }, - data () { - return { - savingToCloud: false, + }, + methods: { + saveAll() { + const pickDestination = () => { + return new Promise((res, rej) => { + OC.dialogs.filepicker( + t('mail', 'Choose a folder to store the attachments in'), + res, + false, + 'httpd/unix-directory', + true + ) + }) } - }, - computed: { - moreThanOne () { - return this.attachments.length > 1 + const saveAttachments = (accountId, folderId, messageId) => directory => { + return saveAttachmentsToFiles(accountId, folderId, messageId, directory) } - }, - methods: { - saveAll () { - const pickDestination = () => { - return new Promise((res, rej) => { - OC.dialogs.filepicker( - t('mail', 'Choose a folder to store the attachments in'), - res, - false, - 'httpd/unix-directory', - true - ) - }) - } - const saveAttachments = (accountId, folderId, messageId) => directory => { - return saveAttachmentsToFiles( - accountId, - folderId, - messageId, - directory - ) - } - const {accountId, folderId, id} = parseUid(this.$route.params.messageUid) + const {accountId, folderId, id} = parseUid(this.$route.params.messageUid) - return pickDestination() - .then(dest => { - this.savingToCloud = true - return dest - }) - .then(saveAttachments(accountId, folderId, id)) - .then(() => console.info('saved')) - .catch(e => console.error('not saved', e)) - .then(() => this.savingToCloud = false) - } - } - } + return pickDestination() + .then(dest => { + this.savingToCloud = true + return dest + }) + .then(saveAttachments(accountId, folderId, id)) + .then(() => console.info('saved')) + .catch(e => console.error('not saved', e)) + .then(() => (this.savingToCloud = false)) + }, + }, +} </script> <style> - .mail-message-attachments { - margin-bottom: 20px; - } +.mail-message-attachments { + margin-bottom: 20px; +} - /* show icon + text for Download all button +/* show icon + text for Download all button as well as when there is only one attachment */ - .attachments-save-to-cloud { - background-position: 9px center; - padding-left: 32px; - } +.attachments-save-to-cloud { + background-position: 9px center; + padding-left: 32px; +} </style> diff --git a/src/components/MessageHTMLBody.vue b/src/components/MessageHTMLBody.vue index b784597f2..6b5a0c33e 100644 --- a/src/components/MessageHTMLBody.vue +++ b/src/components/MessageHTMLBody.vue @@ -6,76 +6,71 @@ {{ t('mail', 'Show images from this sender') }} </button> </div> - <div v-if="loading" - class="icon-loading"/> - <div :class="{hidden: loading}" - id="message-container"> - <iframe id="message-frame" - ref="iframe" - @load="onMessageFrameLoad" - :src="url" - seamless/> + <div v-if="loading" class="icon-loading" /> + <div id="message-container" :class="{hidden: loading}"> + <iframe id="message-frame" ref="iframe" :src="url" seamless @load="onMessageFrameLoad" /> </div> </div> </template> <script> - export default { - name: "MessageHTMLBody", - props: { - url: { - type: String, - required: true, - }, +export default { + name: 'MessageHTMLBody', + props: { + url: { + type: String, + required: true, }, - data () { - return { - loading: true, - hasBlockedContent: false, - } + }, + data() { + return { + loading: true, + hasBlockedContent: false, + } + }, + methods: { + getIframeDoc() { + const iframe = this.$refs.iframe + return iframe.contentDocument || iframe.contentWindow.document }, - methods: { - getIframeDoc () { - const iframe = this.$refs.iframe - return iframe.contentDocument || iframe.contentWindow.document - }, - onMessageFrameLoad () { - const iframeDoc = this.getIframeDoc() - const iframeBody = iframeDoc.querySelectorAll('body')[0] - this.hasBlockedContent = iframeDoc.querySelectorAll('[data-original-src]').length > 0 - || iframeDoc.querySelectorAll('[data-original-style]').length > 0 + onMessageFrameLoad() { + const iframeDoc = this.getIframeDoc() + const iframeBody = iframeDoc.querySelectorAll('body')[0] + this.hasBlockedContent = + iframeDoc.querySelectorAll('[data-original-src]').length > 0 || + iframeDoc.querySelectorAll('[data-original-style]').length > 0 - this.$emit('loaded', iframeBody.outerHTML) - this.loading = false - }, - onShowBlockedContent () { - const iframeDoc = this.getIframeDoc() - iframeDoc.querySelectorAll('[data-original-src]').forEach( - node => node.setAttribute('src', node.getAttribute('data-original-src')) - ) - iframeDoc.querySelectorAll('[data-original-style]').forEach(node => - node.setAttribute('style', node.getAttribute('data-original-style')) - ) + this.$emit('loaded', iframeBody.outerHTML) + this.loading = false + }, + onShowBlockedContent() { + const iframeDoc = this.getIframeDoc() + iframeDoc + .querySelectorAll('[data-original-src]') + .forEach(node => node.setAttribute('src', node.getAttribute('data-original-src'))) + iframeDoc + .querySelectorAll('[data-original-style]') + .forEach(node => node.setAttribute('style', node.getAttribute('data-original-style'))) - this.hasBlockedContent = false - } - } - }; + this.hasBlockedContent = false + }, + }, +} </script> <style scoped> - #message-container { - position: relative; - width: 100%; - height: 0; - padding-bottom: 56.25%; - } +#message-container { + position: relative; + width: 100%; + height: 0; + padding-bottom: 56.25%; +} - #message-frame { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } +#message-frame { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} </style> diff --git a/src/components/MessagePlainTextBody.vue b/src/components/MessagePlainTextBody.vue index f1337e9a4..738e544ef 100644 --- a/src/components/MessagePlainTextBody.vue +++ b/src/components/MessagePlainTextBody.vue @@ -1,26 +1,30 @@ <template> <div> - <div id="mail-content" v-html="body" /> - <div v-if="signature" - class="mail-signature" - v-html="signature" /> + <div id="mail-content" v-html="body"></div> + <div v-if="signature" class="mail-signature" v-html="signature"></div> </div> </template> <script> - export default { - name: "MessagePlainTextBody", - props: [ - 'body', - 'signature', - ] - }; +export default { + name: 'MessagePlainTextBody', + props: { + body: { + type: String, + required: true, + }, + signature: { + type: String, + default: () => undefined, + }, + }, +} </script> <style scoped> - .mail-signature { - font-family: monospace; - opacity: 0.5; - line-height: initial; - } +.mail-signature { + font-family: monospace; + opacity: 0.5; + line-height: initial; +} </style> diff --git a/src/components/Moment.vue b/src/components/Moment.vue index 358212c2a..8b6f80fa9 100644 --- a/src/components/Moment.vue +++ b/src/components/Moment.vue @@ -1,30 +1,34 @@ <template> - <span class="live-relative-timestamp" - :data-timestamp="timestamp * 1000" - :title="title">{{ formatted }}</span> + <span class="live-relative-timestamp" :data-timestamp="timestamp * 1000" :title="title">{{ formatted }}</span> </template> <script> - import { getLocale } from 'nextcloud-server/dist/l10n' - import moment from 'moment'; +import {getLocale} from 'nextcloud-server/dist/l10n' +import moment from 'moment' - if (typeof OC !== 'undefined') { - moment.locale(getLocale()); - } +if (typeof OC !== 'undefined') { + moment.locale(getLocale()) +} - export default { - name: "Moment", - props: [ - 'timestamp', - 'format' - ], - computed: { - title () { - return moment.unix(this.timestamp).format(this.format || 'LLL'); - }, - formatted () { - return moment.unix(this.timestamp).fromNow(); - } - } - } +export default { + name: 'Moment', + props: { + timestamp: { + type: Number, + required: true, + }, + format: { + type: String, + default: 'LLL', + }, + }, + computed: { + title() { + return moment.unix(this.timestamp).format(this.format) + }, + formatted() { + return moment.unix(this.timestamp).fromNow() + }, + }, +} </script> diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue index 6ba82d12e..a5b791942 100644 --- a/src/components/Navigation.vue +++ b/src/components/Navigation.vue @@ -21,161 +21,159 @@ <template> <AppNavigation> - <AppNavigationNew :text="t('mail', 'New message')" - buttonId="mail_new_message" - buttonClass="icon-add" - @click="onNewMessage"/> + <AppNavigationNew + :text="t('mail', 'New message')" + button-id="mail_new_message" + button-class="icon-add" + @click="onNewMessage" + /> <ul id="accounts-list"> <template v-for="group in menu"> - <AppNavigationItem v-for="item in group" - :key="item.key" - :item="item"/> - <AppNavigationSpacer /> + <AppNavigationItem v-for="item in group.items" :key="item.key" :item="item" /> + <AppNavigationSpacer :key="group.id" /> </template> </ul> <AppNavigationSettings :title="t('mail', 'Settings')"> - <AppSettingsMenu/> + <AppSettingsMenu /> </AppNavigationSettings> </AppNavigation> </template> <script> - import {translate as t} from 'nextcloud-server/dist/l10n' - import { - AppContent, +import {translate as t} from 'nextcloud-server/dist/l10n' +import { + AppContent, + AppNavigation, + AppNavigationItem, + AppNavigationNew, + AppNavigationSettings, + AppNavigationSpacer, +} from 'nextcloud-vue' + +import {calculateAccountColor} from '../util/AccountColor' + +const SHOW_COLLAPSED = Object.seal(['inbox', 'flagged', 'drafts', 'sent']) + +import AppSettingsMenu from '../components/AppSettingsMenu' + +export default { + name: 'Navigation', + components: { AppNavigation, AppNavigationItem, AppNavigationNew, AppNavigationSettings, - AppNavigationSpacer - } from 'nextcloud-vue' - - import {calculateAccountColor} from '../util/AccountColor' + AppNavigationSpacer, + AppSettingsMenu, + }, + computed: { + menu() { + return this.$store.getters.getAccounts().map(account => { + const items = [] - const SHOW_COLLAPSED = Object.seal([ - 'inbox', - 'flagged', - 'drafts', - 'sent' - ]); + const isError = account.error - import AppSettingsMenu from '../components/AppSettingsMenu' - - export default { - name: "Navigation", - components: { - AppNavigation, - AppNavigationItem, - AppNavigationNew, - AppNavigationSettings, - AppNavigationSpacer, - AppSettingsMenu, - }, - computed: { - menu() { - return this.$store.getters.getAccounts().map(account => { - const items = [] - - const isError = account.error + if (account.isUnified !== true && account.visible !== false) { + const route = { + name: 'accountSettings', + params: { + accountId: account.id, + }, + } - if (account.isUnified !== true && account.visible !== false) { - items.push({ - id: 'account' + account.id, - key: 'account' + account.id, - text: account.emailAddress, - bullet: isError ? undefined : calculateAccountColor(account.name), // TODO - icon: isError ? 'icon-error' : undefined, - router: { - name: 'accountSettings', - params: { - accountId: account.id, - } - }, - utils: { - actions: [ - { - icon: 'icon-settings', - text: t('mail', 'Edit'), - action: () => { - this.$router.push(route) - }, + items.push({ + id: 'account' + account.id, + key: 'account' + account.id, + text: account.emailAddress, + bullet: isError ? undefined : calculateAccountColor(account.name), // TODO + icon: isError ? 'icon-error' : undefined, + router: route, + utils: { + actions: [ + { + icon: 'icon-settings', + text: t('mail', 'Edit'), + action: () => { + this.$router.push(route) // eslint-disable-line }, - { - icon: 'icon-delete', - text: t('mail', 'Delete'), - action: () => { - this.$store.dispatch('deleteAccount', account) - .catch(console.error.bind(this)) - } - } - ] - } - }) - } + }, + { + icon: 'icon-delete', + text: t('mail', 'Delete'), + action: () => { + this.$store.dispatch('deleteAccount', account).catch(console.error.bind(this)) + }, + }, + ], + }, + }) + } - const folderToEntry = folder => { - let icon = 'folder'; - if (folder.specialRole) { - icon = folder.specialRole; - } + const folderToEntry = folder => { + let icon = 'folder' + if (folder.specialRole) { + icon = folder.specialRole + } - return { - id: 'account' + account.id + '_' + folder.id, - key: 'account' + account.id + '_' + folder.id, - text: folder.name, - icon: 'icon-' + icon, - router: { - name: 'folder', - params: { - accountId: account.id, - folderId: folder.id, - }, - exact: false, - }, - utils: { - counter: folder.unread, + return { + id: 'account' + account.id + '_' + folder.id, + key: 'account' + account.id + '_' + folder.id, + text: folder.name, + icon: 'icon-' + icon, + router: { + name: 'folder', + params: { + accountId: account.id, + folderId: folder.id, }, - collapsible: true, - opened: folder.opened, - children: folder.folders.map(folderToEntry) - } + exact: false, + }, + utils: { + counter: folder.unread, + }, + collapsible: true, + opened: folder.opened, + children: folder.folders.map(folderToEntry), } + } - this.$store.getters.getFolders(account.id) - .filter(folder => !account.collapsed || SHOW_COLLAPSED.indexOf(folder.specialRole) !== -1) - .map(folderToEntry) - .forEach(i => items.push(i)) + this.$store.getters + .getFolders(account.id) + .filter(folder => !account.collapsed || SHOW_COLLAPSED.indexOf(folder.specialRole) !== -1) + .map(folderToEntry) + .forEach(i => items.push(i)) - if (!account.isUnified && account.folders.length > 0) { - items.push({ - id: 'collapse-' + account.id, - key: 'collapse-' + account.id, - classes: ['collapse-folders'], - text: account.collapsed ? t('mail', 'Show all folders') : t('mail', 'Collapse folders'), - action: () => this.$store.commit('toggleAccountCollapsed', account.id) - }) - } + if (!account.isUnified && account.folders.length > 0) { + items.push({ + id: 'collapse-' + account.id, + key: 'collapse-' + account.id, + classes: ['collapse-folders'], + text: account.collapsed ? t('mail', 'Show all folders') : t('mail', 'Collapse folders'), + action: () => this.$store.commit('toggleAccountCollapsed', account.id), + }) + } - return items - }) - } + return { + id: account.id, + items, + } + }) }, - methods: { - onNewMessage () { - // FIXME: assumes that we're on the 'message' route already - this.$router.push({ - name: 'message', - params: { - accountId: this.$route.params.accountId, - folderId: this.$route.params.folderId, - messageUid: 'new', - } - }); - } + }, + methods: { + onNewMessage() { + // FIXME: assumes that we're on the 'message' route already + this.$router.push({ + name: 'message', + params: { + accountId: this.$route.params.accountId, + folderId: this.$route.params.folderId, + messageUid: 'new', + }, + }) }, - } + }, +} </script> -<style scoped> - -</style>
\ No newline at end of file +<style scoped></style> diff --git a/src/components/NewMessageDetail.vue b/src/components/NewMessageDetail.vue index cd4eba3a4..bf479ac8d 100644 --- a/src/components/NewMessageDetail.vue +++ b/src/components/NewMessageDetail.vue @@ -1,199 +1,200 @@ <template> <div class="app-content-details"> - <Loading v-if="loading"/> - <Error v-else-if="error" - :error="error && error.message ? error.message : t('mail', 'Not found')" - :message="errorMessage" - :data="error"> + <Loading v-if="loading" /> + <Error + v-else-if="error" + :error="error && error.message ? error.message : t('mail', 'Not found')" + :message="errorMessage" + :data="error" + > </Error> - <Composer v-else - :fromAccount="composerData.accountId" - :to="composerData.to" - :cc="composerData.cc" - :bcc="composerData.bcc" - :subject="composerData.subject" - :body="composerData.body" - :draft="saveDraft" - :send="sendMessage"/> + <Composer + v-else + :from-account="composerData.accountId" + :to="composerData.to" + :cc="composerData.cc" + :bcc="composerData.bcc" + :subject="composerData.subject" + :body="composerData.body" + :draft="saveDraft" + :send="sendMessage" + /> </div> </template> <script> - import _ from 'lodash' - - import {buildFowardSubject, buildReplyBody} from '../ReplyBuilder' - import Composer from './Composer' - import Error from './Error' - import Loading from './Loading' - import {saveDraft, sendMessage} from '../service/MessageService' - - export default { - name: 'NewMessageDetail', - components: { - Composer, - Error, - Loading, - }, - data() { - return { - loading: false, - draft: undefined, - errorMessage: '', - error: undefined, - } - }, - computed: { - composerData () { - if (!_.isUndefined(this.draft)) { - console.info('todo: handle draft data') - return { - to: this.draft.to, - cc: this.draft.cc, - bcc: this.draft.bcc, // TODO: impl in composer - subject: this.draft.subject, - body: this.draft.body, - } - } else if (!_.isUndefined(this.$route.query.uid)) { - // Forwarded message - - const message = this.$store.getters.getMessageByUid(this.$route.query.uid) - console.debug('forwaring message', message) - - return { - to: [], - cc: [], - subject: buildFowardSubject(message.subject), - body: buildReplyBody( - message.bodyText, - message.from[0], - message.dateInt, - ) - } - } else { - // New or mailto: message - - let accountId - // Only preselect an account when we're not in a unified mailbox - if (this.$route.params.accountId !== 0 - && this.$route.params.accountId !== '0') { - accountId = parseInt(this.$route.params.accountId, 10) - } +import _ from 'lodash' + +import {buildFowardSubject, buildReplyBody} from '../ReplyBuilder' +import Composer from './Composer' +import {getRandomMessageErrorMessage} from '../util/ErrorMessageFactory' +import Error from './Error' +import Loading from './Loading' +import {saveDraft, sendMessage} from '../service/MessageService' + +export default { + name: 'NewMessageDetail', + components: { + Composer, + Error, + Loading, + }, + data() { + return { + loading: false, + draft: undefined, + errorMessage: '', + error: undefined, + } + }, + computed: { + composerData() { + if (!_.isUndefined(this.draft)) { + console.info('todo: handle draft data') + return { + to: this.draft.to, + cc: this.draft.cc, + bcc: this.draft.bcc, // TODO: impl in composer + subject: this.draft.subject, + body: this.draft.body, + } + } else if (!_.isUndefined(this.$route.query.uid)) { + // Forwarded message - return { - accountId, - to: this.stringToRecipients(this.$route.query.to), - cc: this.stringToRecipients(this.$route.query.cc), - subject: this.$route.query.subject || '', - body: this.$route.query.body || '', - } + const message = this.$store.getters.getMessageByUid(this.$route.query.uid) + console.debug('forwaring message', message) + + return { + to: [], + cc: [], + subject: buildFowardSubject(message.subject), + body: buildReplyBody(message.bodyText, message.from[0], message.dateInt), + } + } else { + // New or mailto: message + + let accountId + // Only preselect an account when we're not in a unified mailbox + if (this.$route.params.accountId !== 0 && this.$route.params.accountId !== '0') { + accountId = parseInt(this.$route.params.accountId, 10) + } + + return { + accountId, + to: this.stringToRecipients(this.$route.query.to), + cc: this.stringToRecipients(this.$route.query.cc), + subject: this.$route.query.subject || '', + body: this.$route.query.body || '', } } }, - created () { + }, + watch: { + $route(to, from) { + // `saveDraft` replaced the current URL with the updated draft UID + // in that case we don't really start a new draft but just keep the + // URL consistent, hence not loading anything + if (this.draft && to.name === 'message' && to.params.draftUid === this.draft.uid) { + console.debug('detected navigation to current (new) draft UID, not reloading') + return + } + this.fetchMessage() }, - watch: { - '$route' (to, from) { - // `saveDraft` replaced the current URL with the updated draft UID - // in that case we don't really start a new draft but just keep the - // URL consistent, hence not loading anything - if (this.draft - && to.name === 'message' - && to.params.draftUid === this.draft.uid) { - console.debug('detected navigation to current (new) draft UID, not reloading') - return - } - - this.fetchMessage() + }, + created() { + this.fetchMessage() + }, + methods: { + stringToRecipients(str) { + if (_.isUndefined(str)) { + return [] } - }, - methods: { - stringToRecipients (str) { - if (_.isUndefined(str)) { - return [] - } - return [ - { - label: str, - email: str, - } - ] - }, - fetchMessage () { - this.draft = undefined - this.errorMessage = '' - this.error = undefined - - const draftUid = this.$route.params.draftUid - if (_.isUndefined(draftUid)) { - console.debug('not a draft, nothing to fetch') - // Nothing to fetch - return - } + return [ + { + label: str, + email: str, + }, + ] + }, + fetchMessage() { + this.draft = undefined + this.errorMessage = '' + this.error = undefined + + const draftUid = this.$route.params.draftUid + if (_.isUndefined(draftUid)) { + console.debug('not a draft, nothing to fetch') + // Nothing to fetch + return + } - this.loading = true + this.loading = true - this.$store.dispatch('fetchMessage', draftUid) - .then(draft => { - if (draft.uid !== this.$route.params.draftUid) { - console.debug('User navigated away, loaded draft won\'t be shown') - return - } + this.$store + .dispatch('fetchMessage', draftUid) + .then(draft => { + if (draft.uid !== this.$route.params.draftUid) { + console.debug("User navigated away, loaded draft won't be shown") + return + } - this.draft = draft + this.draft = draft - if (_.isUndefined(this.draft)) { - console.info('draft could not be found', draftUid) - this.errorMessage = getRandomMessageErrorMessage() - this.loading = false - return - } + if (_.isUndefined(this.draft)) { + console.info('draft could not be found', draftUid) + this.errorMessage = getRandomMessageErrorMessage() + this.loading = false + return + } + this.loading = false + }) + .catch(err => { + console.error('could not load draft ' + draftUid, err) + if (err.isError) { + this.errorMessage = t('mail', 'Could not load your draft') + this.error = err this.loading = false - }) - .catch(err => { - console.error('could not load draft ' + draftUid, err) - if (err.isError) { - this.errorMessage = t('mail', 'Could not load your draft') - this.error = err - this.loading = false - } - }) - }, - saveDraft (data) { - if (_.isUndefined(data.draftUID) && this.draft) { - console.debug('draft data does not have a draftUID, adding one') - data.draftUID = this.draft.id - } - return saveDraft(data.account, data) - .then(({uid}) => { - if (_.isUndefined(this.draft)) { - return uid - } - - console.info('replacing draft ' + this.draft.uid + ' with ' + uid) - const update = { - draft: this.draft, - uid, - data, - } - return this.$store.dispatch('replaceDraft', update) - .then(() => this.$router.replace({ - name: 'message', - params: { - accountId: this.$route.params.accountId, - folderId: this.$route.params.folderId, - messageUid: 'new', - draftUid: this.draft.uid, - } - })) - .then(() => uid) - }) - }, - sendMessage (data) { - return sendMessage(data.account, data) + } + }) + }, + saveDraft(data) { + if (_.isUndefined(data.draftUID) && this.draft) { + console.debug('draft data does not have a draftUID, adding one') + data.draftUID = this.draft.id } - } - } + return saveDraft(data.account, data).then(({uid}) => { + if (_.isUndefined(this.draft)) { + return uid + } + + console.info('replacing draft ' + this.draft.uid + ' with ' + uid) + const update = { + draft: this.draft, + uid, + data, + } + return this.$store + .dispatch('replaceDraft', update) + .then(() => + this.$router.replace({ + name: 'message', + params: { + accountId: this.$route.params.accountId, + folderId: this.$route.params.folderId, + messageUid: 'new', + draftUid: this.draft.uid, + }, + }) + ) + .then(() => uid) + }) + }, + sendMessage(data) { + return sendMessage(data.account, data) + }, + }, +} </script> |