From 3d795ea39f472538e529b7e30efc3f7859397c42 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 19 Jan 2018 09:00:59 +0100 Subject: [PATCH] MOBILE-2309 sitehome: Implement provider and register handler --- src/app/app.module.ts | 2 + src/components/tabs/tabs.html | 26 +++-- src/components/tabs/tabs.ts | 63 ++++++++--- src/core/courses/courses.module.ts | 2 +- .../pages/my-overview/my-overview.html | 7 +- .../courses/pages/my-overview/my-overview.ts | 18 ++- .../{handlers.ts => mainmenu-handler.ts} | 2 +- src/core/login/providers/helper.ts | 82 -------------- src/core/mainmenu/pages/menu/menu.html | 2 +- src/core/mainmenu/pages/menu/menu.ts | 18 ++- src/core/mainmenu/providers/delegate.ts | 20 +++- src/core/sitehome/lang/en.json | 4 + .../sitehome/providers/mainmenu-handler.ts | 62 +++++++++++ src/core/sitehome/providers/sitehome.ts | 104 ++++++++++++++++++ src/core/sitehome/sitehome.module.ts | 34 ++++++ 15 files changed, 325 insertions(+), 121 deletions(-) rename src/core/courses/providers/{handlers.ts => mainmenu-handler.ts} (97%) create mode 100644 src/core/sitehome/lang/en.json create mode 100644 src/core/sitehome/providers/mainmenu-handler.ts create mode 100644 src/core/sitehome/providers/sitehome.ts create mode 100644 src/core/sitehome/sitehome.module.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 12c7bfd48..0ea058335 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -57,6 +57,7 @@ import { CoreCoursesModule } from '../core/courses/courses.module'; import { CoreFileUploaderModule } from '../core/fileuploader/fileuploader.module'; import { CoreSharedFilesModule } from '../core/sharedfiles/sharedfiles.module'; import { CoreCourseModule } from '../core/course/course.module'; +import { CoreSiteHomeModule } from '../core/sitehome/sitehome.module'; // Addon modules. import { AddonCalendarModule } from '../addon/calendar/calendar.module'; @@ -92,6 +93,7 @@ export function createTranslateLoader(http: HttpClient) { CoreFileUploaderModule, CoreSharedFilesModule, CoreCourseModule, + CoreSiteHomeModule, AddonCalendarModule ], bootstrap: [IonicApp], diff --git a/src/components/tabs/tabs.html b/src/components/tabs/tabs.html index 9bac58750..25ef4700e 100644 --- a/src/components/tabs/tabs.html +++ b/src/components/tabs/tabs.html @@ -1,12 +1,14 @@ -
- - - - {{ tab.title }} - {{tab.badge}} - - -
-
- -
\ No newline at end of file + + +
+ +
+
\ No newline at end of file diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 93ab502fa..54c23b204 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; +import { Component, Input, Output, EventEmitter, OnInit, OnChanges, AfterViewInit, ViewChild, ElementRef, + SimpleChange } from '@angular/core'; import { CoreTabComponent } from './tab'; /** @@ -35,14 +36,17 @@ import { CoreTabComponent } from './tab'; selector: 'core-tabs', templateUrl: 'tabs.html' }) -export class CoreTabsComponent implements OnInit, AfterViewInit { +export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges { @Input() selectedIndex?: number = 0; // Index of the tab to select. + @Input() hideUntil: boolean; // Determine when should the contents be shown. @Output() ionChange: EventEmitter = new EventEmitter(); // Emitted when the tab changes. @ViewChild('originalTabs') originalTabsRef: ElementRef; tabs: CoreTabComponent[] = []; // List of tabs. selected: number; // Selected tab number. protected originalTabsContainer: HTMLElement; // The container of the original tabs. It will include each tab's content. + protected initialized = false; + protected afterViewInitTriggered = false; constructor() {} @@ -57,22 +61,24 @@ export class CoreTabsComponent implements OnInit, AfterViewInit { * View has been initialized. */ ngAfterViewInit() { - let selectedIndex = this.selectedIndex || 0, - selectedTab = this.tabs[selectedIndex]; - - if (!selectedTab.enabled || !selectedTab.show) { - // The tab is not enabled or not shown. Get the first tab that is enabled. - selectedTab = this.tabs.find((tab, index) => { - if (tab.enabled && tab.show) { - selectedIndex = index; - return true; - } - return false; - }); + this.afterViewInitTriggered = true; + if (!this.initialized && this.hideUntil) { + // Tabs should be shown, initialize them. + this.initializeTabs(); } + } - if (selectedTab) { - this.selectTab(selectedIndex); + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}) { + // We need to wait for ngAfterViewInit because we need core-tab components to be executed. + if (!this.initialized && this.hideUntil && this.afterViewInitTriggered) { + // Tabs should be shown, initialize them. + // Use a setTimeout so child core-tab update their inputs before initializing the tabs. + setTimeout(() => { + this.initializeTabs(); + }); } } @@ -114,6 +120,31 @@ export class CoreTabsComponent implements OnInit, AfterViewInit { return this.tabs[this.selected]; } + /** + * Initialize the tabs, determining the first tab to be shown. + */ + protected initializeTabs() : void { + let selectedIndex = this.selectedIndex || 0, + selectedTab = this.tabs[selectedIndex]; + + if (!selectedTab.enabled || !selectedTab.show) { + // The tab is not enabled or not shown. Get the first tab that is enabled. + selectedTab = this.tabs.find((tab, index) => { + if (tab.enabled && tab.show) { + selectedIndex = index; + return true; + } + return false; + }); + } + + if (selectedTab) { + this.selectTab(selectedIndex); + } + + this.initialized = true; + } + /** * Remove a tab from the list of tabs. * diff --git a/src/core/courses/courses.module.ts b/src/core/courses/courses.module.ts index 39d01fba1..8dfd4bd1f 100644 --- a/src/core/courses/courses.module.ts +++ b/src/core/courses/courses.module.ts @@ -14,7 +14,7 @@ import { NgModule } from '@angular/core'; import { CoreCoursesProvider } from './providers/courses'; -import { CoreCoursesMainMenuHandler } from './providers/handlers'; +import { CoreCoursesMainMenuHandler } from './providers/mainmenu-handler'; import { CoreCoursesMyOverviewProvider } from './providers/my-overview'; import { CoreCoursesDelegate } from './providers/delegate'; import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; diff --git a/src/core/courses/pages/my-overview/my-overview.html b/src/core/courses/pages/my-overview/my-overview.html index 4744a669e..7980e8c02 100644 --- a/src/core/courses/pages/my-overview/my-overview.html +++ b/src/core/courses/pages/my-overview/my-overview.html @@ -17,7 +17,12 @@ - + + + + + +
diff --git a/src/core/courses/pages/my-overview/my-overview.ts b/src/core/courses/pages/my-overview/my-overview.ts index fb5183d40..f9e698ba4 100644 --- a/src/core/courses/pages/my-overview/my-overview.ts +++ b/src/core/courses/pages/my-overview/my-overview.ts @@ -14,10 +14,12 @@ import { Component, OnDestroy } from '@angular/core'; import { IonicPage, NavController } from 'ionic-angular'; +import { CoreSitesProvider } from '../../../../providers/sites'; import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; import { CoreCoursesProvider } from '../../providers/courses'; import { CoreCoursesMyOverviewProvider } from '../../providers/my-overview'; import { CoreCourseHelperProvider } from '../../../course/providers/helper'; +import { CoreSiteHomeProvider } from '../../../sitehome/providers/sitehome'; import * as moment from 'moment'; /** @@ -29,6 +31,9 @@ import * as moment from 'moment'; templateUrl: 'my-overview.html', }) export class CoreCoursesMyOverviewPage implements OnDestroy { + firstSelectedTab: number; + siteHomeEnabled: boolean; + tabsReady: boolean = false; tabShown = 'courses'; timeline = { sort: 'sortbydates', @@ -64,13 +69,24 @@ export class CoreCoursesMyOverviewPage implements OnDestroy { constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider, private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider, - private courseHelper: CoreCourseHelperProvider) {} + private courseHelper: CoreCourseHelperProvider, private sitesProvider: CoreSitesProvider, + private siteHomeProvider: CoreSiteHomeProvider) {} /** * View loaded. */ ionViewDidLoad() { this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); + + // Decide which tab to load first. + this.siteHomeProvider.isAvailable().then((enabled) => { + let site = this.sitesProvider.getCurrentSite(), + displaySiteHome = site.getInfo() && site.getInfo().userhomepage === 0; + + this.siteHomeEnabled = enabled; + this.firstSelectedTab = displaySiteHome ? 0 : 2; + this.tabsReady = true; + }); } /** diff --git a/src/core/courses/providers/handlers.ts b/src/core/courses/providers/mainmenu-handler.ts similarity index 97% rename from src/core/courses/providers/handlers.ts rename to src/core/courses/providers/mainmenu-handler.ts index f09078432..3dad056d8 100644 --- a/src/core/courses/providers/handlers.ts +++ b/src/core/courses/providers/mainmenu-handler.ts @@ -18,7 +18,7 @@ import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/pro import { CoreCoursesMyOverviewProvider } from '../providers/my-overview'; /** - * Handler to inject an option into main menu. + * Handler to add My Courses or My Overview into main menu. */ @Injectable() export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler { diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 77faf9dbc..ba8dd8080 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -404,49 +404,6 @@ export class CoreLoginHelperProvider { */ goToSiteInitialPage() : Promise { return this.appProvider.getRootNavController().setRoot('CoreMainMenuPage'); - // return this.isMyOverviewEnabled().then((myOverview) => { - // let myCourses = !myOverview && this.isMyCoursesEnabled(), - // site = this.sitesProvider.getCurrentSite(), - // promise; - - // if (!site) { - // return Promise.reject(null); - // } - - // // Check if frontpage is needed to be shown. (If configured or if any of the other avalaible). - // if ((site.getInfo() && site.getInfo().userhomepage === 0) || (!myCourses && !myOverview)) { - // promise = this.isFrontpageEnabled(); - // } else { - // promise = Promise.resolve(false); - // } - - // return promise.then((frontpage) => { - // // Check avalaibility in priority order. - // let pageName, - // params; - - // // @todo Use real pages names when they are implemented. - // if (frontpage) { - // pageName = 'Frontpage'; - // } else if (myOverview) { - // pageName = 'MyOverview'; - // } else if (myCourses) { - // pageName = 'MyCourses'; - // } else { - // // Anything else available, go to the user profile. - // pageName = 'User'; - // params = { - // userId: site.getUserId() - // }; - // } - - // if (setRoot) { - // return navCtrl.setRoot(pageName, params, {animate: false}); - // } else { - // return navCtrl.push(pageName, params); - // } - // }); - // }); } /** @@ -547,45 +504,6 @@ export class CoreLoginHelperProvider { return !!CoreConfigConstants.siteurl; } - /** - * Check if the app is configured to use a fixed URL (only 1). - * - * @return {Promise} Promise resolved with boolean: whether there is 1 fixed URL. - */ - protected isFrontpageEnabled() : Promise { - // var $mmaFrontpage = $mmAddonManager.get('$mmaFrontpage'); - // if ($mmaFrontpage && !$mmaFrontpage.isDisabledInSite()) { - // return $mmaFrontpage.isFrontpageAvailable().then(() => { - // return true; - // }).catch(() => { - // return false; - // }); - // } - // @todo: Implement it when front page is implemented. - return Promise.resolve(false); - } - - /** - * Check if My Courses is enabled. - * - * @return {boolean} Whether My Courses is enabled. - */ - protected isMyCoursesEnabled() : boolean { - // @todo: Implement it when My Courses is implemented. - return false; - // return !$mmCourses.isMyCoursesDisabledInSite(); - } - - /** - * Check if My Overview is enabled. - * - * @return {Promise} Promise resolved with boolean: whether My Overview is enabled. - */ - protected isMyOverviewEnabled() : Promise { - // @todo: Implement it when My Overview is implemented. - return Promise.resolve(false); - } - /** * Check if current site is logged out, triggering mmCoreEventSessionExpired if it is. * diff --git a/src/core/mainmenu/pages/menu/menu.html b/src/core/mainmenu/pages/menu/menu.html index 4e56c064d..241edcbf3 100644 --- a/src/core/mainmenu/pages/menu/menu.html +++ b/src/core/mainmenu/pages/menu/menu.html @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/src/core/mainmenu/pages/menu/menu.ts b/src/core/mainmenu/pages/menu/menu.ts index d319918f1..372b7dd5f 100644 --- a/src/core/mainmenu/pages/menu/menu.ts +++ b/src/core/mainmenu/pages/menu/menu.ts @@ -54,6 +54,7 @@ export class CoreMainMenuPage implements OnDestroy { loaded: boolean; redirectPage: string; redirectParams: any; + initialTab: number; protected subscription; protected moreTabData = { @@ -79,25 +80,36 @@ export class CoreMainMenuPage implements OnDestroy { return; } + let site = this.sitesProvider.getCurrentSite(), + displaySiteHome = site.getInfo() && site.getInfo().userhomepage === 0; + this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => { handlers = handlers.slice(0, CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Get main handlers. // Check if handlers are already in tabs. Add the ones that aren't. // @todo: https://github.com/ionic-team/ionic/issues/13633 - for (let i in handlers) { + for (let i = 0; i < handlers.length; i++) { let handler = handlers[i], - found = false; + found = false, + shouldSelect = (displaySiteHome && handler.name == 'CoreSiteHome') || + (!displaySiteHome && handler.name == 'CoreCourses'); - for (let j in this.tabs) { + for (let j = 0; j < this.tabs.length; j++) { let tab = this.tabs[j]; if (tab.title == handler.title && tab.icon == handler.icon) { found = true; + if (shouldSelect) { + this.initialTab = j; + } break; } } if (!found) { this.tabs.push(handler); + if (shouldSelect) { + this.initialTab = this.tabs.length; + } } } diff --git a/src/core/mainmenu/providers/delegate.ts b/src/core/mainmenu/providers/delegate.ts index 773615f58..a6edd6ee6 100644 --- a/src/core/mainmenu/providers/delegate.ts +++ b/src/core/mainmenu/providers/delegate.ts @@ -79,6 +79,18 @@ export interface CoreMainMenuHandlerData { class?: string; }; +/** + * Data returned by the delegate for each handler. + */ +export interface CoreMainMenuHandlerToDisplay extends CoreMainMenuHandlerData { + + /** + * Name of the handler. + * @type {string} + */ + name?: string; +}; + /** * Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin * and notify an update in the data. @@ -90,7 +102,7 @@ export class CoreMainMenuDelegate { protected enabledHandlers: {[s: string]: CoreMainMenuHandler} = {}; protected loaded = false; protected lastUpdateHandlersStart: number; - protected siteHandlers: Subject = new BehaviorSubject([]); + protected siteHandlers: Subject = new BehaviorSubject([]); constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) { this.logger = logger.getInstance('CoreMainMenuDelegate'); @@ -123,7 +135,7 @@ export class CoreMainMenuDelegate { * * @return {Subject} An observable that will receive the handlers. */ - getHandlers() : Subject { + getHandlers() : Subject { return this.siteHandlers; } @@ -223,7 +235,9 @@ export class CoreMainMenuDelegate { for (let name in this.enabledHandlers) { let handler = this.enabledHandlers[name], - data = handler.getDisplayData(); + data: CoreMainMenuHandlerToDisplay = handler.getDisplayData(); + + data.name = handler.name; handlersData.push({ data: data, diff --git a/src/core/sitehome/lang/en.json b/src/core/sitehome/lang/en.json new file mode 100644 index 000000000..639239276 --- /dev/null +++ b/src/core/sitehome/lang/en.json @@ -0,0 +1,4 @@ +{ + "sitehome": "Site home", + "sitenews": "Site announcements" +} \ No newline at end of file diff --git a/src/core/sitehome/providers/mainmenu-handler.ts b/src/core/sitehome/providers/mainmenu-handler.ts new file mode 100644 index 000000000..88d8c28a3 --- /dev/null +++ b/src/core/sitehome/providers/mainmenu-handler.ts @@ -0,0 +1,62 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSiteHomeProvider } from './sitehome'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/providers/delegate'; +import { CoreCoursesMyOverviewProvider } from '../../courses/providers/my-overview'; + +/** + * Handler to add Site Home into main menu. + */ +@Injectable() +export class CoreSiteHomeMainMenuHandler implements CoreMainMenuHandler { + name = 'CoreSiteHome'; + priority = 1000; + isOverviewEnabled: boolean; + + constructor(private siteHomeProvider: CoreSiteHomeProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider) {} + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean|Promise { + // Check if my overview is enabled. + return this.myOverviewProvider.isEnabled().then((enabled) => { + if (enabled) { + // My overview is enabled, Site Home will be inside the overview page. + return false; + } + + // My overview not enabled, check if site home is enabled. + return this.siteHomeProvider.isAvailable(); + }); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreMainMenuHandlerData} Data needed to render the handler. + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + icon: 'home', + title: 'core.sitehome.sitehome', + page: 'CoreSiteHomeIndexPage', + class: 'core-sitehome-handler' + }; + } +} diff --git a/src/core/sitehome/providers/sitehome.ts b/src/core/sitehome/providers/sitehome.ts new file mode 100644 index 000000000..4fffeba3b --- /dev/null +++ b/src/core/sitehome/providers/sitehome.ts @@ -0,0 +1,104 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreSite } from '../../../classes/site'; +import { CoreCourseProvider } from '../../course/providers/course'; + +/** + * Service that provides some features regarding site home. + */ +@Injectable() +export class CoreSiteHomeProvider { + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider) { + this.logger = logger.getInstance('CoreSiteHomeProvider'); + } + + /** + * Returns whether or not the frontpage is available for the current site. + * + * @param {string} [siteId] The site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether it's available. + */ + isAvailable(siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // First check if it's disabled. + if (this.isDisabledInSite(site)) { + return false; + } + + // Use a WS call to check if there's content in the site home. + const siteHomeId = site.getSiteHomeId(), + preSets = {emergencyCache: false}; + + this.logger.debug('Using WS call to check if site home is available.'); + + return this.courseProvider.getSections(siteHomeId, false, true, preSets, site.id).then((sections) : any => { + if (!sections || !sections.length) { + return Promise.reject(null); + } + + for (let i = 0; i < sections.length; i++) { + let section = sections[i]; + if (section.summary || (section.modules && section.modules.length)) { + // It has content, return true. + return true; + } + } + + return Promise.reject(null); + }).catch(() => { + const config = site.getStoredConfig(); + if (config && config.frontpageloggedin) { + const items = config.frontpageloggedin.split(','); + if (items.length > 0) { + // It's enabled. + return true; + } + } + + return false; + }); + }).catch(() => { + return false; + }); + } + + /** + * Check if Site Home is disabled in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + isDisabled(siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isDisabledInSite(site); + }); + } + + /** + * Check if Site Home is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isDisabledInSite(site: CoreSite) : boolean { + site = site || this.sitesProvider.getCurrentSite(); + return site.isFeatureDisabled('$mmSideMenuDelegate_mmaFrontpage'); + } +} diff --git a/src/core/sitehome/sitehome.module.ts b/src/core/sitehome/sitehome.module.ts new file mode 100644 index 000000000..fc41faded --- /dev/null +++ b/src/core/sitehome/sitehome.module.ts @@ -0,0 +1,34 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSiteHomeProvider } from './providers/sitehome'; +import { CoreSiteHomeMainMenuHandler } from './providers/mainmenu-handler'; +import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate'; + +@NgModule({ + declarations: [], + imports: [ + ], + providers: [ + CoreSiteHomeProvider, + CoreSiteHomeMainMenuHandler + ], + exports: [] +}) +export class CoreSiteHomeModule { + constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreSiteHomeMainMenuHandler) { + mainMenuDelegate.registerHandler(mainMenuHandler); + } +}