feat: app release notes

This commit is contained in:
Thea Schöbl
2023-12-01 12:34:20 +00:00
parent 5d47a17629
commit 37945f7d19
18 changed files with 335 additions and 7 deletions

View File

@@ -22,6 +22,7 @@ const app = {
name: 'Goethe-Uni', name: 'Goethe-Uni',
privacyPolicyUrl: 'https://mobile.server.uni-frankfurt.de/_static/privacy.md', privacyPolicyUrl: 'https://mobile.server.uni-frankfurt.de/_static/privacy.md',
settings: [userGroupSetting, languageSetting], settings: [userGroupSetting, languageSetting],
versionHistory: [],
}; };
export default app; export default app;

View File

@@ -0,0 +1,42 @@
// @ts-check
import {readFile} from 'fs/promises';
/**
* @example version(1, import.meta.url)
* @param options {Omit<import('@openstapps/core').SCAppVersionInfo, 'releaseNotes' | 'translations'>}
* @param base {string}
* @returns {Promise<import('@openstapps/core').SCAppVersionInfo>}
*/
export async function version(options, base) {
const de = await readFile(new URL(`${options.version}.de.md`, base), 'utf8');
const en = await readFile(new URL(`${options.version}.en.md`, base), 'utf8');
return {
...options,
releaseNotes: de,
translations: {
en: {
releaseNotes: en,
},
},
};
}
/**
* @param infos {Record<string, import('@openstapps/core').SCAppVersionInfo['published']>}
* @param base {string} Base path of the file as `import.meta.url`
* @returns {Promise<import('@openstapps/core').SCAppVersionInfo[]>}
*/
export async function versions(infos, base) {
return Promise.all(
Object.entries(infos).map(([versionName, published]) =>
version(
{
published,
version: versionName,
},
base,
),
),
).then(it => it.sort(({version: a}, {version: b}) => -a.localeCompare(b, undefined, {numeric: true})));
}

View File

@@ -2,6 +2,7 @@
import aboutPages from './about-pages/index.js'; import aboutPages from './about-pages/index.js';
import defaultApp from '../default/app/index.js'; import defaultApp from '../default/app/index.js';
import {backend as defaultBackend, internal as defaultInternal} from '../default/backend/index.js'; import {backend as defaultBackend, internal as defaultInternal} from '../default/backend/index.js';
import versionHistory from './version-history/index.js';
/** /**
* This is the default configuration for the Goethe university of Frankfurt * This is the default configuration for the Goethe university of Frankfurt
@@ -76,6 +77,7 @@ const config = {
} */ } */
}, },
}, },
versionHistory,
aboutPages, aboutPages,
}, },
backend: defaultBackend, backend: defaultBackend,

View File

@@ -0,0 +1,52 @@
# Goethe-Uni App 3.1
Wir freuen uns euch mehr in der Goethe-Uni App
bieten zu können.
## Navigation zu Gebäuden und Orten
Als eines der Ergebnisse des Ideenwettbewerbs wurde jetzt
ein Navigationsfeature in die App integriert.
Orte auf der Karte, Mensen, sowie sogar Termine (wenn hinterlegt)
bieten jetzt direkt die Option eine Verbindung zu finden, gestützt
durch die Karten App auf deinem Gerät.
## Integration der Jobbörse
Jobs findest du ab sofort auch in der Goethe-Uni App.
Auch das ist ein Ergebnis des Ideenwettbewerbs,
und wir freuen uns es euch hier präsentieren zu können!
## Der Umweltscore
Der Umweltscore für Gerichte wird nun auch in der App angezeigt.
> Nachhaltigkeit, Umweltschutz, Gesundheit und Klimawandel sind
> zentrale Begriffe im gesellschaftlichen Miteinander.
> Unsere Ernährung spielt hierbei eine wichtige Rolle.
> Das Studierendenwerk Frankfurt am Main zeichnet seine Speisenpläne
> ab sofort mit einem Umweltscore aus.
> Anhand dieser Bewertung können Sie direkt ersehen,
> welchen Einfluss Ihre Essenauswahl auf das Klima hat.
## Weitere Verbesserungen
### Performance
Die Performance der App beim Navigieren wurde stark verbessert und ist datensparender.
### Kalender
Die Kalenderabschnitte haben jetzt neue Namen bekommen:
- Der _Kalender_ zeigt Termine für spezifische Tage
- Die _Wochenübersicht_ ist ein Stundenplan mit allen Termine, die sich wiederholen (z. B. Vorlesungen)
- Die _Einzeltermine_ zeigen alle Termine, die sich nicht wiederholen
(z. B. Klausuren)
### Meine App
Der "Meine Kurse" Abschnitt wurde überarbeitet, und zeigt jetzt Termine
für die nächsten Tage und mit mehr Details an.

