diff --git a/.changeset/spotty-ducks-cheer.md b/.changeset/spotty-ducks-cheer.md
new file mode 100644
index 00000000..c138d6be
--- /dev/null
+++ b/.changeset/spotty-ducks-cheer.md
@@ -0,0 +1,9 @@
+---
+'@openstapps/app': minor
+---
+
+Require full reload for setting & language changes
+
+Setting changes are relatively rare, so it makes little sense
+going through the effort of ensuring everything is reactive to
+language changes as well as creating all the pipe bindings etc.
diff --git a/README.md b/README.md
index 85a81111..dc260e21 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Open StApps Monorepo
+#
Open StApps Monorepo
Refer to the [contribution guide](./CONTRIBUTING.md)
diff --git a/frontend/app/package.json b/frontend/app/package.json
index 26495fe9..65d49d85 100644
--- a/frontend/app/package.json
+++ b/frontend/app/package.json
@@ -93,6 +93,7 @@
"cordova-plugin-calendar": "5.1.6",
"date-fns": "2.30.0",
"deepmerge": "4.3.1",
+ "fast-deep-equal": "3.1.3",
"form-data": "4.0.0",
"geojson": "0.5.0",
"ionic-appauth": "0.9.0",
diff --git a/frontend/app/src/app/animation/splash.ts b/frontend/app/src/app/animation/splash.ts
new file mode 100644
index 00000000..0400dbcd
--- /dev/null
+++ b/frontend/app/src/app/animation/splash.ts
@@ -0,0 +1,80 @@
+import {Animation, AnimationController} from '@ionic/angular';
+import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings';
+
+/**
+ * Splash screen animation
+ */
+export function splashAnimation(animationCtl: AnimationController): Animation {
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ return animationCtl
+ .create()
+ .fromTo('opacity', 0, 1)
+ .duration(150)
+ .beforeClearStyles(['visibility'])
+ .addElement(document.querySelector('ion-app')!);
+ }
+
+ const isMd = document.querySelector('ion-app.md') !== null;
+ const navElement = document.querySelector('stapps-navigation-tabs')!;
+ const navBounds = navElement.getBoundingClientRect();
+ let horizontal = navBounds.width < navBounds.height;
+ if (window.getComputedStyle(navElement).display === 'none') {
+ horizontal = true;
+ }
+ const translate = (amount: number, unit = 'px') =>
+ `translate${horizontal ? 'X' : 'Y'}(${horizontal ? amount * -1 : amount}${unit})`;
+ const duration = 2 * (isMd ? mdDuration : iosDuration);
+
+ const animation = animationCtl
+ .create()
+ .duration(duration)
+ .easing(isMd ? mdEasing : iosEasing)
+ .addAnimation(
+ animationCtl.create().beforeClearStyles(['visibility']).addElement(document.querySelector('ion-app')!),
+ )
+ .addAnimation(
+ animationCtl
+ .create()
+ .fromTo('transform', translate(horizontal ? 64 : 192), translate(0))
+ .fromTo('opacity', 0, 1)
+ .addElement(document.querySelector('stapps-navigation > ion-split-pane')!),
+ )
+ .addAnimation(
+ animationCtl
+ .create()
+ .fromTo('transform', translate(64), translate(0))
+ .addElement(document.querySelectorAll('ion-split-pane > ion-menu > ion-content')),
+ )
+ .addAnimation(
+ animationCtl
+ .create()
+ .fromTo('transform', translate(horizontal ? 32 : -72), translate(0))
+ .addElement(document.querySelectorAll('ion-router-outlet > .ion-page > ion-content')!),
+ )
+ .addAnimation(
+ animationCtl
+ .create()
+ .fromTo('transform', translate(100, '%'), translate(0, '%'))
+ .addElement(document.querySelector('stapps-navigation-tabs')!),
+ );
+
+ if (!horizontal) {
+ animation.addAnimation(
+ animationCtl
+ .create()
+ .fromTo('background', 'none', 'none')
+ .addElement(document.querySelector('ion-router-outlet')!),
+ );
+
+ const parallax = document
+ .querySelector('ion-router-outlet > .ion-page > ion-content')
+ ?.shadowRoot?.querySelector('[part=parallax]');
+ if (parallax) {
+ animation.addAnimation(
+ animationCtl.create().fromTo('translate', '0 256px', '0 0px').addElement(parallax),
+ );
+ }
+ }
+
+ return animation;
+}
diff --git a/frontend/app/src/app/app.component.ts b/frontend/app/src/app/app.component.ts
index ca2b6380..be919c23 100644
--- a/frontend/app/src/app/app.component.ts
+++ b/frontend/app/src/app/app.component.ts
@@ -22,28 +22,16 @@ import {environment} from '../environments/environment';
import {Capacitor} from '@capacitor/core';
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
-import {AppVersionService} from './modules/about/app-version.service';
import {SplashScreen} from '@capacitor/splash-screen';
+import {AppVersionService} from './modules/about/app-version.service';
-/**
- * TODO
- */
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
})
export class AppComponent implements AfterContentInit {
- /**
- * TODO
- */
pages: Array<{
- /**
- * TODO
- */
component: unknown;
- /**
- * TODO
- */
title: string;
}>;
@@ -65,7 +53,7 @@ export class AppComponent implements AfterContentInit {
void this.initializeApp();
}
- async ngAfterContentInit() {
+ ngAfterContentInit() {
this.scheduleSyncService.init();
void this.scheduleSyncService.enable();
this.versionService.getPendingReleaseNotes().then(notes => {
@@ -74,24 +62,11 @@ export class AppComponent implements AfterContentInit {
}
});
- if (document.readyState === 'complete') {
- this.hideSplash();
- } else {
- document.addEventListener('readystatechange', () => {
- if (document.readyState === 'complete') this.hideSplash();
- });
- }
- }
-
- async hideSplash() {
if (Capacitor.isNativePlatform()) {
void SplashScreen.hide();
}
}
- /**
- * TODO
- */
async initializeApp() {
App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
this.zone.run(() => {
diff --git a/frontend/app/src/app/app.module.ts b/frontend/app/src/app/app.module.ts
index fd6549a6..1473aecf 100644
--- a/frontend/app/src/app/app.module.ts
+++ b/frontend/app/src/app/app.module.ts
@@ -25,12 +25,10 @@ import moment from 'moment';
import 'moment/min/locales';
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
import SwiperCore, {FreeMode, Navigation} from 'swiper';
-
import {environment} from '../environments/environment';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {CatalogModule} from './modules/catalog/catalog.module';
-import {ConfigModule} from './modules/config/config.module';
import {ConfigProvider} from './modules/config/config.provider';
import {DashboardModule} from './modules/dashboard/dashboard.module';
import {DataModule} from './modules/data/data.module';
@@ -44,7 +42,6 @@ import {SettingsProvider} from './modules/settings/settings.provider';
import {StorageModule} from './modules/storage/storage.module';
import {ThingTranslateModule} from './translation/thing-translate.module';
import {UtilModule} from './util/util.module';
-import {initLogger} from './_helpers/ts-logger';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AboutModule} from './modules/about/about.module';
import {JobModule} from './modules/jobs/jobs.module';
@@ -91,28 +88,25 @@ export function initializerFactory(
) {
return async () => {
try {
- initLogger(logger);
await storageProvider.init();
await configProvider.init();
- await settingsProvider.init();
+ if (configProvider.firstSession) {
+ // set language from browser
+ await settingsProvider.setSettingValue(
+ 'profile',
+ 'language',
+ translateService.getBrowserLang() as SCSettingValue,
+ );
+ }
+ const languageCode = await settingsProvider.getSetting('profile', 'language');
+ // this language will be used as a fallback when a translation isn't found in the current language
+ translateService.setDefaultLang('en');
+ translateService.use(languageCode);
+ moment.locale(languageCode);
+ const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
+ setDefaultOptions({locale: dateFnsLocale});
+ dateFnsConfigurationService.setLocale(dateFnsLocale);
try {
- if (configProvider.firstSession) {
- // set language from browser
- await settingsProvider.setSettingValue(
- 'profile',
- 'language',
- translateService.getBrowserLang() as SCSettingValue,
- );
- }
- const languageCode = (await settingsProvider.getValue('profile', 'language')) as string;
- // this language will be used as a fallback when a translation isn't found in the current language
- translateService.setDefaultLang('en');
- translateService.use(languageCode);
- moment.locale(languageCode);
- const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
- setDefaultOptions({locale: dateFnsLocale});
- dateFnsConfigurationService.setLocale(dateFnsLocale);
-
await defaultAuthService.init();
await paiaAuthService.init();
} catch (error) {
@@ -151,11 +145,12 @@ export function createTranslateLoader(http: HttpClient) {
BrowserAnimationsModule,
CatalogModule,
CommonModule,
- ConfigModule,
DashboardModule,
DataModule,
HebisModule,
- IonicModule.forRoot(),
+ IonicModule.forRoot({
+ animated: 'Cypress' in window ? false : undefined,
+ }),
IonIconModule,
JobModule,
FavoritesModule,
diff --git a/frontend/app/src/app/modules/about/about-page/about-page.component.ts b/frontend/app/src/app/modules/about/about-page/about-page.component.ts
index ea0d5923..ed30c180 100644
--- a/frontend/app/src/app/modules/about/about-page/about-page.component.ts
+++ b/frontend/app/src/app/modules/about/about-page/about-page.component.ts
@@ -14,7 +14,7 @@
*/
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
-import {SCAboutPage, SCAppConfiguration} from '@openstapps/core';
+import {SCAboutPage} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider';
import packageJson from '../../../../../package.json';
import config from 'capacitor.config';
@@ -42,8 +42,7 @@ export class AboutPageComponent implements OnInit {
async ngOnInit() {
const route = this.route.snapshot.url.map(it => it.path).join('/');
- this.content =
- (this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
+ this.content = this.configProvider.config.app.aboutPages[route] ?? {};
this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version);
}
}
diff --git a/frontend/app/src/app/modules/about/about.module.ts b/frontend/app/src/app/modules/about/about.module.ts
index 5c975fa3..0dc3876d 100644
--- a/frontend/app/src/app/modules/about/about.module.ts
+++ b/frontend/app/src/app/modules/about/about.module.ts
@@ -19,7 +19,6 @@ import {FormsModule} from '@angular/forms';
import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
-import {ConfigProvider} from '../config/config.provider';
import {AboutPageComponent} from './about-page/about-page.component';
import {MarkdownModule} from 'ngx-markdown';
import {AboutPageContentComponent} from './about-page/about-page-content.component';
@@ -64,6 +63,5 @@ const settingsRoutes: Routes = [
ScrollingModule,
UtilModule,
],
- providers: [ConfigProvider],
})
export class AboutModule {}
diff --git a/frontend/app/src/app/modules/auth/auth-helper.service.ts b/frontend/app/src/app/modules/auth/auth-helper.service.ts
index 39994816..df3a3482 100644
--- a/frontend/app/src/app/modules/auth/auth-helper.service.ts
+++ b/frontend/app/src/app/modules/auth/auth-helper.service.ts
@@ -18,12 +18,7 @@ import {IPAIAAuthAction} from './paia/paia-auth-action';
import {AuthActions, IAuthAction} from 'ionic-appauth';
import {TranslateService} from '@ngx-translate/core';
import {JSONPath} from 'jsonpath-plus';
-import {
- SCAuthorizationProvider,
- SCAuthorizationProviderType,
- SCUserConfiguration,
- SCUserConfigurationMap,
-} from '@openstapps/core';
+import {SCAuthorizationProviderType, SCUserConfiguration} from '@openstapps/core';
import {ConfigProvider} from '../config/config.provider';
import {StorageProvider} from '../storage/storage.provider';
import {DefaultAuthService} from './default-auth.service';
@@ -37,8 +32,6 @@ const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
providedIn: 'root',
})
export class AuthHelperService {
- userConfigurationMap: SCUserConfigurationMap;
-
constructor(
private translateService: TranslateService,
private configProvider: ConfigProvider,
@@ -47,14 +40,7 @@ export class AuthHelperService {
private paiaAuth: PAIAAuthService,
private browser: SimpleBrowser,
private alertController: AlertController,
- ) {
- this.userConfigurationMap =
- (
- this.configProvider.getAnyValue('auth') as {
- default: SCAuthorizationProvider;
- }
- ).default?.endpoints.mapping ?? {};
- }
+ ) {}
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
let message: string | undefined;
@@ -77,9 +63,10 @@ export class AuthHelperService {
name: '',
role: 'student',
};
- for (const key in this.userConfigurationMap) {
+ const mapping = this.configProvider.config.auth.default!.endpoints.mapping;
+ for (const key in mapping) {
user[key as keyof SCUserConfiguration] = JSONPath({
- path: this.userConfigurationMap[key as keyof SCUserConfiguration] as string,
+ path: mapping[key as keyof SCUserConfiguration] as string,
json: userInfo,
preventEval: true,
})[0];
diff --git a/frontend/app/src/app/modules/auth/default-auth.service.ts b/frontend/app/src/app/modules/auth/default-auth.service.ts
index 701365a2..3c81d54f 100644
--- a/frontend/app/src/app/modules/auth/default-auth.service.ts
+++ b/frontend/app/src/app/modules/auth/default-auth.service.ts
@@ -12,7 +12,6 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see .
*/
-
import {
AuthorizationRequestHandler,
AuthorizationServiceConfiguration,
@@ -24,7 +23,6 @@ import {
} from '@openid/appauth';
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
import {ConfigProvider} from '../config/config.provider';
-import {SCAuthorizationProvider} from '@openstapps/core';
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
import {Injectable} from '@angular/core';
import {AuthService} from './auth.service';
@@ -67,12 +65,9 @@ export class DefaultAuthService extends AuthService {
}
setupConfiguration() {
- const authConfig = this.configProvider.getAnyValue('auth') as {
- default: SCAuthorizationProvider;
- };
- this.authConfig = getClientConfig('default', authConfig);
+ this.authConfig = getClientConfig('default', this.configProvider.config.auth);
this.localConfiguration = new AuthorizationServiceConfiguration(
- getEndpointsConfig('default', authConfig),
+ getEndpointsConfig('default', this.configProvider.config.auth),
);
}
diff --git a/frontend/app/src/app/modules/auth/paia/paia-auth.service.ts b/frontend/app/src/app/modules/auth/paia/paia-auth.service.ts
index 252ab1eb..64db03b8 100644
--- a/frontend/app/src/app/modules/auth/paia/paia-auth.service.ts
+++ b/frontend/app/src/app/modules/auth/paia/paia-auth.service.ts
@@ -12,7 +12,6 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see .
*/
-
import {
AuthorizationError,
AuthorizationRequest,
@@ -47,7 +46,6 @@ import {PAIAAuthorizationResponse} from './paia-authorization-response';
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
import {PAIATokenResponse} from './paia-token-response';
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
-import {SCAuthorizationProvider} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider';
import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods';
import {Injectable} from '@angular/core';
@@ -154,11 +152,10 @@ export class PAIAAuthService {
}
setupConfiguration() {
- const authConfig = this.configProvider.getAnyValue('auth') as {
- paia: SCAuthorizationProvider;
- };
- this.authConfig = getClientConfig('paia', authConfig);
- this.localConfiguration = new AuthorizationServiceConfiguration(getEndpointsConfig('paia', authConfig));
+ this.authConfig = getClientConfig('paia', this.configProvider.config.auth);
+ this.localConfiguration = new AuthorizationServiceConfiguration(
+ getEndpointsConfig('paia', this.configProvider.config.auth),
+ );
}
protected notifyActionListers(action: IPAIAAuthAction) {
diff --git a/frontend/app/src/app/modules/calendar/calendar.service.ts b/frontend/app/src/app/modules/calendar/calendar.service.ts
index ddcd31c5..3a69ac02 100644
--- a/frontend/app/src/app/modules/calendar/calendar.service.ts
+++ b/frontend/app/src/app/modules/calendar/calendar.service.ts
@@ -12,7 +12,6 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see .
*/
-
import {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
import {Injectable} from '@angular/core';
import {ICalEvent} from './ical/ical';
@@ -35,14 +34,14 @@ export class CalendarService {
goToDateClicked = this.goToDate.asObservable();
- calendarName = 'StApps';
+ calendarName: string;
// eslint-disable-next-line @typescript-eslint/no-empty-function
constructor(
readonly calendar: Calendar,
private readonly configProvider: ConfigProvider,
) {
- this.calendarName = (this.configProvider.getValue('name') as string) ?? 'StApps';
+ this.calendarName = this.configProvider.config.app.name ?? 'StApps';
}
async createCalendar(): Promise {
diff --git a/frontend/app/src/app/modules/catalog/catalog.module.ts b/frontend/app/src/app/modules/catalog/catalog.module.ts
index 88644e69..6e701157 100644
--- a/frontend/app/src/app/modules/catalog/catalog.module.ts
+++ b/frontend/app/src/app/modules/catalog/catalog.module.ts
@@ -20,7 +20,6 @@ import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {MomentModule} from 'ngx-moment';
import {DataModule} from '../data/data.module';
-import {SettingsProvider} from '../settings/settings.provider';
import {CatalogComponent} from './catalog.component';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
@@ -46,6 +45,5 @@ const catalogRoutes: Routes = [
DataModule,
UtilModule,
],
- providers: [SettingsProvider],
})
export class CatalogModule {}
diff --git a/frontend/app/src/app/modules/config/config.provider.spec.ts b/frontend/app/src/app/modules/config/config.provider.spec.ts
index c271f153..ee7672b1 100644
--- a/frontend/app/src/app/modules/config/config.provider.spec.ts
+++ b/frontend/app/src/app/modules/config/config.provider.spec.ts
@@ -16,12 +16,6 @@ import {TestBed} from '@angular/core/testing';
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
import {StorageProvider} from '../storage/storage.provider';
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
-import {
- ConfigFetchError,
- ConfigInitError,
- SavedConfigNotAvailable,
- WrongConfigVersionInStorage,
-} from './errors';
import {NGXLogger} from 'ngx-logger';
import {sampleIndexResponse} from '../../_helpers/data/sample-configuration';
diff --git a/frontend/app/src/app/modules/config/config.provider.ts b/frontend/app/src/app/modules/config/config.provider.ts
index 259f1790..b292ebe2 100644
--- a/frontend/app/src/app/modules/config/config.provider.ts
+++ b/frontend/app/src/app/modules/config/config.provider.ts
@@ -14,19 +14,14 @@
*/
import {Injectable} from '@angular/core';
import {Client} from '@openstapps/api';
-import {SCAppConfiguration, SCIndexResponse} from '@openstapps/core';
+import {SCIndexResponse} from '@openstapps/core';
import packageInfo from '@openstapps/core/package.json';
import {NGXLogger} from 'ngx-logger';
import {environment} from '../../../environments/environment';
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
import {StorageProvider} from '../storage/storage.provider';
-import {
- ConfigFetchError,
- ConfigInitError,
- ConfigValueNotAvailable,
- SavedConfigNotAvailable,
- WrongConfigVersionInStorage,
-} from './errors';
+import equals from 'fast-deep-equal/es6';
+import {BehaviorSubject} from 'rxjs';
/**
* Key to store config in storage module
@@ -35,6 +30,17 @@ import {
*/
export const STORAGE_KEY_CONFIG = 'stapps.config';
+/**
+ * Makes an object deeply immutable
+ */
+function deepFreeze(object: T) {
+ for (const key of Object.keys(object)) {
+ const value = (object as Record)[key];
+ if (typeof value === 'object' && !Object.isFrozen(value)) deepFreeze(value!);
+ }
+ return Object.freeze(object);
+}
+
/**
* Provides configuration
*/
@@ -50,7 +56,7 @@ export class ConfigProvider {
/**
* App configuration as IndexResponse
*/
- config: SCIndexResponse;
+ config: Readonly;
/**
* Version of the @openstapps/core package that app is using
@@ -62,6 +68,11 @@ export class ConfigProvider {
*/
firstSession = true;
+ /**
+ * If the config requires an update
+ */
+ needsUpdate$ = new BehaviorSubject(false);
+
/**
* Constructor, initialise api client
* @param storageProvider StorageProvider to load persistent configuration
@@ -76,104 +87,35 @@ export class ConfigProvider {
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
}
- /**
- * Fetches configuration from backend
- */
- async fetch(): Promise {
- try {
- return await this.client.handshake(this.scVersion);
- } catch {
- throw new ConfigFetchError();
- }
- }
-
- /**
- * Returns the value of an app configuration
- * @param attribute requested attribute from app configuration
- */
- public getValue(attribute: keyof SCAppConfiguration) {
- if (this.config.app[attribute] !== undefined) {
- return this.config.app[attribute];
- }
- throw new ConfigValueNotAvailable(attribute);
- }
-
- /**
- * Returns a value of the configuration (not only app configuration)
- * @param attribute requested attribute from the configuration
- */
- public getAnyValue(attribute: keyof SCIndexResponse) {
- if (this.config[attribute] !== undefined) {
- return this.config[attribute];
- }
- throw new ConfigValueNotAvailable(attribute);
- }
-
/**
* Initialises the ConfigProvider
- * @throws ConfigInitError if no configuration could be loaded.
- * @throws WrongConfigVersionInStorage if fetch failed and saved config has wrong SCVersion
*/
async init(): Promise {
- let loadError;
- let fetchError;
- // load saved configuration
- try {
- this.config = await this.loadLocal();
- this.firstSession = false;
- this.logger.log(`initialised configuration from storage`);
- if (this.config.backend.SCVersion !== this.scVersion) {
- loadError = new WrongConfigVersionInStorage(this.scVersion, this.config.backend.SCVersion);
+ this.config = (await this.storageProvider.has(STORAGE_KEY_CONFIG))
+ ? await this.storageProvider.get(STORAGE_KEY_CONFIG)
+ : undefined!;
+ this.firstSession = !this.config;
+
+ const updatedConfig = this.client.handshake(this.scVersion).then(async fetchedConfig => {
+ if (!equals(fetchedConfig, this.config)) {
+ await this.storageProvider.put(STORAGE_KEY_CONFIG, fetchedConfig);
+ this.logger.log(`Config updated`);
+ this.needsUpdate$.next(true);
+ this.needsUpdate$.complete();
}
- } catch (error) {
- loadError = error;
- }
- // fetch remote configuration from backend
- try {
- const fetchedConfig: SCIndexResponse = await this.fetch();
- await this.set(fetchedConfig);
- this.logger.log(`initialised configuration from remote`);
- } catch (error) {
- fetchError = error;
- }
- // check for occurred errors and throw them
- if (loadError !== undefined && fetchError !== undefined) {
- throw new ConfigInitError();
- }
- if (loadError !== undefined) {
- this.logger.warn(loadError);
- }
- if (fetchError !== undefined) {
- this.logger.warn(fetchError);
- }
- }
+ return fetchedConfig;
+ });
- /**
- * Returns saved configuration from StorageModule
- * @throws SavedConfigNotAvailable if no configuration could be loaded
- */
- async loadLocal(): Promise {
- // get local configuration
- if (await this.storageProvider.has(STORAGE_KEY_CONFIG)) {
- return this.storageProvider.get(STORAGE_KEY_CONFIG);
+ this.config ??= await updatedConfig;
+ this.config = deepFreeze(this.config);
+
+ if (this.config.backend.SCVersion !== this.scVersion) {
+ this.logger.warn(
+ 'Incompatible config, expected',
+ this.scVersion,
+ 'but got',
+ this.config.backend.SCVersion,
+ );
}
- throw new SavedConfigNotAvailable();
- }
-
- /**
- * Saves the configuration from the provider
- * @param config configuration to save
- */
- async save(config: SCIndexResponse): Promise {
- await this.storageProvider.put(STORAGE_KEY_CONFIG, config);
- }
-
- /**
- * Sets the configuration in the module and writes it into app storage
- * @param config SCIndexResponse to set
- */
- async set(config: SCIndexResponse): Promise {
- this.config = config;
- await this.save(this.config);
}
}
diff --git a/frontend/app/src/app/modules/config/errors.ts b/frontend/app/src/app/modules/config/errors.ts
deleted file mode 100644
index f9fcd84d..00000000
--- a/frontend/app/src/app/modules/config/errors.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2022 StApps
- * This program is free software: you can redistribute it and/or modify it
- * under the terms of the GNU General Public License as published by the Free
- * Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
- * more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * this program. If not, see .
- */
-
-import {AppError} from '../../_helpers/errors';
-
-/**
- * Error that is thrown when fetching from backend fails
- */
-export class ConfigFetchError extends AppError {
- constructor() {
- super('ConfigFetchError', 'App configuration could not be fetched!');
- }
-}
-
-/**
- * Error that is thrown when the ConfigProvider could be initialised
- */
-export class ConfigInitError extends AppError {
- constructor() {
- super('ConfigInitError', 'App configuration could not be initialised!');
- }
-}
-
-/**
- * Error that is thrown when the requested config value is not available
- */
-export class ConfigValueNotAvailable extends AppError {
- constructor(valueKey: string) {
- super('ConfigValueNotAvailable', `No attribute "${valueKey}" in config available!`);
- }
-}
-
-/**
- * Error that is thrown when no saved config is available
- */
-export class SavedConfigNotAvailable extends AppError {
- constructor() {
- super('SavedConfigNotAvailable', 'No saved app configuration available.');
- }
-}
-
-/**
- * Error that is thrown when the SCVersion of the saved config is not compatible with the app
- */
-export class WrongConfigVersionInStorage extends AppError {
- constructor(correctVersion: string, savedVersion: string) {
- super(
- 'WrongConfigVersionInStorage',
- `The saved configs backend version ${savedVersion} ` +
- `does not equal the configured backend version ${correctVersion} of the app.`,
- );
- }
-}
diff --git a/frontend/app/src/app/modules/dashboard/dashboard.module.ts b/frontend/app/src/app/modules/dashboard/dashboard.module.ts
index a39c0daa..1ed36fa7 100644
--- a/frontend/app/src/app/modules/dashboard/dashboard.module.ts
+++ b/frontend/app/src/app/modules/dashboard/dashboard.module.ts
@@ -21,7 +21,6 @@ import {SwiperModule} from 'swiper/angular';
import {TranslateModule, TranslatePipe} from '@ngx-translate/core';
import {MomentModule} from 'ngx-moment';
import {DataModule} from '../data/data.module';
-import {SettingsProvider} from '../settings/settings.provider';
import {DashboardComponent} from './dashboard.component';
import {SearchSectionComponent} from './sections/search-section/search-section.component';
import {NewsSectionComponent} from './sections/news-section/news-section.component';
@@ -70,6 +69,6 @@ const catalogRoutes: Routes = [
NewsModule,
JobModule,
],
- providers: [SettingsProvider, TranslatePipe],
+ providers: [TranslatePipe],
})
export class DashboardModule {}
diff --git a/frontend/app/src/app/modules/data/data.module.ts b/frontend/app/src/app/modules/data/data.module.ts
index 09697966..ea7e5a66 100644
--- a/frontend/app/src/app/modules/data/data.module.ts
+++ b/frontend/app/src/app/modules/data/data.module.ts
@@ -32,7 +32,6 @@ import {ScheduleProvider} from '../calendar/schedule.provider';
import {GeoNavigationDirective} from '../map/geo-navigation.directive';
import {MapWidgetComponent} from '../map/widget/map-widget.component';
import {MenuModule} from '../menu/menu.module';
-import {SettingsProvider} from '../settings/settings.provider';
import {StorageModule} from '../storage/storage.module';
import {ActionChipListComponent} from './chips/action-chip-list.component';
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
@@ -214,7 +213,6 @@ import {ShareButtonComponent} from './elements/share-button.component';
StAppsWebHttpClient,
CalendarService,
RoutingStackService,
- SettingsProvider,
{
provide: SimpleBrowser,
useFactory: browserFactory,
diff --git a/frontend/app/src/app/modules/data/detail/data-detail.component.ts b/frontend/app/src/app/modules/data/detail/data-detail.component.ts
index 9ac757b4..4a69a1b9 100644
--- a/frontend/app/src/app/modules/data/detail/data-detail.component.ts
+++ b/frontend/app/src/app/modules/data/detail/data-detail.component.ts
@@ -15,13 +15,13 @@
import {Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {ModalController} from '@ionic/angular';
-import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
-import {SCLanguageCode, SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
+import {SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
import {DataProvider, DataScope} from '../data.provider';
import {FavoritesService} from '../../favorites/favorites.service';
import {take} from 'rxjs/operators';
import {Network} from '@capacitor/network';
import {DataListContext} from '../list/data-list.component';
+import {lastValueFrom} from 'rxjs';
export interface ExternalDataLoadEvent {
uid: SCUuid;
@@ -29,6 +29,13 @@ export interface ExternalDataLoadEvent {
resolve: (item: SCThings | null | undefined) => void;
}
+/**
+ * Type guard for SCSavableThing
+ */
+function isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
+ return (thing as SCSaveableThing).data !== undefined;
+}
+
/**
* A Component to display an SCThing detailed
*/
@@ -53,11 +60,6 @@ export class DataDetailComponent implements OnInit {
@Input() autoRouteDataPath = true;
- /**
- * The language of the item
- */
- language: SCLanguageCode;
-
/**
* Indicating wether internet connectivity is given or not
*/
@@ -79,20 +81,12 @@ export class DataDetailComponent implements OnInit {
@Output() loadItem: EventEmitter = new EventEmitter();
- /**
- * Type guard for SCSavableThing
- */
- static isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
- return (thing as SCSaveableThing).data !== undefined;
- }
-
constructor(
protected readonly route: ActivatedRoute,
router: Router,
private readonly dataProvider: DataProvider,
private readonly favoritesService: FavoritesService,
readonly modalController: ModalController,
- translateService: TranslateService,
) {
this.inputItem = router.getCurrentNavigation()?.extras.state?.item;
if (!this.inputItem?.origin) {
@@ -100,10 +94,6 @@ export class DataDetailComponent implements OnInit {
// This can happen, for example, when detail views use `inPlace` list items
delete this.inputItem;
}
- this.language = translateService.currentLang as SCLanguageCode;
- translateService.onLangChange.subscribe((event: LangChangeEvent) => {
- this.language = event.lang as SCLanguageCode;
- });
this.isDisconnected = new Promise(async resolve => {
const isConnected = (await Network.getStatus()).connected;
@@ -126,13 +116,8 @@ export class DataDetailComponent implements OnInit {
)
: this.dataProvider.get(uid, DataScope.Remote)));
- this.item = item
- ? // eslint-disable-next-line unicorn/no-null
- DataDetailComponent.isSCSavableThing(item)
- ? item.data
- : item
- : // eslint-disable-next-line unicorn/no-null
- null;
+ // eslint-disable-next-line unicorn/no-null
+ this.item = item ? (isSCSavableThing(item) ? item.data : item) : null;
} catch {
// eslint-disable-next-line unicorn/no-null
this.item = null;
@@ -144,14 +129,10 @@ export class DataDetailComponent implements OnInit {
await this.getItem(uid ?? '', false);
// fallback to the saved item (from favorites)
if (this.item === null) {
- this.favoritesService
- .get(uid)
- .pipe(take(1))
- .subscribe(item => {
- if (item !== undefined) {
- this.item = item.data;
- }
- });
+ const item = await lastValueFrom(this.favoritesService.get(uid).pipe(take(1)));
+ if (item) {
+ this.item = item.data;
+ }
}
}
}
diff --git a/frontend/app/src/app/modules/data/elements/offers-in-list.component.ts b/frontend/app/src/app/modules/data/elements/offers-in-list.component.ts
index 57281fe8..52085807 100644
--- a/frontend/app/src/app/modules/data/elements/offers-in-list.component.ts
+++ b/frontend/app/src/app/modules/data/elements/offers-in-list.component.ts
@@ -30,8 +30,8 @@ export class OffersInListComponent {
@Input() set offers(it: Array>) {
this._offers = it;
this.price = it[0].prices?.default;
- this.settingsProvider.getSetting('profile', 'group').then(group => {
- this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never];
+ this.settingsProvider.getSetting('profile', 'group').then(group => {
+ this.price = it[0].prices?.[group.replace(/s$/, '') as never];
});
const availabilities = new Set(it.map(offer => offer.availability));
diff --git a/frontend/app/src/app/modules/data/list/search-page.component.ts b/frontend/app/src/app/modules/data/list/search-page.component.ts
index 90ff6d8b..5bc548df 100644
--- a/frontend/app/src/app/modules/data/list/search-page.component.ts
+++ b/frontend/app/src/app/modules/data/list/search-page.component.ts
@@ -17,14 +17,7 @@ import {ActivatedRoute, Router} from '@angular/router';
import {Keyboard} from '@capacitor/keyboard';
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
import {Capacitor} from '@capacitor/core';
-import {
- SCFacet,
- SCFeatureConfiguration,
- SCSearchFilter,
- SCSearchQuery,
- SCSearchSort,
- SCThings,
-} from '@openstapps/core';
+import {SCFacet, SCSearchFilter, SCSearchQuery, SCSearchSort, SCThings} from '@openstapps/core';
import {NGXLogger} from 'ngx-logger';
import {combineLatest, Subject} from 'rxjs';
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
@@ -170,9 +163,8 @@ export class SearchPageComponent implements OnInit {
private readonly route: ActivatedRoute,
protected positionService: PositionService,
private readonly configProvider: ConfigProvider,
- animationController: AnimationController,
) {
- this.routeAnimation = searchPageSwitchAnimation(animationController);
+ this.routeAnimation = searchPageSwitchAnimation(inject(AnimationController));
}
/**
@@ -323,16 +315,6 @@ export class SearchPageComponent implements OnInit {
this.queryChanged.next();
}
});
- this.settingsProvider.settingsActionChanged$
- .pipe(takeUntilDestroyed(this.destroy$))
- .subscribe(({type, payload}) => {
- if (type === 'stapps.settings.changed') {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const {category, name, value} = payload!;
- this.logger.log(`received event "settings.changed" with category:
- ${category}, name: ${name}, value: ${JSON.stringify(value)}`);
- }
- });
this.dataRoutingService
.itemSelectListener()
.pipe(takeUntilDestroyed(this.destroy$))
@@ -342,12 +324,8 @@ export class SearchPageComponent implements OnInit {
}
});
}
- try {
- const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
- this.isHebisAvailable = !!features.plugins?.['hebis-plugin']?.urlPath;
- } catch (error) {
- this.logger.error(error);
- }
+ this.isHebisAvailable =
+ this.configProvider.config.app.features.plugins?.['hebis-plugin']?.urlPath !== undefined;
}
/**
diff --git a/frontend/app/src/app/modules/data/rating.provider.ts b/frontend/app/src/app/modules/data/rating.provider.ts
index 3841acc6..6448dec1 100644
--- a/frontend/app/src/app/modules/data/rating.provider.ts
+++ b/frontend/app/src/app/modules/data/rating.provider.ts
@@ -21,7 +21,6 @@ import {
SCRatingResponse,
SCRatingRoute,
SCUserGroup,
- SCUserGroupSetting,
SCUuid,
} from '@openstapps/core';
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
@@ -63,9 +62,7 @@ export class RatingProvider {
}
private get userGroup(): Promise {
- return this.settingsProvider
- .getSetting('profile', 'group')
- .then(it => (it as SCUserGroupSetting).value as SCUserGroup);
+ return this.settingsProvider.getSetting('profile', 'group');
}
private async getStoredRatings(): Promise {
diff --git a/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-service.ts b/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-service.ts
index 363a852c..ccba33c0 100644
--- a/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-service.ts
+++ b/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-service.ts
@@ -75,7 +75,7 @@ export class PlaceMensaService {
sort: [
{
arguments: {
- field: `offers.prices.${(priceGroup.value as string).replace(/s$/, '')}`,
+ field: `offers.prices.${(priceGroup as string).replace(/s$/, '')}`,
},
order: 'desc',
type: 'generic',
diff --git a/frontend/app/src/app/modules/favorites/favorites-page.component.ts b/frontend/app/src/app/modules/favorites/favorites-page.component.ts
index 167f863a..37bb7a06 100644
--- a/frontend/app/src/app/modules/favorites/favorites-page.component.ts
+++ b/frontend/app/src/app/modules/favorites/favorites-page.component.ts
@@ -12,21 +12,13 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see .
*/
-import {Component, OnInit} from '@angular/core';
-import {AlertController, AnimationController} from '@ionic/angular';
-import {ActivatedRoute, Router} from '@angular/router';
-import {NGXLogger} from 'ngx-logger';
+import {Component, inject, OnInit} from '@angular/core';
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
import {combineLatest} from 'rxjs';
import {SCThingType} from '@openstapps/core';
import {FavoritesService} from './favorites.service';
-import {DataRoutingService} from '../data/data-routing.service';
import {ContextMenuService} from '../menu/context/context-menu.service';
import {SearchPageComponent} from '../data/list/search-page.component';
-import {DataProvider} from '../data/data.provider';
-import {SettingsProvider} from '../settings/settings.provider';
-import {PositionService} from '../map/position.service';
-import {ConfigProvider} from '../config/config.provider';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
/**
@@ -42,34 +34,7 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
showNavigation = false;
- constructor(
- alertController: AlertController,
- dataProvider: DataProvider,
- contextMenuService: ContextMenuService,
- settingsProvider: SettingsProvider,
- logger: NGXLogger,
- dataRoutingService: DataRoutingService,
- router: Router,
- route: ActivatedRoute,
- positionService: PositionService,
- private favoritesService: FavoritesService,
- configProvider: ConfigProvider,
- animationController: AnimationController,
- ) {
- super(
- alertController,
- dataProvider,
- contextMenuService,
- settingsProvider,
- logger,
- dataRoutingService,
- router,
- route,
- positionService,
- configProvider,
- animationController,
- );
- }
+ private favoritesService = inject(FavoritesService);
ngOnInit() {
super.ngOnInit(false);
@@ -96,16 +61,6 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
this.queryChanged.next();
}
});
- this.settingsProvider.settingsActionChanged$
- .pipe(takeUntilDestroyed(this.destroy$))
- .subscribe(({type, payload}) => {
- if (type === 'stapps.settings.changed') {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const {category, name, value} = payload!;
- this.logger.log(`received event "settings.changed" with category:
- ${category}, name: ${name}, value: ${JSON.stringify(value)}`);
- }
- });
this.dataRoutingService
.itemSelectListener()
.pipe(takeUntilDestroyed(this.destroy$))
diff --git a/frontend/app/src/app/modules/favorites/favorites.service.ts b/frontend/app/src/app/modules/favorites/favorites.service.ts
index 9c8cf121..d9179fab 100644
--- a/frontend/app/src/app/modules/favorites/favorites.service.ts
+++ b/frontend/app/src/app/modules/favorites/favorites.service.ts
@@ -28,7 +28,6 @@ import {
} from '@openstapps/core';
import {StorageProvider} from '../storage/storage.provider';
import {DataProvider} from '../data/data.provider';
-import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
import {TranslateService} from '@ngx-translate/core';
import {ThingTranslateService} from '../../translation/thing-translate.service';
import {BehaviorSubject, Observable} from 'rxjs';
@@ -41,11 +40,6 @@ import {debounceTime, map} from 'rxjs/operators';
providedIn: 'root',
})
export class FavoritesService {
- /**
- * Translation pipe
- */
- thingTranslatePipe: ThingTranslatePipe;
-
favorites = new BehaviorSubject