From 7a0f6867d6c0c6150ac18125ed3333c6651c4f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 21 Jan 2021 17:27:52 +0100 Subject: [PATCH] MOBILE-3631 messages: Settings page --- src/addons/messages/messages-lazy.module.ts | 6 + src/addons/messages/messages.module.ts | 8 +- .../group-conversations.page.ts | 3 +- .../messages/pages/settings/settings.html | 145 +++++++++ .../pages/settings/settings.module.ts | 47 +++ .../messages/pages/settings/settings.page.ts | 300 ++++++++++++++++++ .../messages/pages/settings/settings.scss | 10 + .../messages/services/handlers/settings.ts | 60 ++++ 8 files changed, 575 insertions(+), 4 deletions(-) create mode 100644 src/addons/messages/pages/settings/settings.html create mode 100644 src/addons/messages/pages/settings/settings.module.ts create mode 100644 src/addons/messages/pages/settings/settings.page.ts create mode 100644 src/addons/messages/pages/settings/settings.scss create mode 100644 src/addons/messages/services/handlers/settings.ts diff --git a/src/addons/messages/messages-lazy.module.ts b/src/addons/messages/messages-lazy.module.ts index b49237824..dedc69c34 100644 --- a/src/addons/messages/messages-lazy.module.ts +++ b/src/addons/messages/messages-lazy.module.ts @@ -18,6 +18,7 @@ import { RouterModule, ROUTES, Routes } from '@angular/router'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; import { AddonMessagesContactsRoutingModule } from './pages/contacts/messages-contacts-routing.module'; import { AddonMessagesIndexRoutingModule } from './pages/index-35/messages-index-routing.module'; +import { AddonMessagesSettingsHandlerService } from './services/handlers/settings'; function buildRoutes(injector: Injector): Routes { return [ @@ -35,6 +36,11 @@ function buildRoutes(injector: Injector): Routes { loadChildren: () => import('./pages/search/search.module') .then(m => m.AddonMessagesSearchPageModule), }, + { + path: AddonMessagesSettingsHandlerService.PAGE_NAME, + loadChildren: () => import('./pages/settings/settings.module') + .then(m => m.AddonMessagesSettingsPageModule), + }, { path: 'contacts', // 3.6 or greater. loadChildren: () => import('./pages/contacts/contacts.module') diff --git a/src/addons/messages/messages.module.ts b/src/addons/messages/messages.module.ts index 7173c6296..4cb26505b 100644 --- a/src/addons/messages/messages.module.ts +++ b/src/addons/messages/messages.module.ts @@ -21,6 +21,9 @@ import { CORE_SITE_SCHEMAS } from '@services/sites'; import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate'; import { AddonMessagesMainMenuHandler, AddonMessagesMainMenuHandlerService } from './services/handlers/mainmenu'; import { CoreCronDelegate } from '@services/cron'; +import { CoreSettingsDelegate } from '@features/settings/services/settings-delegate'; +import { AddonMessagesSettingsHandler } from './services/handlers/settings'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; const mainMenuChildrenRoutes: Routes = [ { @@ -32,6 +35,7 @@ const mainMenuChildrenRoutes: Routes = [ @NgModule({ imports: [ CoreMainMenuRoutingModule.forChild({ children: mainMenuChildrenRoutes }), + CoreMainMenuTabRoutingModule.forChild( mainMenuChildrenRoutes), ], providers: [ { @@ -46,6 +50,7 @@ const mainMenuChildrenRoutes: Routes = [ useFactory: () => () => { CoreMainMenuDelegate.instance.registerHandler(AddonMessagesMainMenuHandler.instance); CoreCronDelegate.instance.register(AddonMessagesMainMenuHandler.instance); + CoreSettingsDelegate.instance.registerHandler(AddonMessagesSettingsHandler.instance); }, }, @@ -67,8 +72,6 @@ export class AddonMessagesModule { messagesProvider: AddonMessagesProvider, sitesProvider: CoreSitesProvider, linkHelper: CoreContentLinksHelperProvider, - settingsHandler: AddonMessagesSettingsHandler, - settingsDelegate: CoreSettingsDelegate, pushNotificationsDelegate: CorePushNotificationsDelegate, addContactHandler: AddonMessagesAddContactUserHandler, blockContactHandler: AddonMessagesBlockContactUserHandler, @@ -83,7 +86,6 @@ export class AddonMessagesModule { userDelegate.registerHandler(addContactHandler); userDelegate.registerHandler(blockContactHandler); cronDelegate.register(syncHandler); - settingsDelegate.registerHandler(settingsHandler); pushNotificationsDelegate.registerClickHandler(pushClickHandler); // Sync some discussions when device goes online. diff --git a/src/addons/messages/pages/group-conversations/group-conversations.page.ts b/src/addons/messages/pages/group-conversations/group-conversations.page.ts index fc50c1a7d..df925f4bf 100644 --- a/src/addons/messages/pages/group-conversations/group-conversations.page.ts +++ b/src/addons/messages/pages/group-conversations/group-conversations.page.ts @@ -38,6 +38,7 @@ import { AddonMessagesOfflineConversationMessagesDBRecordFormatted, AddonMessagesOfflineMessagesDBRecordFormatted, } from '@addons/messages/services/database/messages'; +import { AddonMessagesSettingsHandlerService } from '@addons/messages/services/handlers/settings'; // import { CoreSplitViewComponent } from '@components/split-view/split-view'; /** @@ -522,7 +523,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { */ gotoSettings(): void { // @todo this.splitviewCtrl.push - CoreNavigator.instance.navigateToSitePath('settings'); + CoreNavigator.instance.navigateToSitePath(AddonMessagesSettingsHandlerService.PAGE_NAME); } /** diff --git a/src/addons/messages/pages/settings/settings.html b/src/addons/messages/pages/settings/settings.html new file mode 100644 index 000000000..f1fa655a1 --- /dev/null +++ b/src/addons/messages/pages/settings/settings.html @@ -0,0 +1,145 @@ + + + + + + {{ 'addon.messages.messages' | translate }} + + + + + + + + + + + {{ 'addon.messages.blocknoncontacts' | translate }} + + + + + + + +

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

+
+ + {{ 'addon.messages.contactableprivacy_onlycontacts' | translate }} + + + + {{ 'addon.messages.contactableprivacy_coursemember' | translate }} + + + + {{ 'addon.messages.contactableprivacy_site' | translate }} + + +
+
+
+ + + +
+ + + + + +

{{ notification.displayname }}

+
+ +

{{ 'core.settings.loggedin' | translate }}

+
+ +

{{ 'core.settings.loggedoff' | translate }}

+
+
+

{{ 'addon.notifications.notificationpreferences' | translate }}

+
+
+ + + + + {{ processor.displayname }} + + + + + {{ processor.lockedmessage }} + + + {{ 'core.settings.disabled' | translate }} + + + + + + + + {{ processor.displayname }} + + + + + +
+ {{'core.settings.locked' | translate }} +
+ + {{ 'core.settings.disabled' | translate }} +
+
+ + + {{ processor.displayname }} + + + + {{ 'core.settings.' + state | translate }} + + + + + + {{'core.settings.locked' | translate }} + + + {{ 'core.settings.disabled' | translate }} + + +
+
+
+
+
+ + + + +

{{ 'core.settings.general' | translate }}

+ + +

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

+
+ +
+
+
+
+
diff --git a/src/addons/messages/pages/settings/settings.module.ts b/src/addons/messages/pages/settings/settings.module.ts new file mode 100644 index 000000000..ef42c4058 --- /dev/null +++ b/src/addons/messages/pages/settings/settings.module.ts @@ -0,0 +1,47 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { CoreSharedModule } from '@/core/shared.module'; + +import { AddonMessagesSettingsPage } from './settings.page'; + +const routes: Routes = [ + { + path: '', + component: AddonMessagesSettingsPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreSharedModule, + ], + declarations: [ + AddonMessagesSettingsPage, + ], + exports: [RouterModule], +}) +export class AddonMessagesSettingsPageModule {} diff --git a/src/addons/messages/pages/settings/settings.page.ts b/src/addons/messages/pages/settings/settings.page.ts new file mode 100644 index 000000000..4e540d70d --- /dev/null +++ b/src/addons/messages/pages/settings/settings.page.ts @@ -0,0 +1,300 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { + AddonMessagesProvider, AddonMessagesMessagePreferences, + AddonMessagesMessagePreferencesNotification, + AddonMessagesMessagePreferencesNotificationProcessor, + AddonMessages, +} from '../../services/messages'; +import { CoreUser } from '@features/user/services/user'; +import { CoreApp } from '@services/app'; +import { CoreConfig } from '@services/config'; +import { CoreEvents } from '@singletons/events'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreConstants } from '@/core/constants'; +import { IonRefresher } from '@ionic/angular'; + +/** + * Page that displays the messages settings page. + */ +@Component({ + selector: 'page-addon-messages-settings', + templateUrl: 'settings.html', + styleUrls: ['settings.scss'], +}) +export class AddonMessagesSettingsPage implements OnInit, OnDestroy { + + protected updateTimeout?: number; + + preferences?: AddonMessagesMessagePreferences; + preferencesLoaded = false; + contactablePrivacy?: number | boolean; + advancedContactable = false; // Whether the site supports "advanced" contactable privacy. + allowSiteMessaging = false; + onlyContactsValue = AddonMessagesProvider.MESSAGE_PRIVACY_ONLYCONTACTS; + courseMemberValue = AddonMessagesProvider.MESSAGE_PRIVACY_COURSEMEMBER; + siteValue = AddonMessagesProvider.MESSAGE_PRIVACY_SITE; + groupMessagingEnabled = false; + sendOnEnter = false; + + protected previousContactableValue?: number | boolean; + + constructor() { + + const currentSite = CoreSites.instance.getCurrentSite(); + this.advancedContactable = !!currentSite?.isVersionGreaterEqualThan('3.6'); + this.allowSiteMessaging = !!currentSite?.canUseAdvancedFeature('messagingallusers'); + this.groupMessagingEnabled = AddonMessages.instance.isGroupMessagingEnabled(); + + this.asyncInit(); + } + + protected async asyncInit(): Promise { + this.sendOnEnter = !!(await CoreConfig.instance.get(CoreConstants.SETTINGS_SEND_ON_ENTER, !CoreApp.instance.isMobile())); + } + + /** + * Runs when the page has loaded. This event only happens once per page being created. + * If a page leaves but is cached, then this event will not fire again on a subsequent viewing. + * Setup code for the page. + */ + ngOnInit(): void { + this.fetchPreferences(); + } + + /** + * Fetches preference data. + * + * @return Promise resolved when done. + */ + protected async fetchPreferences(): Promise { + try { + const preferences = await AddonMessages.instance.getMessagePreferences(); + if (this.groupMessagingEnabled) { + // Simplify the preferences. + for (const component of preferences.components) { + // Only display get the notification preferences. + component.notifications = component.notifications.filter((notification) => + notification.preferencekey == AddonMessagesProvider.NOTIFICATION_PREFERENCES_KEY); + + component.notifications.forEach((notification) => { + notification.processors.forEach( + (processor: AddonMessagesMessagePreferencesNotificationProcessorFormatted) => { + processor.checked = processor.loggedin.checked || processor.loggedoff.checked; + }, + ); + }); + } + } + + this.preferences = preferences; + this.contactablePrivacy = preferences.blocknoncontacts; + this.previousContactableValue = this.contactablePrivacy; + } catch (error) { + CoreDomUtils.instance.showErrorModal(error); + } finally { + this.preferencesLoaded = true; + } + } + + /** + * Update preferences. The purpose is to store the updated data, it won't be reflected in the view. + */ + protected updatePreferences(): void { + AddonMessages.instance.invalidateMessagePreferences().finally(() => { + this.fetchPreferences(); + }); + } + + /** + * Update preferences after a certain time. The purpose is to store the updated data, it won't be reflected in the view. + */ + protected updatePreferencesAfterDelay(): void { + // Cancel pending updates. + clearTimeout(this.updateTimeout); + + this.updateTimeout = window.setTimeout(() => { + this.updateTimeout = undefined; + this.updatePreferences(); + }, 5000); + } + + /** + * Save the contactable privacy setting.. + * + * @param value The value to set. + */ + async saveContactablePrivacy(value?: number | boolean): Promise { + if (this.contactablePrivacy == this.previousContactableValue) { + // Value hasn't changed from previous, it probably means that we just fetched the value from the server. + return; + } + + const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true); + + if (!this.advancedContactable) { + // Convert from boolean to number. + value = value ? 1 : 0; + } + + try { + await CoreUser.instance.updateUserPreference('message_blocknoncontacts', String(value)); + // Update the preferences since they were modified. + this.updatePreferencesAfterDelay(); + this.previousContactableValue = this.contactablePrivacy; + } catch (message) { + // Show error and revert change. + CoreDomUtils.instance.showErrorModal(message); + this.contactablePrivacy = this.previousContactableValue; + } finally { + modal.dismiss(); + } + } + + /** + * Change the value of a certain preference. + * + * @param notification Notification object. + * @param state State name, ['loggedin', 'loggedoff']. + * @param processor Notification processor. + */ + async changePreference( + notification: AddonMessagesMessagePreferencesNotificationFormatted, + state: string, + processor: AddonMessagesMessagePreferencesNotificationProcessorFormatted, + ): Promise { + + const valueArray: string[] = []; + let value = 'none'; + + if (this.groupMessagingEnabled) { + // Update both states at the same time. + const promises: Promise[] = []; + + notification.processors.forEach((processor: AddonMessagesMessagePreferencesNotificationProcessorFormatted) => { + if (processor.checked) { + valueArray.push(processor.name); + } + }); + + if (value.length > 0) { + value = valueArray.join(','); + } + + notification.updating = true; + + promises.push(CoreUser.instance.updateUserPreference(notification.preferencekey + '_loggedin', value)); + promises.push(CoreUser.instance.updateUserPreference(notification.preferencekey + '_loggedoff', value)); + + try { + await Promise.all(promises); + // Update the preferences since they were modified. + this.updatePreferencesAfterDelay(); + } catch (error) { + // Show error and revert change. + CoreDomUtils.instance.showErrorModal(error); + processor.checked = !processor.checked; + } finally { + notification.updating = false; + } + + return; + } + + // Update only the specified state. + const processorState = processor[state]; + const preferenceName = notification.preferencekey + '_' + processorState.name; + + notification.processors.forEach((processor) => { + if (processor[state].checked) { + valueArray.push(processor.name); + } + }); + + if (value.length > 0) { + value = valueArray.join(','); + } + + if (!notification.updating) { + notification.updating = {}; + } + + notification.updating[state] = true; + try { + await CoreUser.instance.updateUserPreference(preferenceName, value); + // Update the preferences since they were modified. + this.updatePreferencesAfterDelay(); + } catch (error) { + // Show error and revert change. + CoreDomUtils.instance.showErrorModal(error); + processorState.checked = !processorState.checked; + } finally { + notification.updating[state] = false; + } + } + + /** + * Refresh the list of preferences. + * + * @param refresher Refresher. + */ + refreshPreferences(refresher?: CustomEvent): void { + AddonMessages.instance.invalidateMessagePreferences().finally(() => { + this.fetchPreferences().finally(() => { + refresher?.detail.complete(); + }); + }); + } + + sendOnEnterChanged(): void { + // Save the value. + CoreConfig.instance.set(CoreConstants.SETTINGS_SEND_ON_ENTER, this.sendOnEnter ? 1 : 0); + + // Notify the app. + CoreEvents.trigger( + CoreEvents.SEND_ON_ENTER_CHANGED, + { sendOnEnter: !!this.sendOnEnter }, + CoreSites.instance.getCurrentSiteId(), + ); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + // If there is a pending action to update preferences, execute it right now. + if (this.updateTimeout) { + clearTimeout(this.updateTimeout); + this.updatePreferences(); + } + } + +} + +/** + * Message preferences notification with some caclulated data. + */ +type AddonMessagesMessagePreferencesNotificationFormatted = AddonMessagesMessagePreferencesNotification & { + updating?: boolean | {[state: string]: boolean}; // Calculated in the app. Whether the notification is being updated. +}; + +/** + * Message preferences notification processor with some caclulated data. + */ +type AddonMessagesMessagePreferencesNotificationProcessorFormatted = AddonMessagesMessagePreferencesNotificationProcessor & { + checked?: boolean; // Calculated in the app. Whether the processor is checked either for loggedin or loggedoff. +}; diff --git a/src/addons/messages/pages/settings/settings.scss b/src/addons/messages/pages/settings/settings.scss new file mode 100644 index 000000000..138395573 --- /dev/null +++ b/src/addons/messages/pages/settings/settings.scss @@ -0,0 +1,10 @@ +ion-app.app-root page-addon-messages-settings { + .list-header { + margin-bottom: 0; + border-top: 0; + } + + .toggle { + display: inline-block; + } +} \ No newline at end of file diff --git a/src/addons/messages/services/handlers/settings.ts b/src/addons/messages/services/handlers/settings.ts new file mode 100644 index 000000000..35cfd9435 --- /dev/null +++ b/src/addons/messages/services/handlers/settings.ts @@ -0,0 +1,60 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate'; +import { makeSingleton } from '@singletons'; +import { AddonMessages } from '../messages'; +import { AddonMessagesMainMenuHandlerService } from './mainmenu'; + +/** + * Message settings handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonMessagesSettingsHandlerService implements CoreSettingsHandler { + + static readonly PAGE_NAME = 'settings'; + + name = 'AddonMessages'; + priority = 600; + + /** + * Check if the handler is enabled on a site level. + * + * @return Whether or not the handler is enabled on a site level. + */ + async isEnabled(): Promise { + const messagingEnabled = await AddonMessages.instance.isPluginEnabled(); + + return messagingEnabled && AddonMessages.instance.isMessagePreferencesEnabled(); + } + + /** + * Returns the data needed to render the handler. + * + * @return Data needed to render the handler. + */ + getDisplayData(): CoreSettingsHandlerData { + return { + icon: 'fas-comments', + title: 'addon.messages.messages', + page: AddonMessagesMainMenuHandlerService.PAGE_NAME + '/' + AddonMessagesSettingsHandlerService.PAGE_NAME, + class: 'addon-messages-settings-handler', + }; + } + +} + +export class AddonMessagesSettingsHandler extends makeSingleton(AddonMessagesSettingsHandlerService) {} +