View File

@@ -0,0 +1,49 @@
# Goethe-Uni App 3.1
The Goethe-Uni App got even better!
## Navigation to buildings and places
As part of the "Ideenwettbewerb," the idea competition,
we have now integrated a navigation feature into the app.
Orte auf der Karte, Mensen, sowie sogar Termine (wenn hinterlegt)
bieten jetzt direkt die Option eine Verbindung zu finden, gestützt
durch die Karten App auf deinem Gerät.
## Integration of the job market
Jobs are now also available in the Goethe-Uni App.
This feature is also a result of the idea competition,
and we're happy to be able to present it to you here!
## The environment score
The environment score for dishes is now displayed inside the app.
> Sustainability, environment protection, health, and climate change are
> central topics in how we live today in our society.
> Our eating habits play an important role in it.
> The "Studierendenwerk Frankfurt am Main" is marking up its menus
> from now on with the so-called "Umweltscore," the environment score.
> Based on this rating, you can see the impact your meal choice would have on our climate.
## Further improvements
### Performance
The performance while navigating around the app has been heavily improved and requires less data to work.
### Calendar
The calendar sections have new names:
- The _calendar_ shows appointments on specific days
- The _week overview_ is a schedule with all events that repeat (e.g. lectures)
- The _single events_ show all appointments that don't repeat (e.g. exams)
### My App
The "my courses" section has been revamped,
and now shows events for the next days and with more detail.

View File

@@ -0,0 +1,12 @@
// @ts-check
import {versions} from '../../default/tools/version.js';
/** @type {import('@openstapps/core').SCAppVersionInfo[]} */
const versionHistory = await versions(
{
'3.1.0': {},
},
import.meta.url,
);
export default versionHistory;

View File

