From 679e96b8659bc18e5ec0d463531423060a118ffd Mon Sep 17 00:00:00 2001 From: "amirmohammd.ahmadi" Date: Sat, 27 Sep 2025 10:21:11 +0330 Subject: [PATCH 1/5] fix: RTL and Bidirectional Text Issues --- src/components/TextEditor.vue | 98 +++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/components/TextEditor.vue b/src/components/TextEditor.vue index c84c4ddf23..542abe5c4d 100644 --- a/src/components/TextEditor.vue +++ b/src/components/TextEditor.vue @@ -202,6 +202,11 @@ export default { beforeMount() { this.loadEditorTranslations(getLanguage()) }, + mounted() { + this.$nextTick(() => { + this.setupRTLSupport() + }) + }, methods: { getLink(text) { const results = searchProvider(text) @@ -446,6 +451,70 @@ export default { } this.editorInstance.execute('insertItem', { content, isHtml: this.html }, '!') }, + setupRTLSupport() { + // Function to detect RTL characters + const isRTL = (text) => { + const rtlChars = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/ + return rtlChars.test(text) + } + + // Wait for editor to be ready + setTimeout(() => { + const editorElement = this.$el?.querySelector('.ck-editor__editable') + if (!editorElement) return + + // Add input event listener for auto RTL/LTR detection + editorElement.addEventListener('input', (e) => { + const selection = window.getSelection() + if (!selection.rangeCount) return + + const range = selection.getRangeAt(0) + const container = range.commonAncestorContainer + const textNode = container.nodeType === Node.TEXT_NODE ? container : container.firstChild + + if (textNode && textNode.textContent) { + const text = textNode.textContent + const parentElement = textNode.parentElement || textNode.parentNode + + if (isRTL(text)) { + parentElement.style.direction = 'rtl' + parentElement.style.textAlign = 'right' + parentElement.style.unicodeBidi = 'embed' + } else if (/^[a-zA-Z0-9\s.,!?;:'"()\-_+=<>{}[\]|\\/@#$%^&*`~]+$/.test(text)) { + parentElement.style.direction = 'ltr' + parentElement.style.textAlign = 'left' + parentElement.style.unicodeBidi = 'embed' + } + } + }) + + // Set initial direction based on content + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'childList') { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.TEXT_NODE && node.textContent) { + const text = node.textContent + const parentElement = node.parentElement || node.parentNode + + if (isRTL(text)) { + parentElement.style.direction = 'rtl' + parentElement.style.textAlign = 'right' + parentElement.style.unicodeBidi = 'embed' + } + } + }) + } + }) + }) + + observer.observe(editorElement, { + childList: true, + subtree: true, + characterData: true + }) + }, 1000) + }, }, } @@ -728,5 +797,34 @@ https://github.com/ckeditor/ckeditor5/issues/1142 background: var(--color-primary-element-light) !important; color: var(--color-main-text) !important; } +/* RTL Support for mixed content */ +.ck-editor__editable { + unicode-bidi: plaintext !important; + text-align: start !important; +} + +/* Better RTL handling for mixed Persian/English text */ +.ck-editor__editable p, +.ck-editor__editable div, +.ck-editor__editable span { + unicode-bidi: isolate !important; + text-align: start !important; +} + +/* Ensure proper text flow for mixed content */ +.ck-editor__editable * { + unicode-bidi: isolate !important; +} + +/* Force proper direction for text nodes */ +.ck-editor__editable [style*="direction: rtl"] { + direction: rtl !important; + text-align: right !important; +} + +.ck-editor__editable [style*="direction: ltr"] { + direction: ltr !important; + text-align: left !important; +} From 105a8d7fca5ced487e71fbb322276c37fc995178 Mon Sep 17 00:00:00 2001 From: "amirmohammd.ahmadi" Date: Sat, 27 Sep 2025 10:44:35 +0330 Subject: [PATCH 2/5] fix: issue with calendar invitations from external domains in Nextcloud Mail app/Calendar app --- src/components/Imip.vue | 95 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/src/components/Imip.vue b/src/components/Imip.vue index e9091c600e..dbdd86bd25 100644 --- a/src/components/Imip.vue +++ b/src/components/Imip.vue @@ -157,6 +157,22 @@ function findAttendee(vEvent, email) { return undefined } +function findAttendeeByEmails(vEvent, emails) { + if (!vEvent || !Array.isArray(emails) || emails.length === 0) { + return undefined + } + + const emailSet = new Set(emails.map(e => removeMailtoPrefix(e).toLowerCase())) + for (const attendee of [...vEvent.getPropertyIterator('ORGANIZER'), ...vEvent.getAttendeeIterator()]) { + const normalized = removeMailtoPrefix(attendee.email).toLowerCase() + if (emailSet.has(normalized)) { + return attendee + } + } + + return undefined +} + export default { name: 'Imip', components: { @@ -173,6 +189,10 @@ export default { type: Object, required: true, }, + message: { + type: Object, + required: true, + }, }, data() { return { @@ -197,6 +217,7 @@ export default { currentUserPrincipalEmail: 'getCurrentUserPrincipalEmail', clonedWriteableCalendars: 'getClonedWriteableCalendars', currentUserPrincipal: 'getCurrentUserPrincipal', + accounts: 'getAccounts', }), /** @@ -208,6 +229,14 @@ export default { return this.scheduling.method }, + isFromDigikala() { + if (!this.message.from || !this.message.from[0]) { + return false + } + const fromEmail = this.message.from[0].email?.toLowerCase() || '' + return fromEmail.endsWith('@digikala.com') + }, + /** * @return {boolean} */ @@ -305,7 +334,7 @@ export default { * @return {boolean} */ userIsAttendee() { - return !!findAttendee(this.attachedVEvent, this.currentUserPrincipalEmail) + return !!findAttendeeByEmails(this.attachedVEvent, this.allUserEmails) }, /** @@ -314,10 +343,38 @@ export default { * @return {string|undefined} */ existingParticipationStatus() { - const attendee = findAttendee(this.existingVEvent, this.currentUserPrincipalEmail) + const attendee = findAttendeeByEmails(this.existingVEvent, this.allUserEmails) return attendee?.participationStatus ?? undefined }, + /** + * All user's email addresses (principal + all mail account addresses and aliases) + * + * @return {string[]} + */ + allUserEmails() { + const emails = new Set() + if (this.currentUserPrincipalEmail) { + emails.add(this.currentUserPrincipalEmail.toLowerCase()) + } + if (Array.isArray(this.accounts)) { + for (const account of this.accounts) { + if (account?.emailAddress) { + emails.add(String(account.emailAddress).toLowerCase()) + } + if (Array.isArray(account?.aliases)) { + for (const alias of account.aliases) { + const address = alias?.alias || alias?.emailAddress + if (address) { + emails.add(String(address).toLowerCase()) + } + } + } + } + } + return Array.from(emails) + }, + /** * The status message to show in case of REPLY messages. * @@ -352,6 +409,14 @@ export default { }) }, + existingEventFetched: { + immediate: false, + async handler(fetched) { + if (!fetched) return + await this.autoCreateTentativeIfNeeded() + }, + }, + /** * List of calendar options for the target calendar picker. * @@ -410,6 +475,14 @@ export default { }, }, }, + + async mounted() { + // If data already fetched on mount, attempt auto-create once + if (this.existingEventFetched) { + await this.autoCreateTentativeIfNeeded() + } + }, + methods: { async accept() { await this.saveEventWithParticipationStatus(ACCEPTED) @@ -420,6 +493,22 @@ export default { async decline() { await this.saveEventWithParticipationStatus(DECLINED) }, + async autoCreateTentativeIfNeeded() { + try { + if ( + this.isRequest + && !this.wasProcessed + && this.userIsAttendee + && this.eventIsInFuture + && this.existingEventFetched + && !this.isExistingEvent + ) { + await this.saveEventWithParticipationStatus(TENTATIVE) + } + } catch (e) { + // ignore auto-create failures + } + }, async saveEventWithParticipationStatus(status) { let vCalendar if (this.isExistingEvent) { @@ -428,7 +517,7 @@ export default { vCalendar = this.attachedVCalendar } const vEvent = vCalendar.getFirstComponent('VEVENT') - const attendee = findAttendee(vEvent, this.currentUserPrincipalEmail) + const attendee = findAttendeeByEmails(vEvent, this.allUserEmails) if (!attendee) { return } From c493b7e37f49598c163dfcf86ad44ec2109e387d Mon Sep 17 00:00:00 2001 From: "amirmohammd.ahmadi" Date: Sat, 27 Sep 2025 10:49:09 +0330 Subject: [PATCH 3/5] fix bugs --- src/components/TextEditor.vue | 98 ----------------------------------- 1 file changed, 98 deletions(-) diff --git a/src/components/TextEditor.vue b/src/components/TextEditor.vue index 542abe5c4d..c84c4ddf23 100644 --- a/src/components/TextEditor.vue +++ b/src/components/TextEditor.vue @@ -202,11 +202,6 @@ export default { beforeMount() { this.loadEditorTranslations(getLanguage()) }, - mounted() { - this.$nextTick(() => { - this.setupRTLSupport() - }) - }, methods: { getLink(text) { const results = searchProvider(text) @@ -451,70 +446,6 @@ export default { } this.editorInstance.execute('insertItem', { content, isHtml: this.html }, '!') }, - setupRTLSupport() { - // Function to detect RTL characters - const isRTL = (text) => { - const rtlChars = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/ - return rtlChars.test(text) - } - - // Wait for editor to be ready - setTimeout(() => { - const editorElement = this.$el?.querySelector('.ck-editor__editable') - if (!editorElement) return - - // Add input event listener for auto RTL/LTR detection - editorElement.addEventListener('input', (e) => { - const selection = window.getSelection() - if (!selection.rangeCount) return - - const range = selection.getRangeAt(0) - const container = range.commonAncestorContainer - const textNode = container.nodeType === Node.TEXT_NODE ? container : container.firstChild - - if (textNode && textNode.textContent) { - const text = textNode.textContent - const parentElement = textNode.parentElement || textNode.parentNode - - if (isRTL(text)) { - parentElement.style.direction = 'rtl' - parentElement.style.textAlign = 'right' - parentElement.style.unicodeBidi = 'embed' - } else if (/^[a-zA-Z0-9\s.,!?;:'"()\-_+=<>{}[\]|\\/@#$%^&*`~]+$/.test(text)) { - parentElement.style.direction = 'ltr' - parentElement.style.textAlign = 'left' - parentElement.style.unicodeBidi = 'embed' - } - } - }) - - // Set initial direction based on content - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === 'childList') { - mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.TEXT_NODE && node.textContent) { - const text = node.textContent - const parentElement = node.parentElement || node.parentNode - - if (isRTL(text)) { - parentElement.style.direction = 'rtl' - parentElement.style.textAlign = 'right' - parentElement.style.unicodeBidi = 'embed' - } - } - }) - } - }) - }) - - observer.observe(editorElement, { - childList: true, - subtree: true, - characterData: true - }) - }, 1000) - }, }, } @@ -797,34 +728,5 @@ https://github.com/ckeditor/ckeditor5/issues/1142 background: var(--color-primary-element-light) !important; color: var(--color-main-text) !important; } -/* RTL Support for mixed content */ -.ck-editor__editable { - unicode-bidi: plaintext !important; - text-align: start !important; -} - -/* Better RTL handling for mixed Persian/English text */ -.ck-editor__editable p, -.ck-editor__editable div, -.ck-editor__editable span { - unicode-bidi: isolate !important; - text-align: start !important; -} - -/* Ensure proper text flow for mixed content */ -.ck-editor__editable * { - unicode-bidi: isolate !important; -} - -/* Force proper direction for text nodes */ -.ck-editor__editable [style*="direction: rtl"] { - direction: rtl !important; - text-align: right !important; -} - -.ck-editor__editable [style*="direction: ltr"] { - direction: ltr !important; - text-align: left !important; -} From 54fa8692d684b84497087f2203a6dffcda41e61e Mon Sep 17 00:00:00 2001 From: "amirmohammd.ahmadi" Date: Sat, 27 Sep 2025 11:14:00 +0330 Subject: [PATCH 4/5] fix: write REQUEST type iMIP messages to calendars automatically --- lib/Address.php | 2 +- src/components/Composer.vue | 6 +++--- src/components/Message.vue | 4 +++- src/components/MessagePlainTextBody.vue | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/Address.php b/lib/Address.php index 3dc44c0c17..69ac3773d9 100644 --- a/lib/Address.php +++ b/lib/Address.php @@ -34,7 +34,7 @@ public static function fromHorde(Horde_Mail_Rfc822_Address $horde): self { } public static function fromRaw(string $label, string $email): self { - $wrapped = new Horde_Mail_Rfc822_Address($email); + $wrapped = new Horde_Mail_Rfc822_Address(strtolower($email)); // If no label is set we use the email if ($label !== $email) { $wrapped->personal = $label; diff --git a/src/components/Composer.vue b/src/components/Composer.vue index 00d6134ce8..c876c73437 100644 --- a/src/components/Composer.vue +++ b/src/components/Composer.vue @@ -1295,7 +1295,7 @@ export default { || account.name.toLowerCase().indexOf(term.toLowerCase()) !== -1, ) .map(account => ({ - email: account.emailAddress, + email: account.emailAddress.toLowerCase(), label: account.name, })) this.autocompleteRecipients = uniqBy('email')(this.autocompleteRecipients.concat(selfRecipients)) @@ -1353,7 +1353,7 @@ export default { return } option = {} - option.email = this.recipientSearchTerms[type] + option.email = this.recipientSearchTerms[type].toLowerCase() option.label = this.recipientSearchTerms[type] this.recipientSearchTerms[type] = '' } @@ -1523,7 +1523,7 @@ export default { if (!this.seemsValidEmailAddress(value)) { throw new Error('Skipping because it does not look like a valid email address') } - return { email: value, label: value } + return { email: value.toLowerCase(), label: value } }, /** diff --git a/src/components/Message.vue b/src/components/Message.vue index 8ede00d289..474bc4602b 100644 --- a/src/components/Message.vue +++ b/src/components/Message.vue @@ -23,7 +23,9 @@
+ :scheduling="scheduling" + :message="message" + />
{ - return `
${t('mail', 'Quoted text')}${match}
` + return `
${t('mail', 'Quoted text')}${match}
` }) }, signatureSummaryAndBody() { From 6e2d4ae23d82dd42aa6ad254a7cd0b11416095c8 Mon Sep 17 00:00:00 2001 From: "amirmohammd.ahmadi" Date: Mon, 20 Oct 2025 12:57:57 +0330 Subject: [PATCH 5/5] add digikala domain in admin setting --- lib/Settings/AdminSettings.php | 6 ++++++ src/components/Imip.vue | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index 4276814034..885c48a44a 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -145,6 +145,12 @@ public function getForm() { $this->classificationSettingsService->isClassificationEnabledByDefault(), ); + $this->initialStateService->provideInitialState( + Application::APP_ID, + 'digikala_domain', + $this->config->getAppValue('mail', 'digikala_domain', '@digikala.com'), + ); + return new TemplateResponse(Application::APP_ID, 'settings-admin'); } diff --git a/src/components/Imip.vue b/src/components/Imip.vue index dbdd86bd25..4a40351606 100644 --- a/src/components/Imip.vue +++ b/src/components/Imip.vue @@ -123,6 +123,7 @@ import { flatten } from 'ramda' import { showError } from '@nextcloud/dialogs' import useMainStore from '../store/mainStore.js' import { mapState } from 'pinia' +import { loadState } from '@nextcloud/initial-state' // iMIP methods const REQUEST = 'REQUEST' @@ -210,6 +211,7 @@ export default { existingEventFetched: false, targetCalendar: undefined, comment: '', + digikalaDomain: loadState('mail', 'digikala_domain', '@digikala.com'), } }, computed: { @@ -234,7 +236,7 @@ export default { return false } const fromEmail = this.message.from[0].email?.toLowerCase() || '' - return fromEmail.endsWith('@digikala.com') + return fromEmail.endsWith(this.digikalaDomain) }, /**