From 8c3697a579a218cc9d402550d0702c3655cd5071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 8 Apr 2024 12:45:10 +0200 Subject: [PATCH 01/19] MOBILE-4565 a11y: Label improvements --- scripts/langindex.json | 2 ++ .../components/calendar/addon-calendar-calendar.html | 4 ++-- src/addons/calendar/lang.json | 2 ++ .../pages/courses-storage/courses-storage.html | 12 +++++++----- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 5dd3147e9..bc790fe92 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -143,6 +143,8 @@ "addon.calendar.mon": "calendar", "addon.calendar.monday": "calendar", "addon.calendar.monthlyview": "calendar", + "addon.calendar.monthnext": "calendar", + "addon.calendar.monthprev": "calendar", "addon.calendar.newevent": "calendar", "addon.calendar.noevents": "local_moodlemobileapp", "addon.calendar.nopermissiontoupdatecalendar": "error", diff --git a/src/addons/calendar/components/calendar/addon-calendar-calendar.html b/src/addons/calendar/components/calendar/addon-calendar-calendar.html index 2d543551b..2de84725b 100644 --- a/src/addons/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addons/calendar/components/calendar/addon-calendar-calendar.html @@ -12,7 +12,7 @@ - + @@ -23,7 +23,7 @@ - + diff --git a/src/addons/calendar/lang.json b/src/addons/calendar/lang.json index 8820fd715..00e0549e9 100644 --- a/src/addons/calendar/lang.json +++ b/src/addons/calendar/lang.json @@ -39,6 +39,8 @@ "mon": "Mon", "monday": "Monday", "monthlyview": "Monthly view", + "monthnext": "Next month", + "monthprev": "Previous month", "newevent": "New event", "noevents": "There are no events", "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event.", diff --git a/src/addons/storagemanager/pages/courses-storage/courses-storage.html b/src/addons/storagemanager/pages/courses-storage/courses-storage.html index 892a08ab1..40ccc995e 100644 --- a/src/addons/storagemanager/pages/courses-storage/courses-storage.html +++ b/src/addons/storagemanager/pages/courses-storage/courses-storage.html @@ -37,8 +37,9 @@ {{ totalSize | coreBytesToSize }} - + [disabled]="completelyDownloadedCourses.length === 0" color="danger" fill="clear" + [attr.aria-label]="'addon.storagemanager.deletecourses' | translate"> + - - + + From 540beee17e29aca37e3db0f283cdd0610a0ec3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 15 Apr 2024 12:54:14 +0200 Subject: [PATCH 02/19] MOBILE-4565 a11y: Search box must alert of minimum required length --- scripts/langindex.json | 1 + src/core/features/courses/pages/list/list.html | 2 +- .../search/components/search-box/core-search-box.html | 8 ++++++-- .../search/components/search-box/search-box.scss | 2 -- .../search/components/search-box/search-box.ts | 11 ++++++++--- src/core/features/search/lang.json | 1 + src/core/features/tag/pages/search/search.html | 2 +- 7 files changed, 18 insertions(+), 9 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index bc790fe92..a271644f8 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -2407,6 +2407,7 @@ "core.search.allcategories": "local_moodlemobileapp", "core.search.allcourses": "search", "core.search.empty": "local_moodlemobileapp", + "core.search.err_minlength": "form", "core.search.filtercategories": "local_moodlemobileapp", "core.search.filtercourses": "local_moodlemobileapp", "core.search.filterheader": "search", diff --git a/src/core/features/courses/pages/list/list.html b/src/core/features/courses/pages/list/list.html index 47b406bc2..35b91bb56 100644 --- a/src/core/features/courses/pages/list/list.html +++ b/src/core/features/courses/pages/list/list.html @@ -26,7 +26,7 @@ + searchArea="CoreCoursesSearch" [lengthCheck]="1" /> diff --git a/src/core/features/search/components/search-box/core-search-box.html b/src/core/features/search/components/search-box/core-search-box.html index 054ab491a..c10f9d2ea 100644 --- a/src/core/features/search/components/search-box/core-search-box.html +++ b/src/core/features/search/components/search-box/core-search-box.html @@ -2,8 +2,7 @@ - + + + + {{ 'core.search.err_minlength' | translate : {'$a': {'format': lengthCheck} } }} + + diff --git a/src/core/features/search/components/search-box/search-box.scss b/src/core/features/search/components/search-box/search-box.scss index 8afdc8c3d..fa22c8742 100644 --- a/src/core/features/search/components/search-box/search-box.scss +++ b/src/core/features/search/components/search-box/search-box.scss @@ -35,8 +35,6 @@ } } - - .core-search-history { max-height: calc(-120px + 80vh); overflow-y: auto; diff --git a/src/core/features/search/components/search-box/search-box.ts b/src/core/features/search/components/search-box/search-box.ts index c013928d3..1db5989c8 100644 --- a/src/core/features/search/components/search-box/search-box.ts +++ b/src/core/features/search/components/search-box/search-box.ts @@ -62,6 +62,7 @@ export class CoreSearchBoxComponent implements OnInit { searchText = ''; history: CoreSearchHistoryDBRecord[] = []; historyShown = false; + showLengthAlert = false; constructor() { this.onSubmit = new EventEmitter(); @@ -86,14 +87,17 @@ export class CoreSearchBoxComponent implements OnInit { * @param e Event. */ submitForm(e?: Event): void { - e && e.preventDefault(); - e && e.stopPropagation(); + e?.preventDefault(); + e?.stopPropagation(); if (this.searchText.length < this.lengthCheck) { - // The view should handle this case, but we check it here too just in case. + this.showLengthAlert = true; + return; } + this.showLengthAlert = false; + if (this.searchArea) { this.saveSearchToHistory(this.searchText); } @@ -147,6 +151,7 @@ export class CoreSearchBoxComponent implements OnInit { clearForm(): void { this.searched = ''; this.searchText = ''; + this.showLengthAlert = false; this.onClear.emit(); } diff --git a/src/core/features/search/lang.json b/src/core/features/search/lang.json index 5a17626cd..d1fbb00cd 100644 --- a/src/core/features/search/lang.json +++ b/src/core/features/search/lang.json @@ -8,5 +8,6 @@ "globalsearch": "Global search", "noresults": "No results for \"{{$a}}\"", "noresultshelp": "Check for typos or try using different keywords", + "err_minlength": "You must enter at least {{$a.format}} characters here.", "resultby": "By {{$a}}" } diff --git a/src/core/features/tag/pages/search/search.html b/src/core/features/tag/pages/search/search.html index 40a682ac7..d04d7e819 100644 --- a/src/core/features/tag/pages/search/search.html +++ b/src/core/features/tag/pages/search/search.html @@ -19,7 +19,7 @@ + autocorrect="off" [spellcheck]="false" [autoFocus]="false" [lengthCheck]="1" searchArea="CoreTag" /> From e87b05bd69fb96ce194016861ade7204b01fe17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 15 Apr 2024 13:08:33 +0200 Subject: [PATCH 03/19] MOBILE-4565 a11y: Add alert to empty search message --- src/addons/messages/pages/search/search.html | 2 +- src/addons/mod/forum/pages/search/search.html | 2 +- .../components/index/addon-mod-glossary-index.html | 4 ++-- src/core/features/courses/pages/list/list.html | 4 ++-- src/core/features/courses/pages/list/list.ts | 9 +++++++-- .../search/pages/global-search/global-search.html | 2 +- src/core/features/tag/pages/search/search.html | 2 +- .../features/user/pages/participants/participants.html | 2 +- 8 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/addons/messages/pages/search/search.html b/src/addons/messages/pages/search/search.html index 60fa3c1ca..30d66eff8 100644 --- a/src/addons/messages/pages/search/search.html +++ b/src/addons/messages/pages/search/search.html @@ -28,7 +28,7 @@ + icon="fas-magnifying-glass" [message]="'core.noresults' | translate" role="alert" /> diff --git a/src/addons/mod/forum/pages/search/search.html b/src/addons/mod/forum/pages/search/search.html index c31a0b312..18ed2b35d 100644 --- a/src/addons/mod/forum/pages/search/search.html +++ b/src/addons/mod/forum/pages/search/search.html @@ -31,7 +31,7 @@ - +

{{ 'core.search.empty' | translate }}

{{ 'core.search.noresults' | translate: { $a: resultsSource.getQuery() } }}

diff --git a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html index fc3f19726..10ff6ad2e 100644 --- a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html @@ -24,7 +24,7 @@ - + @@ -65,7 +65,7 @@ + [message]="'addon.mod_glossary.noentriesfound' | translate" [attr.role]="hasSearched ? 'alert' : null" /> diff --git a/src/core/features/courses/pages/list/list.html b/src/core/features/courses/pages/list/list.html index 35b91bb56..b896132fb 100644 --- a/src/core/features/courses/pages/list/list.html +++ b/src/core/features/courses/pages/list/list.html @@ -28,7 +28,7 @@ [placeholder]="'core.courses.search' | translate" [searchLabel]="'core.courses.search' | translate" [autoFocus]="searchMode" searchArea="CoreCoursesSearch" [lengthCheck]="1" /> - + @@ -45,7 +45,7 @@ + [message]="'core.courses.nosearchresults' | translate" role="alert" /> diff --git a/src/core/features/courses/pages/list/list.ts b/src/core/features/courses/pages/list/list.ts index 0a3d460ec..1993496cb 100644 --- a/src/core/features/courses/pages/list/list.ts +++ b/src/core/features/courses/pages/list/list.ts @@ -49,6 +49,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { coursesLoaded = 0; canLoadMore = false; loadMoreError = false; + loadingMessage = Translate.instant('core.loading'); showOnlyEnrolled = false; @@ -176,6 +177,8 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { * @returns Promise resolved when done. */ protected async loadCourses(clearTheList = false): Promise { + this.loadingMessage = Translate.instant('core.loading'); + this.loadMoreError = false; try { @@ -249,9 +252,10 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.searchTotal = 0; this.logSearch = CoreTime.once(() => this.performLogSearch()); - const modal = await CoreDomUtils.showModalLoading('core.searching', true); + this.loaded = false; await this.searchCourses().finally(() => { - modal.dismiss(); + this.loaded = true; + }); } @@ -310,6 +314,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { */ protected async searchCourses(): Promise { this.loadMoreError = false; + this.loadingMessage = Translate.instant('core.searching'); try { const response = await CoreCourses.search(this.searchText, this.searchPage, undefined, this.showOnlyEnrolled); diff --git a/src/core/features/search/pages/global-search/global-search.html b/src/core/features/search/pages/global-search/global-search.html index 61f677773..ebdb674d9 100644 --- a/src/core/features/search/pages/global-search/global-search.html +++ b/src/core/features/search/pages/global-search/global-search.html @@ -35,7 +35,7 @@ [error]="loadMoreError" /> -

{{ 'core.search.empty' | translate }}

+

{{ 'core.search.empty' | translate }}

{{ 'core.search.noresults' | translate: { $a: resultsSource.getQuery() } }}

{{ 'core.search.noresultshelp' | translate }}

diff --git a/src/core/features/tag/pages/search/search.html b/src/core/features/tag/pages/search/search.html index d04d7e819..8416209e6 100644 --- a/src/core/features/tag/pages/search/search.html +++ b/src/core/features/tag/pages/search/search.html @@ -34,7 +34,7 @@
+ [message]="'core.tag.notagsfound' | translate: {$a: query}" role="alert" />
diff --git a/src/core/features/user/pages/participants/participants.html b/src/core/features/user/pages/participants/participants.html index 4775bb79f..96bb9d5d9 100644 --- a/src/core/features/user/pages/participants/participants.html +++ b/src/core/features/user/pages/participants/participants.html @@ -12,7 +12,7 @@ [message]="'core.user.noparticipants' | translate" /> + [message]="'core.noresults' | translate" [attr.role]="searchQuery ? 'alert' : null" /> Date: Mon, 15 Apr 2024 13:50:09 +0200 Subject: [PATCH 04/19] MOBILE-4565 timeline: Action button will be shown on the next line --- .../events/addon-block-timeline-events.html | 71 +++++++++---------- .../timeline/components/events/events.scss | 24 +++---- 2 files changed, 43 insertions(+), 52 deletions(-) diff --git a/src/addons/block/timeline/components/events/addon-block-timeline-events.html b/src/addons/block/timeline/components/events/addon-block-timeline-events.html index 52aa0092c..415bc6636 100644 --- a/src/addons/block/timeline/components/events/addon-block-timeline-events.html +++ b/src/addons/block/timeline/components/events/addon-block-timeline-events.html @@ -16,48 +16,43 @@ - - - - - {{event.timesort * 1000 | coreFormatDate:"strftimetime24" }} - - - -

- - - - {{ 'addon.block_timeline.overdue' | translate }} - -

-

- - - -

-

- - - -

-
-
+ + + {{event.timesort * 1000 | coreFormatDate:"strftimetime24" }} + - - - {{event.action.name}} - - {{event.action.itemcount}} + +

+ + + + {{ 'addon.block_timeline.overdue' | translate }} - +

+

+ + + +

+

+ + + +

+
+ + {{event.action.name}} + + {{event.action.itemcount}} + + +
diff --git a/src/addons/block/timeline/components/events/events.scss b/src/addons/block/timeline/components/events/events.scss index 21fae2d8d..709f503bd 100644 --- a/src/addons/block/timeline/components/events/events.scss +++ b/src/addons/block/timeline/components/events/events.scss @@ -27,26 +27,22 @@ h4.core-bold { --margin-end: 0.5rem; --margin-vertical: 0; } + + ion-label { + display: flex; + flex-direction: column; + + .addon-block-timeline-activity-action { + display: flex; + justify-content: flex-end; + } + } } .addon-block-timeline-activity-time { flex-grow: 0; } -.addon-block-timeline-activity-action { - display: flex; - justify-content: flex-end; -} - -.addon-block-timeline-activity-main, -.addon-block-timeline-activity-name { - flex-grow: 1; - p { - overflow: hidden; - text-overflow: ellipsis; - } -} - .addon-block-timeline-activity-name { flex-grow: 1; overflow: hidden; From b71fb0ae05a7a6d14f57ec3852606ca8d2ecfc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 15 Apr 2024 14:24:09 +0200 Subject: [PATCH 05/19] MOBILE-4565 a11y: Dynamic context menu --- .../messages/pages/discussion/discussion.html | 22 ++++++++++++++----- .../messages/pages/discussion/discussion.ts | 3 --- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/addons/messages/pages/discussion/discussion.html b/src/addons/messages/pages/discussion/discussion.html index db3c27112..b31c90905 100644 --- a/src/addons/messages/pages/discussion/discussion.html +++ b/src/addons/messages/pages/discussion/discussion.html @@ -24,16 +24,26 @@ [content]="'addon.messages.info' | translate" (action)="viewInfo()" iconAction="fas-circle-info" /> - + + + + - + + + + Date: Mon, 15 Apr 2024 15:43:57 +0200 Subject: [PATCH 06/19] MOBILE-4565 a11y: Add aria-current=date to calendar --- .../calendar/components/calendar/addon-calendar-calendar.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/addons/calendar/components/calendar/addon-calendar-calendar.html b/src/addons/calendar/components/calendar/addon-calendar-calendar.html index 2de84725b..83438a0e6 100644 --- a/src/addons/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addons/calendar/components/calendar/addon-calendar-calendar.html @@ -55,7 +55,8 @@ "weekend": day.isweekend, "duration_finish": day.haslastdayofevent }' [class.addon-calendar-event-past-day]="month.isPastMonth || day.ispast" role="cell" - (ariaButtonClick)="dayClicked(day.mday)" [tabindex]="activeView ? 0 : -1"> + (ariaButtonClick)="dayClicked(day.mday)" [tabindex]="activeView ? 0 : -1" + [attr.aria-current]="month.isCurrentMonth && day.istoday ? 'date' : null">

{{ day.periodName | translate }} From 1b301dae580dc4619cd6c5c75027b3a8714de45d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 15 Apr 2024 16:01:37 +0200 Subject: [PATCH 07/19] MOBILE-4565 a11y: Do not let the user focus on elements in movement --- src/core/components/swipe-slides/swipe-slides.html | 2 +- src/core/components/swipe-slides/swipe-slides.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/components/swipe-slides/swipe-slides.html b/src/core/components/swipe-slides/swipe-slides.html index bcfc05ac8..490a6f3e3 100644 --- a/src/core/components/swipe-slides/swipe-slides.html +++ b/src/core/components/swipe-slides/swipe-slides.html @@ -1,4 +1,4 @@ - + diff --git a/src/core/components/swipe-slides/swipe-slides.ts b/src/core/components/swipe-slides/swipe-slides.ts index fa3864c38..360968d83 100644 --- a/src/core/components/swipe-slides/swipe-slides.ts +++ b/src/core/components/swipe-slides/swipe-slides.ts @@ -72,7 +72,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe protected hostElement: HTMLElement; protected unsubscribe?: () => void; protected resizeListener: CoreEventObserver; - protected activeSlideIndexes: number[] = []; + protected activeSlideIndex?: number; protected onReadyPromise = new CorePromisedValue(); constructor( @@ -112,7 +112,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe * @returns Whether the slide is active. */ isActive(index: number): boolean { - return this.activeSlideIndexes.includes(index); + return this.activeSlideIndex === index; } /** @@ -153,7 +153,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe item: items[initialIndex], }; - this.activeSlideIndexes = [initialIndex]; + this.activeSlideIndex = initialIndex; this.manager.setSelectedItem(items[initialIndex]); this.onWillChange.emit(initialItemData); @@ -268,7 +268,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe return; } - this.activeSlideIndexes.push(currentItemData.index); + this.activeSlideIndex = undefined; this.manager?.setSelectedItem(currentItemData.item); this.onWillChange.emit(currentItemData); @@ -283,12 +283,12 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe async slideDidChange(): Promise { const currentItemData = await this.getCurrentSlideItemData(); if (!currentItemData) { - this.activeSlideIndexes = []; + this.activeSlideIndex = undefined; return; } - this.activeSlideIndexes = [currentItemData.index]; + this.activeSlideIndex = currentItemData.index; this.onDidChange.emit(currentItemData); From 7dfb3c13df9a8e1246d6b412c06238de8f92b8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 15 Apr 2024 17:38:36 +0200 Subject: [PATCH 08/19] MOBILE-4565 a11y: Solve user tours keyboard focus --- src/core/features/usertours/services/user-tours.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/features/usertours/services/user-tours.ts b/src/core/features/usertours/services/user-tours.ts index 3afbe03c4..f497c07bb 100644 --- a/src/core/features/usertours/services/user-tours.ts +++ b/src/core/features/usertours/services/user-tours.ts @@ -127,6 +127,7 @@ export class CoreUserToursService { const tour = CoreDirectivesRegistry.require(element, CoreUserToursUserTourComponent); viewContainer?.setAttribute('aria-hidden', 'true'); + viewContainer?.setAttribute('tabindex', '-1'); this.toursListeners[options.id]?.forEach(listener => listener.resolve()); @@ -149,6 +150,8 @@ export class CoreUserToursService { const viewContainer = container.querySelector('ion-router-outlet, ion-nav, #ion-view-container-root'); viewContainer?.removeAttribute('aria-hidden'); + viewContainer?.removeAttribute('tabindex'); + } /** From 404cb9129ab45e329c29d272b6cc81908676446e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 16 Apr 2024 10:06:55 +0200 Subject: [PATCH 09/19] MOBILE-4565 messages: Use new Ionic accordion component on groups --- src/addons/calendar/pages/index/index.ts | 2 +- .../messages/pages/discussion/discussion.ts | 7 +- .../group-conversations.html | 142 +++----- .../group-conversations.ts | 307 ++++++++++-------- src/addons/messages/services/messages.ts | 52 +-- ...w-recent-conversations-and-contacts_22.png | Bin 27710 -> 27946 bytes 6 files changed, 236 insertions(+), 274 deletions(-) diff --git a/src/addons/calendar/pages/index/index.ts b/src/addons/calendar/pages/index/index.ts index ac6828c74..0943286ae 100644 --- a/src/addons/calendar/pages/index/index.ts +++ b/src/addons/calendar/pages/index/index.ts @@ -159,7 +159,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { } /** - * View loaded. + * @inheritdoc */ ngOnInit(): void { this.loadUpcoming = !!CoreNavigator.getRouteBooleanParam('upcoming'); diff --git a/src/addons/messages/pages/discussion/discussion.ts b/src/addons/messages/pages/discussion/discussion.ts index 44ec23ea9..fe11d4c22 100644 --- a/src/addons/messages/pages/discussion/discussion.ts +++ b/src/addons/messages/pages/discussion/discussion.ts @@ -25,6 +25,7 @@ import { AddonMessages, AddonMessagesConversationMessageFormatted, AddonMessagesSendMessageResults, + AddonMessagesUpdateConversationAction, } from '../../services/messages'; import { AddonMessagesOffline, AddonMessagesOfflineMessagesDBRecordFormatted } from '../../services/messages-offline'; import { AddonMessagesSync, AddonMessagesSyncProvider } from '../../services/messages-sync'; @@ -1298,7 +1299,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView CoreEvents.trigger(AddonMessagesProvider.UPDATE_CONVERSATION_LIST_EVENT, { conversationId: this.conversation.id, - action: 'favourite', + action: AddonMessagesUpdateConversationAction.FAVOURITE, value: this.conversation.isfavourite, }, this.siteId); } catch (error) { @@ -1330,7 +1331,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView CoreEvents.trigger(AddonMessagesProvider.UPDATE_CONVERSATION_LIST_EVENT, { conversationId: this.conversation.id, - action: 'mute', + action: AddonMessagesUpdateConversationAction.MUTE, value: this.conversation.ismuted, }, this.siteId); @@ -1446,7 +1447,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView AddonMessagesProvider.UPDATE_CONVERSATION_LIST_EVENT, { conversationId: this.conversation.id, - action: 'delete', + action: AddonMessagesUpdateConversationAction.DELETE, }, this.siteId, ); diff --git a/src/addons/messages/pages/group-conversations/group-conversations.html b/src/addons/messages/pages/group-conversations/group-conversations.html index 313ebb0b5..145d43b60 100644 --- a/src/addons/messages/pages/group-conversations/group-conversations.html +++ b/src/addons/messages/pages/group-conversations/group-conversations.html @@ -21,7 +21,7 @@ - + @@ -30,110 +30,46 @@

{{ 'addon.messages.contacts' | translate }}

+

{{ 'addon.messages.contacts' | translate }}

- - - {{ 'addon.messages.pendingcontactrequests' | translate:{$a: contactRequestsCount} }} - + + {{contactRequestsCount}} +
- - - -
- - - - - -

{{ 'addon.messages.nofavourites' | translate }}

-
-
-
- - - - - - - - - -
- - - - - -

{{ 'addon.messages.nogroupconversations' | translate }}

-
-
-
- - - - - - - - -
- - - - - -

{{ 'addon.messages.noindividualconversations' | translate }}

-
-
-
- - - - - - + + + + +

{{ option.titleString | translate }} ({{ option.count }})

+
+ + {{ option.unread }} + +
+
+ + + + + + +

{{ option.emptyString| translate }}

+
+
+
+ + + + + +
+
+
diff --git a/src/addons/messages/pages/group-conversations/group-conversations.ts b/src/addons/messages/pages/group-conversations/group-conversations.ts index c27a34d7d..20fbedc11 100644 --- a/src/addons/messages/pages/group-conversations/group-conversations.ts +++ b/src/addons/messages/pages/group-conversations/group-conversations.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; -import { IonContent } from '@ionic/angular'; +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { AccordionGroupChangeEventDetail, IonAccordionGroup, IonContent } from '@ionic/angular'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { @@ -21,6 +21,8 @@ import { AddonMessagesConversationFormatted, AddonMessages, AddonMessagesNewMessagedEventData, + AddonMessagesUnreadConversationCountsEventData, + AddonMessagesUpdateConversationAction, } from '../../services/messages'; import { AddonMessagesOffline, @@ -40,6 +42,12 @@ import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-lin import { CorePlatform } from '@services/platform'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; +const enum AddonMessagesGroupConversationOptionNames { + FAVOURITES = 'favourites', + GROUP = 'group', + INDIVIDUAL = 'individual', +} + /** * Page that displays the list of conversations, including group conversations. */ @@ -51,43 +59,49 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view'; export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; - @ViewChild(IonContent) content?: IonContent; - @ViewChild('favlist') favListEl?: ElementRef; - @ViewChild('grouplist') groupListEl?: ElementRef; - @ViewChild('indlist') indListEl?: ElementRef; + @ViewChild('accordionGroup', { static: true }) accordionGroup!: IonAccordionGroup; loaded = false; loadingMessage: string; selectedConversationId?: number; selectedUserId?: number; contactRequestsCount = 0; - favourites: AddonMessagesGroupConversationOption = { - type: undefined, - favourites: true, - count: 0, - unread: 0, - conversations: [], - }; - group: AddonMessagesGroupConversationOption = { - type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP, - favourites: false, - count: 0, - unread: 0, - conversations: [], - }; - - individual: AddonMessagesGroupConversationOption = { - type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, - favourites: false, - count: 0, - unread: 0, - conversations: [], - }; + groupConversations: AddonMessagesGroupConversationOption[] = [ + { + optionName: AddonMessagesGroupConversationOptionNames.FAVOURITES, + titleString: 'core.favourites', + emptyString: 'addon.messages.nofavourites', + type: undefined, + favourites: true, + count: 0, + unread: 0, + conversations: [], + }, + { + optionName: AddonMessagesGroupConversationOptionNames.GROUP, + titleString: 'addon.messages.groupconversations', + emptyString: 'addon.messages.nogroupconversations', + type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP, + favourites: false, + count: 0, + unread: 0, + conversations: [], + }, + { + optionName: AddonMessagesGroupConversationOptionNames.INDIVIDUAL, + titleString: 'addon.messages.individualconversations', + emptyString: 'addon.messages.noindividualconversations', + type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + favourites: false, + count: 0, + unread: 0, + conversations: [], + }, + ]; typeGroup = AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP; - currentListEl?: HTMLElement; protected siteId: string; protected currentUserId: number; @@ -100,6 +114,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { protected updateConversationListObserver: CoreEventObserver; protected contactRequestsCountObserver: CoreEventObserver; protected memberInfoObserver: CoreEventObserver; + protected firstExpand = false; constructor( protected route: ActivatedRoute, @@ -114,9 +129,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { (data) => { // Check if the new message belongs to the option that is currently expanded. const expandedOption = this.getExpandedOption(); - const messageOption = this.getConversationOption(data); + const messageOptionName = this.getConversationOptionName(data); - if (expandedOption != messageOption) { + if (expandedOption?.optionName !== messageOptionName) { return; // Message doesn't belong to current list, stop. } @@ -155,8 +170,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } // Sort the affected list. - const option = this.getConversationOption(conversation); - option.conversations = AddonMessages.sortConversations(option.conversations || []); + const optionName = this.getConversationOptionName(conversation); + const option = this.getConversationGroupByName(optionName); + option.conversations = AddonMessages.sortConversations(option.conversations); if (isNewer) { // The last message is newer than the previous one, scroll to top to keep viewing the conversation. @@ -209,11 +225,11 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { this.updateConversationListObserver = CoreEvents.on( AddonMessagesProvider.UPDATE_CONVERSATION_LIST_EVENT, (data) => { - if (data && data.action == 'mute') { - // If the conversation is displayed, change its muted value. + if (data?.action === AddonMessagesUpdateConversationAction.MUTE) { + // If the conversation is displayed, change its muted value. const expandedOption = this.getExpandedOption(); - if (expandedOption && expandedOption.conversations) { + if (expandedOption?.conversations) { const conversation = this.findConversation(data.conversationId, undefined, expandedOption); if (conversation) { conversation.ismuted = !!data.value; @@ -233,7 +249,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { this.pushObserver = CorePushNotificationsDelegate.on('receive') .subscribe((notification) => { // New message received. If it's from current site, refresh the data. - if (CoreUtils.isFalseOrZero(notification.notif) && notification.site == this.siteId) { + if (CoreUtils.isFalseOrZero(notification.notif) && notification.site === this.siteId) { // Don't refresh unread counts, it's refreshed from the main menu handler in this case. this.refreshData(undefined, false); } @@ -243,9 +259,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { this.cronObserver = CoreEvents.on( AddonMessagesProvider.UNREAD_CONVERSATION_COUNTS_EVENT, (data) => { - this.favourites.unread = data.favourites; - this.individual.unread = data.individual + data.self; // Self is only returned if it's not favourite. - this.group.unread = data.group; + this.setCounts(data, 'unread'); }, this.siteId, ); @@ -269,15 +283,14 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } const expandedOption = this.getExpandedOption(); - if (expandedOption == this.individual || expandedOption == this.favourites) { - if (!expandedOption.conversations || expandedOption.conversations.length <= 0) { - return; - } + if (expandedOption?.optionName === AddonMessagesGroupConversationOptionNames.GROUP || + !expandedOption?.conversations.length) { + return; + } - const conversation = this.findConversation(undefined, data.userId, expandedOption); - if (conversation) { - conversation.isblocked = data.userBlocked; - } + const conversation = this.findConversation(undefined, data.userId, expandedOption); + if (conversation) { + conversation.isblocked = data.userBlocked; } }, this.siteId, @@ -285,7 +298,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } /** - * Component loaded. + * @inheritdoc */ async ngOnInit(): Promise { this.route.queryParams.subscribe(async (params) => { @@ -305,15 +318,11 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { if (!this.selectedConversationId && !this.selectedUserId && CoreScreen.isTablet) { // Load the first conversation. - let conversation: AddonMessagesConversationForList; const expandedOption = this.getExpandedOption(); - if (expandedOption && expandedOption.conversations.length) { - conversation = expandedOption.conversations[0]; - - if (conversation) { - await this.gotoConversation(conversation.id); - } + const conversation = expandedOption?.conversations[0]; + if (conversation) { + await this.gotoConversation(conversation.id); } } @@ -341,22 +350,19 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { try { await Promise.all(promises); - // The expanded status hasn't been initialized. Do it now. - if (this.favourites.expanded === undefined && (this.selectedConversationId || this.selectedUserId)) { + if (!this.firstExpand && (this.selectedConversationId || this.selectedUserId)) { // A certain conversation should be opened. // We don't know which option it belongs to, so we need to fetch the data for all of them. - const promises: Promise[] = []; - - promises.push(this.fetchDataForOption(this.favourites, false)); - promises.push(this.fetchDataForOption(this.group, false)); - promises.push(this.fetchDataForOption(this.individual, false)); + const promises = this.groupConversations.map((option) => + this.fetchDataForOption(option, false)); await Promise.all(promises); // All conversations have been loaded, find the one we need to load and expand its option. const conversation = this.findConversation(this.selectedConversationId, this.selectedUserId); if (conversation) { - const option = this.getConversationOption(conversation); + const optionName = this.getConversationOptionName(conversation); + const option = this.getConversationGroupByName(optionName); await this.expandOption(option); @@ -376,18 +382,24 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { /** * Fetch data for the expanded option. - * - * @returns Promise resolved when done. */ protected async fetchDataForExpandedOption(): Promise { - if (this.favourites.expanded === undefined) { + if (!this.firstExpand) { // Calculate which option should be expanded initially. - this.favourites.expanded = this.favourites.count != 0 && !this.group.unread && !this.individual.unread; - this.group.expanded = !this.favourites.expanded && this.group.count != 0 && !this.individual.unread; - this.individual.expanded = !this.favourites.expanded && !this.group.expanded; - } + let expandOption = this.groupConversations.find((option) => option.unread); - this.loadCurrentListElement(); + if (!expandOption) { + expandOption = this.groupConversations.find((option) => option.count > 0); + } + + if (!expandOption) { + expandOption = this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.INDIVIDUAL); + } + + this.accordionGroup.value = expandOption.optionName; + + this.firstExpand = true; + } const expandedOption = this.getExpandedOption(); @@ -418,8 +430,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { conversations: [], canLoadMore: false, }; - let offlineMessages: - AddonMessagesOfflineAnyMessagesFormatted[] = []; + let offlineMessages: AddonMessagesOfflineAnyMessagesFormatted[] = []; // Get the conversations and, if needed, the offline messages. Always try to get the latest data. promises.push(AddonMessages.invalidateConversations(this.siteId).then(async () => { @@ -469,9 +480,36 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { await AddonMessages.invalidateConversationCounts(this.siteId); const counts = await AddonMessages.getConversationCounts(this.siteId); - this.favourites.count = counts.favourites; - this.individual.count = counts.individual + counts.self; // Self is only returned if it's not favourite. - this.group.count = counts.group; + this.setCounts(counts); + } + + /** + * Set conversation counts. + * + * @param counts Counts to set. + * @param valueToSet Value to set count or unread. + */ + protected setCounts( + counts: AddonMessagesUnreadConversationCountsEventData, + valueToSet: 'count' | 'unread' = 'count', + ): void { + this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.FAVOURITES)[valueToSet] = counts.favourites; + this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.INDIVIDUAL)[valueToSet] = + counts.individual + counts.self; // Self is only returned if it's not favourite. + this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.GROUP)[valueToSet] = counts.group; + } + + /** + * Get a conversation group by its name. + * + * @param name Name of the group. + * @returns The conversation group. + */ + protected getConversationGroupByName(name: AddonMessagesGroupConversationOptionNames): AddonMessagesGroupConversationOption { + const option = this.groupConversations.find((group) => group.optionName === name); + + // Option should always be defined. + return option ?? this.groupConversations[0]; } /** @@ -491,16 +529,19 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { if (conversationId) { const conversations: AddonMessagesConversationForList[] = option ? option.conversations - : (this.favourites.conversations.concat(this.group.conversations).concat(this.individual.conversations)); + : this.groupConversations.flatMap((option) => option.conversations); - return conversations.find((conv) => conv.id == conversationId); + return conversations.find((conv) => conv.id === conversationId); } - const conversations = option - ? option.conversations - : this.favourites.conversations.concat(this.individual.conversations); + let conversations = option?.conversations; + if (!conversations) { + // Only check on favourites and individual conversations. + conversations = this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.FAVOURITES).conversations + .concat(this.getConversationGroupByName(AddonMessagesGroupConversationOptionNames.INDIVIDUAL).conversations); + } - return conversations.find((conv) => conv.userid == userId); + return conversations.find((conv) => conv.userid === userId); } /** @@ -509,12 +550,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * @returns Option currently expanded. */ protected getExpandedOption(): AddonMessagesGroupConversationOption | undefined { - if (this.favourites.expanded) { - return this.favourites; - } else if (this.group.expanded) { - return this.group; - } else if (this.individual.expanded) { - return this.individual; + if (this.accordionGroup.value) { + return this.getConversationGroupByName(this.accordionGroup.value as AddonMessagesGroupConversationOptionNames); } } @@ -572,7 +609,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { option.loadMoreError = true; } - infiniteComplete && infiniteComplete(); + infiniteComplete?.(); } /** @@ -617,13 +654,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { imageurl: message.conversation?.imageurl || '', }; - if (this.getConversationOption(conversation) == option) { + if (this.getConversationOptionName(conversation) === option.optionName) { // Message belongs to current option, add the conversation. this.addLastOfflineMessage(conversation, message); - this.addOfflineConversation(conversation); + this.addOfflineConversation(conversation, option); } } - } else if (option.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { + } else if (option.type === AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { // It's a new conversation. Check if we already created it (there is more than one message for the same user). const conversation = this.findConversation(undefined, message.touserid, option); @@ -655,7 +692,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { }; this.addLastOfflineMessage(conversation, message); - this.addOfflineConversation(conversation); + this.addOfflineConversation(conversation, option); return; })); @@ -670,9 +707,12 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * Add an offline conversation into the right list of conversations. * * @param conversation Offline conversation to add. + * @param option Option where to add the conversation. */ - protected addOfflineConversation(conversation: AddonMessagesConversationForList): void { - const option = this.getConversationOption(conversation); + protected addOfflineConversation( + conversation: AddonMessagesConversationForList, + option: AddonMessagesGroupConversationOption, + ): void { option.conversations.unshift(conversation); } @@ -693,23 +733,23 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } /** - * Given a conversation, return its option (favourites, group, individual). + * Given a conversation, return its option name. * * @param conversation Conversation to check. - * @returns Option object. + * @returns Option name. */ - protected getConversationOption( + protected getConversationOptionName( conversation: AddonMessagesConversationForList | AddonMessagesNewMessagedEventData, - ): AddonMessagesGroupConversationOption { + ): AddonMessagesGroupConversationOptionNames { if (conversation.isfavourite) { - return this.favourites; + return AddonMessagesGroupConversationOptionNames.FAVOURITES; } - if (conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP) { - return this.group; + if (conversation.type === AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP) { + return AddonMessagesGroupConversationOptionNames.GROUP; } - return this.individual; + return AddonMessagesGroupConversationOptionNames.INDIVIDUAL; } /** @@ -727,9 +767,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { try { await this.fetchData(refreshUnreadCounts); } finally { - if (refresher) { - refresher?.complete(); - } + refresher?.complete(); } } } @@ -737,19 +775,20 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { /** * Toogle the visibility of an option (expand/collapse). * - * @param option The option to expand/collapse. + * @param ev The event of the accordion. */ - toggle(option: AddonMessagesGroupConversationOption): void { - if (option.expanded) { - // Already expanded, close it. - option.expanded = false; - this.loadCurrentListElement(); - } else { - // Pass getCounts=true to update the counts everytime the user expands an option. - this.expandOption(option, true).catch((error) => { - CoreDomUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); - }); + accordionGroupChange(ev: AccordionGroupChangeEventDetail): void { + const optionName = ev.value as AddonMessagesGroupConversationOptionNames; + if (!optionName) { + return; } + + const option = this.getConversationGroupByName(optionName); + + // Pass getCounts=true to update the counts everytime the user expands an option. + this.expandOption(option, true).catch((error) => { + CoreDomUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); + }); } /** @@ -761,40 +800,18 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { */ protected async expandOption(option: AddonMessagesGroupConversationOption, getCounts = false): Promise { // Collapse all and expand the right one. - this.favourites.expanded = false; - this.group.expanded = false; - this.individual.expanded = false; - - option.expanded = true; option.loading = true; + this.accordionGroup.value = option.optionName; try { await this.fetchDataForOption(option, false, getCounts); - - this.loadCurrentListElement(); } catch (error) { - option.expanded = false; + this.accordionGroup.value = undefined; throw error; } finally { option.loading = false; } - - } - - /** - * Load the current list element based on the expanded list. - */ - protected loadCurrentListElement(): void { - if (this.favourites.expanded) { - this.currentListEl = this.favListEl && this.favListEl.nativeElement; - } else if (this.group.expanded) { - this.currentListEl = this.groupListEl && this.groupListEl.nativeElement; - } else if (this.individual.expanded) { - this.currentListEl = this.indListEl && this.indListEl.nativeElement; - } else { - this.currentListEl = undefined; - } } /** @@ -805,7 +822,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } /** - * Page destroyed. + * @inheritdoc */ ngOnDestroy(): void { this.newMessagesObserver?.off(); @@ -825,11 +842,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * Conversation options. */ export type AddonMessagesGroupConversationOption = { + optionName: AddonMessagesGroupConversationOptionNames; + titleString: string; + emptyString: string; type?: number; // Option type. favourites: boolean; // Whether it contains favourites conversations. count: number; // Number of conversations. unread?: number; // Number of unread conversations. - expanded?: boolean; // Whether the option is currently expanded. loading?: boolean; // Whether the option is being loaded. canLoadMore?: boolean; // Whether it can load more data. loadMoreError?: boolean; // Whether there was an error loading more conversations. diff --git a/src/addons/messages/services/messages.ts b/src/addons/messages/services/messages.ts index 158756a45..f95558d21 100644 --- a/src/addons/messages/services/messages.ts +++ b/src/addons/messages/services/messages.ts @@ -35,8 +35,6 @@ import { CoreWSError } from '@classes/errors/wserror'; import { AddonNotificationsPreferencesNotificationProcessorState } from '@addons/notifications/services/notifications'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; -const ROOT_CACHE_KEY = 'mmaMessages:'; - declare module '@singletons/events' { /** @@ -57,12 +55,20 @@ declare module '@singletons/events' { } +export const enum AddonMessagesUpdateConversationAction { + MUTE = 'mute', + FAVOURITE = 'favourite', + DELETE = 'delete', +} + /** * Service to handle messages. */ @Injectable({ providedIn: 'root' }) export class AddonMessagesProvider { + protected static readonly ROOT_CACHE_KEY = 'mmaMessages:'; + static readonly NEW_MESSAGE_EVENT = 'addon_messages_new_message_event'; static readonly READ_CHANGED_EVENT = 'addon_messages_read_changed_event'; static readonly OPEN_CONVERSATION_EVENT = 'addon_messages_open_conversation_event'; // Notify a conversation should be opened. @@ -396,7 +402,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForBlockedContacts(userId: number): string { - return ROOT_CACHE_KEY + 'blockedContacts:' + userId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'blockedContacts:' + userId; } /** @@ -405,7 +411,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForContacts(): string { - return ROOT_CACHE_KEY + 'contacts'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'contacts'; } /** @@ -414,7 +420,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForUserContacts(): string { - return ROOT_CACHE_KEY + 'userContacts'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'userContacts'; } /** @@ -423,7 +429,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForContactRequests(): string { - return ROOT_CACHE_KEY + 'contactRequests'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'contactRequests'; } /** @@ -432,7 +438,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForContactRequestsCount(): string { - return ROOT_CACHE_KEY + 'contactRequestsCount'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'contactRequestsCount'; } /** @@ -442,7 +448,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ getCacheKeyForDiscussion(userId: number): string { - return ROOT_CACHE_KEY + 'discussion:' + userId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'discussion:' + userId; } /** @@ -452,7 +458,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForMessageCount(userId: number): string { - return ROOT_CACHE_KEY + 'count:' + userId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'count:' + userId; } /** @@ -461,7 +467,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForUnreadConversationCounts(): string { - return ROOT_CACHE_KEY + 'unreadConversationCounts'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'unreadConversationCounts'; } /** @@ -470,7 +476,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForDiscussions(): string { - return ROOT_CACHE_KEY + 'discussions'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'discussions'; } /** @@ -481,7 +487,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForConversation(userId: number, conversationId: number): string { - return ROOT_CACHE_KEY + 'conversation:' + userId + ':' + conversationId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversation:' + userId + ':' + conversationId; } /** @@ -492,7 +498,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForConversationBetweenUsers(userId: number, otherUserId: number): string { - return ROOT_CACHE_KEY + 'conversationBetweenUsers:' + userId + ':' + otherUserId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversationBetweenUsers:' + userId + ':' + otherUserId; } /** @@ -503,7 +509,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForConversationMembers(userId: number, conversationId: number): string { - return ROOT_CACHE_KEY + 'conversationMembers:' + userId + ':' + conversationId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversationMembers:' + userId + ':' + conversationId; } /** @@ -514,7 +520,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForConversationMessages(userId: number, conversationId: number): string { - return ROOT_CACHE_KEY + 'conversationMessages:' + userId + ':' + conversationId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversationMessages:' + userId + ':' + conversationId; } /** @@ -535,7 +541,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForConversationCounts(): string { - return ROOT_CACHE_KEY + 'conversationCounts'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversationCounts'; } /** @@ -546,7 +552,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForMemberInfo(userId: number, otherUserId: number): string { - return ROOT_CACHE_KEY + 'memberInfo:' + userId + ':' + otherUserId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'memberInfo:' + userId + ':' + otherUserId; } /** @@ -556,7 +562,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getCacheKeyForSelfConversation(userId: number): string { - return ROOT_CACHE_KEY + 'selfconversation:' + userId; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'selfconversation:' + userId; } /** @@ -575,7 +581,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getRootCacheKeyForConversations(): string { - return ROOT_CACHE_KEY + 'conversations:'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'conversations:'; } /** @@ -1132,7 +1138,7 @@ export class AddonMessagesProvider { const result = await site.read( 'core_message_get_conversation_counts', - { }, + {}, preSets, ); @@ -1388,7 +1394,7 @@ export class AddonMessagesProvider { * @returns Cache key. */ protected getMessagePreferencesCacheKey(): string { - return ROOT_CACHE_KEY + 'messagePreferences'; + return AddonMessagesProvider.ROOT_CACHE_KEY + 'messagePreferences'; } /** @@ -2706,7 +2712,7 @@ export class AddonMessagesProvider { * @param conversations Array of conversations. * @returns Conversations sorted with most recent last. */ - sortConversations(conversations: AddonMessagesConversationFormatted[]): AddonMessagesConversationFormatted[] { + sortConversations(conversations: AddonMessagesConversationFormatted[] = []): AddonMessagesConversationFormatted[] { return conversations.sort((a, b) => { const timeA = Number(a.lastmessagedate); const timeB = Number(b.lastmessagedate); @@ -3684,7 +3690,7 @@ export type AddonMessagesNewMessagedEventData = { */ export type AddonMessagesUpdateConversationListEventData = { conversationId: number; - action: string; + action: AddonMessagesUpdateConversationAction; value?: boolean; }; diff --git a/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_22.png b/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_22.png index d6eada1dc1f5a113469f4c3b46da1d92751e77f4..c4749d4017f31425ef417d829078cac8b6888678 100644 GIT binary patch literal 27946 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 From 44f4f9d2a6175d883a5506e7f314ee92286b8aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 16 Apr 2024 14:45:11 +0200 Subject: [PATCH 10/19] MOBILE-4565 calendar: Fix header length --- src/addons/calendar/pages/index/index.html | 3 ++- src/addons/calendar/tests/behat/create_events.feature | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/addons/calendar/pages/index/index.html b/src/addons/calendar/pages/index/index.html index 6e3c382bb..f07174d3f 100644 --- a/src/addons/calendar/pages/index/index.html +++ b/src/addons/calendar/pages/index/index.html @@ -4,7 +4,8 @@ -

{{ (showCalendar ? 'addon.calendar.calendarevents' : 'addon.calendar.upcomingevents') | translate }}

+

{{ 'addon.calendar.calendar' | translate }}

+

{{ 'addon.calendar.upcomingevents' | translate }}

diff --git a/src/addons/calendar/tests/behat/create_events.feature b/src/addons/calendar/tests/behat/create_events.feature index 9b301653c..8a334d941 100755 --- a/src/addons/calendar/tests/behat/create_events.feature +++ b/src/addons/calendar/tests/behat/create_events.feature @@ -43,7 +43,7 @@ Feature: Test creation of calendar events in app And I set the field "Description" to "This is User Event 01 description." in the app And I set the field "Location" to "Barcelona" in the app And I press "Save" in the app - Then I should find "Calendar events" in the app + Then I should find "Calendar" in the app # Verify that event was created right. When I open the calendar for "4" "2025" in the app From 7e6312a9d56ffea91191514e39e95b0f841ceee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 19 Apr 2024 15:32:14 +0200 Subject: [PATCH 11/19] MOBILE-4565 a11y: Fix text color on dark mode --- src/core/features/course/components/module/module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/features/course/components/module/module.scss b/src/core/features/course/components/module/module.scss index d46bf5a78..a57402960 100644 --- a/src/core/features/course/components/module/module.scss +++ b/src/core/features/course/components/module/module.scss @@ -126,7 +126,7 @@ } .activity-extrabadges { - color: var(--gray-700); + color: var(--medium); } .activity-description-availabilityinfo { From 45fb4cb92a3122dc65682f9597837a0eb0f8ee51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 23 Apr 2024 12:52:34 +0200 Subject: [PATCH 12/19] MOBILE-4565 a11y: Fix a lot of focus problems --- .../block/tags/components/tags/tags.scss | 3 + .../blog/pages/edit-entry/edit-entry.html | 2 +- src/addons/blog/pages/index/index.html | 54 ++++++------- src/addons/blog/pages/index/index.scss | 2 +- .../calendar/components/filter/filter.html | 2 +- .../airnotifier/pages/devices/devices.html | 3 +- .../messages/pages/settings/settings.html | 8 +- .../addon-mod-assign-submission.html | 4 +- .../mod/chat/pages/sessions/sessions.html | 2 +- .../mod/data/components/search/search.html | 2 +- .../pages/new-discussion/new-discussion.html | 6 +- src/addons/mod/glossary/pages/edit/edit.html | 6 +- .../workshop/pages/submission/submission.html | 2 +- .../pages/settings/settings.html | 6 +- src/core/components/combobox/combobox.scss | 51 +++++++----- .../core-context-menu-popover.html | 6 +- src/core/components/mod-icon/mod-icon.ts | 2 +- .../components/swipe-slides/swipe-slides.html | 3 +- .../tabs-outlet/core-tabs-outlet.html | 4 +- src/core/components/tabs/tabs.scss | 2 + src/core/features/courses/pages/my/my.scss | 1 - .../features/mainmenu/pages/menu/menu.scss | 79 ++++++++++--------- src/core/features/settings/pages/dev/dev.html | 8 +- .../settings/pages/deviceinfo/deviceinfo.html | 2 +- .../settings/pages/general/general.html | 38 ++++----- .../features/settings/pages/site/site.html | 2 +- .../synchronization/synchronization.html | 2 +- src/core/services/utils/dom.ts | 34 +++++--- src/theme/components/collapsible-header.scss | 11 +++ src/theme/components/error-accordion.scss | 12 +-- src/theme/components/ion-card.scss | 2 + src/theme/components/ion-item.scss | 16 +++- src/theme/components/ion-tab-bar.scss | 6 -- src/theme/helpers/custom.mixins.scss | 60 ++++++++++++-- src/theme/theme.base.scss | 67 ++-------------- src/theme/theme.design-system.scss | 3 + src/theme/theme.scss | 1 - 37 files changed, 277 insertions(+), 237 deletions(-) delete mode 100644 src/theme/components/ion-tab-bar.scss diff --git a/src/addons/block/tags/components/tags/tags.scss b/src/addons/block/tags/components/tags/tags.scss index 2282396da..2de71afde 100644 --- a/src/addons/block/tags/components/tags/tags.scss +++ b/src/addons/block/tags/components/tags/tags.scss @@ -1,3 +1,5 @@ +@use "theme/globals" as *; + :host .core-block-content ::ng-deep { ion-label { max-width: 100%; @@ -31,6 +33,7 @@ vertical-align: baseline; text-decoration: none; border-radius: var(--mdl-shape-borderRadius-xs); + @include core-focus-outline(); } .s20 { font-size: 2.7em; diff --git a/src/addons/blog/pages/edit-entry/edit-entry.html b/src/addons/blog/pages/edit-entry/edit-entry.html index 842cf3400..2c516a75a 100644 --- a/src/addons/blog/pages/edit-entry/edit-entry.html +++ b/src/addons/blog/pages/edit-entry/edit-entry.html @@ -54,7 +54,7 @@
@if (associationsExpanded) { - + @if (associatedModule) { @if (showMyEntriesToggle) { - + {{ 'addon.blog.showonlyyourentries' | translate }} @@ -65,34 +65,32 @@
- -
-
- -
- - @if (tagsEnabled && entry.tags && entry.tags!.length > 0) { - - -
{{ 'core.tag.tags' | translate }}:
- -
-
- } - - @for (file of entry.attachmentfiles; track $index) { - - } - - @if (entry.uniquehash) { - - {{ 'addon.blog.linktooriginalentry' | translate }} - - } - +
+
+
- + + @if (tagsEnabled && entry.tags && entry.tags!.length > 0) { + + +
{{ 'core.tag.tags' | translate }}:
+ +
+
+ } + + @for (file of entry.attachmentfiles; track $index) { + + } + + @if (entry.uniquehash) { + + {{ 'addon.blog.linktooriginalentry' | translate }} + + } + +
@if (entry.lastmodified > entry.created || (entry.userid === currentUserId && entry.publishstate !== 'draft')) { diff --git a/src/addons/blog/pages/index/index.scss b/src/addons/blog/pages/index/index.scss index 16cd7210f..c610046a4 100644 --- a/src/addons/blog/pages/index/index.scss +++ b/src/addons/blog/pages/index/index.scss @@ -7,7 +7,7 @@ } .entry { - border-top: 1px solid var(--stroke); + border-bottom: 1px solid var(--stroke); &-visibility-permission { display: flex; diff --git a/src/addons/calendar/components/filter/filter.html b/src/addons/calendar/components/filter/filter.html index 5d746163a..ba83242d3 100644 --- a/src/addons/calendar/components/filter/filter.html +++ b/src/addons/calendar/components/filter/filter.html @@ -9,7 +9,7 @@ - +