@@ -17,7 +17,7 @@
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {Platform} from '@ionic/angular'; import {ModalController, Platform} from '@ionic/angular';
import {TranslateService} from '@ngx-translate/core'; import {TranslateService} from '@ngx-translate/core';
import {ThingTranslateService} from './translation/thing-translate.service'; import {ThingTranslateService} from './translation/thing-translate.service';
@@ -45,6 +45,7 @@ describe('AppComponent', () => {
let platformIsSpy; let platformIsSpy;
let storageProvider: jasmine.SpyObj<StorageProvider>; let storageProvider: jasmine.SpyObj<StorageProvider>;
let simpleBrowser: jasmine.SpyObj<SimpleBrowser>; let simpleBrowser: jasmine.SpyObj<SimpleBrowser>;
let modalController: jasmine.SpyObj<ModalController>;
beforeEach(() => { beforeEach(() => {
platformReadySpy = Promise.resolve(); platformReadySpy = Promise.resolve();
@@ -71,6 +72,7 @@ describe('AppComponent', () => {
ngxLogger = jasmine.createSpyObj('NGXLogger', ['log', 'error', 'warn']); ngxLogger = jasmine.createSpyObj('NGXLogger', ['log', 'error', 'warn']);
storageProvider = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put']); storageProvider = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put']);
simpleBrowser = jasmine.createSpyObj('SimpleBrowser', ['open']); simpleBrowser = jasmine.createSpyObj('SimpleBrowser', ['open']);
modalController = jasmine.createSpyObj('ModalController', ['create', 'dismiss', 'getTop']);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes([]), HttpClientTestingModule, AuthModule], imports: [RouterTestingModule.withRoutes([]), HttpClientTestingModule, AuthModule],
@@ -85,6 +87,7 @@ describe('AppComponent', () => {
{provide: NGXLogger, useValue: ngxLogger}, {provide: NGXLogger, useValue: ngxLogger},
{provide: StorageProvider, useValue: storageProvider}, {provide: StorageProvider, useValue: storageProvider},
{provide: SimpleBrowser, useValue: simpleBrowser}, {provide: SimpleBrowser, useValue: simpleBrowser},
{provide: ModalController, useValue: modalController},
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents(); }).compileComponents();

View File

@@ -22,6 +22,7 @@ import {environment} from '../environments/environment';
import {Capacitor} from '@capacitor/core'; import {Capacitor} from '@capacitor/core';
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service'; import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
import {Keyboard, KeyboardResize} from '@capacitor/keyboard'; import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
import {AppVersionService} from './modules/about/app-version.service';
import {SplashScreen} from '@capacitor/splash-screen'; import {SplashScreen} from '@capacitor/splash-screen';
/** /**
@@ -59,13 +60,19 @@ export class AppComponent implements AfterContentInit {
private readonly authHelper: AuthHelperService, private readonly authHelper: AuthHelperService,
private readonly toastController: ToastController, private readonly toastController: ToastController,
private readonly scheduleSyncService: ScheduleSyncService, private readonly scheduleSyncService: ScheduleSyncService,
private readonly versionService: AppVersionService,
) { ) {
void this.initializeApp(); void this.initializeApp();
} }
ngAfterContentInit() { async ngAfterContentInit() {
this.scheduleSyncService.init(); this.scheduleSyncService.init();
void this.scheduleSyncService.enable(); void this.scheduleSyncService.enable();
this.versionService.getPendingReleaseNotes().then(notes => {
if (notes) {
this.versionService.presentReleaseNotes(notes);
}
});
if (document.readyState === 'complete') { if (document.readyState === 'complete') {
requestIdleCallback(this.hideSplash.bind(this)); requestIdleCallback(this.hideSplash.bind(this));

View File

@@ -18,6 +18,8 @@ import {SCAboutPage, SCAppConfiguration} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider'; import {ConfigProvider} from '../../config/config.provider';
import packageJson from '../../../../../package.json'; import packageJson from '../../../../../package.json';
import config from 'capacitor.config'; import config from 'capacitor.config';
import {App} from '@capacitor/app';
import {Capacitor} from '@capacitor/core';
@Component({ @Component({
selector: 'about-page', selector: 'about-page',
@@ -27,9 +29,11 @@ import config from 'capacitor.config';
export class AboutPageComponent implements OnInit { export class AboutPageComponent implements OnInit {
content: SCAboutPage; content: SCAboutPage;
appName = config.appName; name = config.appName;
version = packageJson.version; stappsVersion = packageJson.version;
version: string;
constructor(private readonly route: ActivatedRoute, private readonly configProvider: ConfigProvider) {} constructor(private readonly route: ActivatedRoute, private readonly configProvider: ConfigProvider) {}
@@ -37,5 +41,6 @@ export class AboutPageComponent implements OnInit {
const route = this.route.snapshot.url.map(it => it.path).join('/'); const route = this.route.snapshot.url.map(it => it.path).join('/');
this.content = this.content =
(this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {}; (this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version);
} }
} }

View File

@@ -25,7 +25,7 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content parallax *ngIf="content"> <ion-content parallax *ngIf="content">
<ion-text>{{ appName }} v{{ version }}</ion-text> <ion-text>{{ 'about.VERSION_INFO' | translate: {name, version, stappsVersion} }}</ion-text>
<div class="page-content"> <div class="page-content">
<about-page-content *ngFor="let element of content.content" [content]="element"></about-page-content> <about-page-content *ngFor="let element of content.content" [content]="element"></about-page-content>
</div> </div>

View File

@@ -0,0 +1,60 @@
import {Injectable} from '@angular/core';
import {StorageProvider} from '../storage/storage.provider';
import {ConfigProvider} from '../config/config.provider';
import {ModalController} from '@ionic/angular';
import {Capacitor} from '@capacitor/core';
import {ReleaseNotesComponent} from './release-notes.component';
import {SCAppVersionInfo} from '@openstapps/core';
import {App} from '@capacitor/app';
export const RELEASE_NOTES_SHOWN_KEY = 'release_notes_shown';
@Injectable({providedIn: 'root'})
export class AppVersionService {
constructor(
private storage: StorageProvider,
private config: ConfigProvider,
private modalController: ModalController,
) {}
/**
* Get the release notes of the latest published version
*/
get publishedVersions() {
const platform = Capacitor.getPlatform() as 'android' | 'ios' | 'web';
return this.config.config.app.versionHistory?.filter(({published}) => published[platform] !== undefined);
}
/**
* Get the latest release notes that have not been presented yet
*/
async getPendingReleaseNotes() {
if (Capacitor.getPlatform() === 'web') {
return;
}
const storedVersion = (await this.storage.has(RELEASE_NOTES_SHOWN_KEY))
? await this.storage.get<string>(RELEASE_NOTES_SHOWN_KEY)
: '';
const currentVersion = await App.getInfo().then(info => info.version);
return this.publishedVersions?.find(({version}) => {
const wasNotShown = version.localeCompare(storedVersion, undefined, {numeric: true}) === 1;
const isNotFutureVersion = version.localeCompare(currentVersion, undefined, {numeric: true}) <= 0;
return wasNotShown && isNotFutureVersion;
});
}
/**
* Present release notes
*/
async presentReleaseNotes(version: SCAppVersionInfo) {
const modal = await this.modalController.create({
component: ReleaseNotesComponent,
componentProps: {
versionInfo: version,
},
});
await modal.present();
await modal.onDidDismiss();
await this.storage.put(RELEASE_NOTES_SHOWN_KEY, version.version);
}
}

View File

@@ -0,0 +1,21 @@
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
import {SCAppVersionInfo} from '@openstapps/core';
import {MarkdownModule} from 'ngx-markdown';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {IonicModule, ModalController} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {UtilModule} from '../../util/util.module';
@Component({
selector: 'stapps-release-notes',
templateUrl: 'release-notes.html',
styleUrls: ['release-notes.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [UtilModule, MarkdownModule, ThingTranslateModule, IonicModule, TranslateModule],
})
export class ReleaseNotesComponent {
@Input() versionInfo: SCAppVersionInfo;
constructor(readonly modalController: ModalController) {}
}

View File

@@ -0,0 +1,16 @@
<ion-header>
<ion-toolbar>
<ion-title><span class="ion-text-wrap">{{'releaseNotes.TITLE_UPDATED' | translate}}</span></ion-title>
<ion-buttons slot="end">
<ion-button [strong]="true" (click)="modalController.dismiss()"
>{{'modal.DISMISS_NEUTRAL' | translate}}</ion-button
>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content parallax>
<markdown
class="content-card ion-padding"
[data]="'releaseNotes' | translateSimple: versionInfo"
></markdown>
</ion-content>

View File

@@ -0,0 +1,13 @@
@import '../../../theme/util/mixins';
ion-title {
padding-block: var(--spacing-md);
}
.content-card {
@include border-radius-in-parallax(var(--border-radius-default));
display: block;
margin: var(--spacing-md);
background: var(--ion-item-background);
}

View File

@@ -21,7 +21,6 @@ import {DaytimeKeyPipe} from './daytime-key.pipe';
import {LazyPipe} from './lazy.pipe'; import {LazyPipe} from './lazy.pipe';
import {NextDateInListPipe} from './next-date-in-list.pipe'; import {NextDateInListPipe} from './next-date-in-list.pipe';
import {EditModalComponent} from './edit-modal.component'; import {EditModalComponent} from './edit-modal.component';
import {BrowserModule} from '@angular/platform-browser';
import {IonicModule} from '@ionic/angular'; import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {ElementSizeChangeDirective} from './element-size-change.directive'; import {ElementSizeChangeDirective} from './element-size-change.directive';
@@ -33,10 +32,11 @@ import {SectionComponent} from './section.component';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {IonContentParallaxDirective} from './ion-content-parallax.directive'; import {IonContentParallaxDirective} from './ion-content-parallax.directive';
import {FormatDistanceToNowStrictPipeModule, FormatRelativeToNowPipeModule} from 'ngx-date-fns'; import {FormatDistanceToNowStrictPipeModule, FormatRelativeToNowPipeModule} from 'ngx-date-fns';
import {CommonModule} from '@angular/common';
@NgModule({ @NgModule({
imports: [ imports: [
BrowserModule, CommonModule,
IonicModule, IonicModule,
TranslateModule, TranslateModule,
ThingTranslateModule.forChild(), ThingTranslateModule.forChild(),

View File

@@ -28,6 +28,12 @@
"toast": { "toast": {
"TITLE_COPIED": "In die Zwischenablage kopiert" "TITLE_COPIED": "In die Zwischenablage kopiert"
}, },
"about": {
"VERSION_INFO": "{{name}} {{version}} basierend auf StApps {{stappsVersion}}"
},
"releaseNotes": {
"TITLE_UPDATED": "Deine App wurde aktualisiert!"
},
"app": { "app": {
"ui": { "ui": {
"CLOSE": "Schließen", "CLOSE": "Schließen",

View File

@@ -28,6 +28,12 @@
"toast": { "toast": {
"TITLE_COPIED": "Copied to clipboard" "TITLE_COPIED": "Copied to clipboard"
}, },
"about": {
"VERSION_INFO": "{{name}} {{version}} based on StApps {{stappsVersion}}"
},
"releaseNotes": {
"TITLE_UPDATED": "Your app was updated!"
},
"app": { "app": {
"ui": { "ui": {
"CLOSE": "Close", "CLOSE": "Close",

View File

@@ -18,6 +18,7 @@ import {SCMap} from '../general/map.js';
import {SCLanguageSetting, SCSetting, SCUserGroupSetting} from '../things/setting.js'; import {SCLanguageSetting, SCSetting, SCUserGroupSetting} from '../things/setting.js';
import {SCAuthorizationProviderType} from './authorization.js'; import {SCAuthorizationProviderType} from './authorization.js';
import {SCFeatureConfiguration} from './feature.js'; import {SCFeatureConfiguration} from './feature.js';
import {SCISO8601Date} from '../general/time.js';
/** /**
* An app configuration menu item * An app configuration menu item
@@ -136,6 +137,38 @@ export interface SCAppConfiguration {
* URL where a web instance of the app is available * URL where a web instance of the app is available
*/ */
url?: string; url?: string;
/**
* Version history in sequence of their publishing date.
*
* The items are ordered in descending version index order,
* where the first item is always the most recent release.
*/
versionHistory?: SCAppVersionInfo[];
}
/**
* Info about app version releases
*/
export interface SCAppVersionInfo {
/**
* Published date for each platform.
*
* Missing entries mean the version has not been published on that platform yet.
*/
published: Partial<Record<'web' | 'android' | 'ios', SCISO8601Date>>;
/**
* Version index that increments by one each version
*/
version: string;
/**
* Release notes of the version
*/
releaseNotes: string;
/**
* Translations
*/
translations: SCTranslations<{releaseNotes: string}>;
} }
/** /**