diff --git a/.circleci/config.yml b/.circleci/config.yml
index 232b792e72..e9a73594b9 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -326,6 +326,16 @@ jobs:
paths:
- << parameters.build_config_name >>.apk
root: native/android
+ - run:
+ command: mv app/build/outputs/bundle/release/app-release.aab << parameters.build_config_name >>.aab
+ name: Rename aab
+ working_directory: native/android
+ - store_artifacts:
+ path: native/android/<< parameters.build_config_name >>.aab
+ - persist_to_workspace:
+ paths:
+ - << parameters.build_config_name >>.aab
+ root: native/android
- unless:
condition:
or:
@@ -569,7 +579,7 @@ jobs:
name: '[FL] Browserstack Upload Live'
working_directory: native
- run:
- command: bundle exec fastlane android playstore_upload build_config_name:<< parameters.build_config_name >> apk_path:attached_workspace/<< parameters.build_config_name >>.apk production_delivery:"<< parameters.production_delivery >>" version_name:${NEW_VERSION_NAME} version_code:${NEW_VERSION_CODE}
+ command: bundle exec fastlane android playstore_upload build_config_name:<< parameters.build_config_name >> aab_path:attached_workspace/<< parameters.build_config_name >>.aab production_delivery:"<< parameters.production_delivery >>" version_name:${NEW_VERSION_NAME} version_code:${NEW_VERSION_CODE}
name: '[FL] Play Store Upload'
working_directory: native
- notify
diff --git a/.circleci/src/jobs/build_android.yml b/.circleci/src/jobs/build_android.yml
index a1516164ea..c8e5213882 100644
--- a/.circleci/src/jobs/build_android.yml
+++ b/.circleci/src/jobs/build_android.yml
@@ -70,6 +70,16 @@ steps:
root: native/android
paths:
- << parameters.build_config_name >>.apk
+ - run:
+ name: Rename aab
+ command: mv app/build/outputs/bundle/release/app-release.aab << parameters.build_config_name >>.aab
+ working_directory: native/android
+ - store_artifacts:
+ path: native/android/<< parameters.build_config_name >>.aab
+ - persist_to_workspace:
+ root: native/android
+ paths:
+ - << parameters.build_config_name >>.aab
- unless:
condition:
or:
diff --git a/.circleci/src/jobs/deliver_android.yml b/.circleci/src/jobs/deliver_android.yml
index 35d7c7cd6a..0fdd494385 100644
--- a/.circleci/src/jobs/deliver_android.yml
+++ b/.circleci/src/jobs/deliver_android.yml
@@ -32,6 +32,6 @@ steps:
working_directory: native
- run:
name: '[FL] Play Store Upload'
- command: bundle exec fastlane android playstore_upload build_config_name:<< parameters.build_config_name >> apk_path:attached_workspace/<< parameters.build_config_name >>.apk production_delivery:"<< parameters.production_delivery >>" version_name:${NEW_VERSION_NAME} version_code:${NEW_VERSION_CODE}
+ command: bundle exec fastlane android playstore_upload build_config_name:<< parameters.build_config_name >> aab_path:attached_workspace/<< parameters.build_config_name >>.aab production_delivery:"<< parameters.production_delivery >>" version_name:${NEW_VERSION_NAME} version_code:${NEW_VERSION_CODE}
working_directory: native
- notify
diff --git a/.eslintrc.js b/.eslintrc.js
index 3718e06eed..b222ab6fe5 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -113,6 +113,12 @@ module.exports = {
allowNullableString: true,
},
],
+ '@typescript-eslint/array-type': [
+ 'error',
+ {
+ default: 'array',
+ },
+ ],
'@typescript-eslint/switch-exhaustiveness-check': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
diff --git a/assets/icons/chat-bot.svg b/assets/icons/chat-bot.svg
new file mode 100644
index 0000000000..d269472545
--- /dev/null
+++ b/assets/icons/chat-bot.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/chat-person.svg b/assets/icons/chat-person.svg
new file mode 100644
index 0000000000..de09e960a7
--- /dev/null
+++ b/assets/icons/chat-person.svg
@@ -0,0 +1,4 @@
+
diff --git a/build-configs/BuildConfigType.ts b/build-configs/BuildConfigType.ts
index 3bb5b9d357..b8303e244e 100644
--- a/build-configs/BuildConfigType.ts
+++ b/build-configs/BuildConfigType.ts
@@ -52,9 +52,9 @@ export type CommonBuildConfigType = {
// Host name of the web app, used for sharing, deep linking and social media previews.
hostName: string
// Hostnames from which resources are automatically downloaded for offline usage.
- allowedHostNames: Array
+ allowedHostNames: string[]
// Linked hosts that can may look similar https://chromium.googlesource.com/chromium/src/+/master/docs/security/lookalikes/lookalike-domains.md#automated-warning-removal
- allowedLookalikes: Array
+ allowedLookalikes: string[]
// Regex defining which urls to intercept as they are internal ones.
supportedIframeSources: string[]
internalLinksHijackPattern: string
diff --git a/build-configs/common/theme/colors.ts b/build-configs/common/theme/colors.ts
index e04c60431e..392fe6003a 100644
--- a/build-configs/common/theme/colors.ts
+++ b/build-configs/common/theme/colors.ts
@@ -14,6 +14,7 @@ export type ColorsType = {
positiveHighlight: string
negativeHighlight: string
invalidInput: string
+ warningColor: string
linkColor: string
themeContrast: string
}
@@ -31,5 +32,6 @@ export const commonLightColors = {
positiveHighlight: '#188038',
negativeHighlight: '#8b0000',
invalidInput: '#B3261E',
+ warningColor: '#FFA726',
linkColor: '#0b57d0',
}
diff --git a/e2e-tests/native/test/helpers/Selector.ts b/e2e-tests/native/test/helpers/Selector.ts
index f2c4d636a8..79278eb070 100644
--- a/e2e-tests/native/test/helpers/Selector.ts
+++ b/e2e-tests/native/test/helpers/Selector.ts
@@ -1,5 +1,5 @@
export class Selector {
- private queries: Array = new Array()
+ private queries: string[] = new Array()
public ByText(text: string): Selector {
if (driver.isAndroid) {
diff --git a/e2e-tests/shared/constants.ts b/e2e-tests/shared/constants.ts
index 9d50835dba..e4cad3e4a2 100644
--- a/e2e-tests/shared/constants.ts
+++ b/e2e-tests/shared/constants.ts
@@ -1,6 +1,6 @@
export const filter = 'wirschaffendas'
export const contentSearch = 'language'
-export const defaultCity = 'Testumgebung Ende-zu-Ende-Testing'
+export const defaultCity = 'E2E-Testumgebung'
export const augsburgCity = 'Stadt Augsburg'
export const language = 'en'
diff --git a/e2e-tests/web/wdio.conf.ts b/e2e-tests/web/wdio.conf.ts
index 83fbd8874e..b6711e4bd3 100644
--- a/e2e-tests/web/wdio.conf.ts
+++ b/e2e-tests/web/wdio.conf.ts
@@ -1,7 +1,7 @@
import { browsers, ciCapabilities } from './capabilities.js'
import waitForLocalhost from './waitForLocalhost.js'
-const getCapabilities = (): Array => {
+const getCapabilities = (): WebdriverIO.Capabilities[] => {
if (process.env.CI) {
return [ciCapabilities]
}
diff --git a/native/android/fastlane/Fastfile b/native/android/fastlane/Fastfile
index f7a1f5c99f..108f47da94 100644
--- a/native/android/fastlane/Fastfile
+++ b/native/android/fastlane/Fastfile
@@ -68,7 +68,7 @@ lane :build do |options|
end
gradle(
- task: "assembleRelease",
+ tasks: ["assembleRelease", "bundleRelease"],
properties: {
:BUILD_CONFIG_NAME => build_config_name,
:VERSION_CODE => version_code,
diff --git a/native/fastlane/Fastfile b/native/fastlane/Fastfile
index d44554b317..d0a8505b6b 100644
--- a/native/fastlane/Fastfile
+++ b/native/fastlane/Fastfile
@@ -66,8 +66,8 @@ platform :android do
# version_code: The version code of the app
# version_name: The version name of the app
# build_config_name: The name of the build config
- # apk_path: The path of the apk to upload (relative to home dir)
- # production_delivery: Whether the apk should be uploaded to the production track
+ # aab_path: The path of the aab to upload (relative to home dir)
+ # production_delivery: Whether the aab should be uploaded to the production track
desc "Deliver the app to Play Store. Depending on the option `production_delivery` the update is released to the general public."
lane :playstore_upload do |options|
ensure_env_vars(
@@ -77,10 +77,10 @@ platform :android do
version_code = options[:version_code]
version_name = options[:version_name]
build_config_name = options[:build_config_name]
- apk_path = options[:apk_path]
+ aab_path = options[:aab_path]
production_delivery = options[:production_delivery]
- if [version_name, version_code, build_config_name, apk_path, production_delivery].include?(nil)
+ if [version_name, version_code, build_config_name, aab_path, production_delivery].include?(nil)
raise "'nil' passed as parameter! Aborting..."
end
@@ -102,7 +102,7 @@ platform :android do
skip_upload_screenshots: skip_images,
skip_upload_metadata: false,
release_status: "completed",
- apk: "#{ENV['HOME']}/#{apk_path}",
+ aab: "#{ENV['HOME']}/#{aab_path}",
json_key_data: ENV["GOOGLE_SERVICE_ACCOUNT_JSON"]
)
end
@@ -153,6 +153,7 @@ platform :android do
skip_upload_screenshots: true,
skip_upload_metadata: true,
skip_upload_apk: true,
+ skip_upload_aab: true,
release_status: "completed",
json_key_data: ENV["GOOGLE_SERVICE_ACCOUNT_JSON"]
)
diff --git a/native/run b/native/run
index 912740166f..a93ab41d51 100755
--- a/native/run
+++ b/native/run
@@ -21,7 +21,7 @@ program
.option('--production', 'whether a production (release) build should be made')
.action((buildConfigName, options) => {
const { production } = options
- const productionFlag = production ? '--mode=release' : ''
+ const buildFlag = production ? '--mode=release' : '--active-arch-only'
const jsonBuildConfig = execSync(
`yarn workspace --silent build-configs --silent manage to-json ${buildConfigName} android`,
@@ -34,7 +34,7 @@ program
.toString()
.replaceAll('\n', ' ')
execSync(
- `yarn cross-env ${buildConfig} yarn react-native run-android --no-packager --appId ${applicationId} ${productionFlag}`,
+ `yarn cross-env ${buildConfig} yarn react-native run-android --no-packager --appId ${applicationId} ${buildFlag}`,
{ stdio: 'inherit' },
)
})
diff --git a/native/src/Navigator.tsx b/native/src/Navigator.tsx
index b9afa30bc9..aeb056c4c6 100644
--- a/native/src/Navigator.tsx
+++ b/native/src/Navigator.tsx
@@ -88,7 +88,8 @@ const Stack = createStackNavigator()
const Navigator = (): ReactElement | null => {
const showSnackbar = useSnackbar()
- const { settings, cityCode, changeCityCode, languageCode, updateSettings } = useAppContext()
+ const appContext = useAppContext()
+ const { settings, cityCode, changeCityCode, updateSettings } = appContext
const navigation = useNavigation>()
const [initialRoute, setInitialRoute] = useState(null)
@@ -96,8 +97,8 @@ const Navigator = (): ReactElement | null => {
const { data: cities, error: citiesError, refresh: refreshCities } = useLoadCities()
useEffect(() => {
- initialPushNotificationRequest(cityCode, languageCode).catch(reportError)
- }, [cityCode, languageCode])
+ initialPushNotificationRequest(appContext).catch(reportError)
+ }, [appContext])
useForegroundPushNotificationListener({ showSnackbar, navigate: navigation.navigate })
diff --git a/native/src/__mocks__/react-native-blob-util.ts b/native/src/__mocks__/react-native-blob-util.ts
index 48fe48979f..5797b5e786 100644
--- a/native/src/__mocks__/react-native-blob-util.ts
+++ b/native/src/__mocks__/react-native-blob-util.ts
@@ -27,7 +27,7 @@ const existsMock = (file: string): Promise => {
return Promise.resolve(exists || isParentOfExisting)
}
-const lsMock = (path: string): Promise> => {
+const lsMock = (path: string): Promise => {
const filesInPath = Object.keys(mockFiles).filter(filePath => filePath.startsWith(path))
return Promise.resolve(filesInPath)
}
@@ -58,7 +58,7 @@ export default {
},
},
fs: {
- ls: jest.fn>, [string]>(lsMock),
+ ls: jest.fn, [string]>(lsMock),
exists: jest.fn, [string]>(existsMock),
isDir: jest.fn, [string]>(async () => true),
writeFile: jest.fn, [string, string, string]>(writeMockFile),
diff --git a/native/src/components/CitySelector.tsx b/native/src/components/CitySelector.tsx
index c49259e8da..9f70fd07b0 100644
--- a/native/src/components/CitySelector.tsx
+++ b/native/src/components/CitySelector.tsx
@@ -33,7 +33,7 @@ const SearchCounter = styled.Text`
`
type CitySelectorProps = {
- cities: Array
+ cities: CityModel[]
navigateToDashboard: (city: CityModel) => void
}
diff --git a/native/src/components/CustomHeaderButtons.tsx b/native/src/components/CustomHeaderButtons.tsx
index 8367de5e9d..31dae0478f 100644
--- a/native/src/components/CustomHeaderButtons.tsx
+++ b/native/src/components/CustomHeaderButtons.tsx
@@ -40,8 +40,8 @@ const onOverflowMenuPress = (cancelButtonLabel: string) => (props: OnOverflowMen
const CustomHeaderButtons = (props: {
cancelLabel: string
- items: Array
- overflowItems: Array
+ items: ReactNode[]
+ overflowItems: ReactNode[]
}): ReactElement => {
const { cancelLabel, items, overflowItems } = props
const { t } = useTranslation('common')
diff --git a/native/src/components/ExportEventButton.tsx b/native/src/components/ExportEventButton.tsx
index f09e16abbf..35ea57a4d7 100644
--- a/native/src/components/ExportEventButton.tsx
+++ b/native/src/components/ExportEventButton.tsx
@@ -1,9 +1,9 @@
import { DateTime } from 'luxon'
import React, { ReactElement, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { Linking, Platform } from 'react-native'
+import { Platform } from 'react-native'
import RNCalendarEvents, { Calendar, CalendarEventWritable, RecurrenceFrequency } from 'react-native-calendar-events'
-import { PERMISSIONS, requestMultiple } from 'react-native-permissions'
+import { PERMISSIONS, openSettings, requestMultiple } from 'react-native-permissions'
import { Frequency } from 'rrule'
import styled from 'styled-components/native'
@@ -89,8 +89,8 @@ const ExportEventButton = ({ event }: ExportEventButtonType): ReactElement => {
showSnackbar({
text: 'noCalendarPermission',
positiveAction: {
- label: t('settings'),
- onPress: Linking.openSettings,
+ label: t('layout:settings'),
+ onPress: openSettings,
},
})
return
diff --git a/native/src/components/List.tsx b/native/src/components/List.tsx
index 118d0f016b..5e3db382ca 100644
--- a/native/src/components/List.tsx
+++ b/native/src/components/List.tsx
@@ -10,7 +10,7 @@ const NoItemsMessage = styled.Text`
`
type ListProps = {
- items: Array
+ items: T[]
noItemsMessage?: ReactElement | string
renderItem: (props: { item: T; index: number }) => ReactElement
Header?: ReactElement
diff --git a/native/src/components/NearbyCities.tsx b/native/src/components/NearbyCities.tsx
index a953b61461..d4a06bd02d 100644
--- a/native/src/components/NearbyCities.tsx
+++ b/native/src/components/NearbyCities.tsx
@@ -31,7 +31,7 @@ const StyledIcon = styled(Icon)`
`
type NearbyCitiesProps = {
- cities: Array
+ cities: CityModel[]
navigateToDashboard: (city: CityModel) => void
filterText: string
}
diff --git a/native/src/components/News.tsx b/native/src/components/News.tsx
index 98b7f55885..a33d6edea3 100644
--- a/native/src/components/News.tsx
+++ b/native/src/components/News.tsx
@@ -37,7 +37,7 @@ const getPageTitle = (
return t('localNews.pageTitle')
}
-type NewsModelsType = Array
+type NewsModelsType = (LocalNewsModel | TunewsModel)[]
type NewsProps = {
news: NewsModelsType
diff --git a/native/src/components/Note.tsx b/native/src/components/Note.tsx
index a930bcdc9a..efc96a39c2 100644
--- a/native/src/components/Note.tsx
+++ b/native/src/components/Note.tsx
@@ -5,7 +5,7 @@ import { NoteIcon } from '../assets'
import Icon from './base/Icon'
const NoteBox = styled.View`
- background-color: ${props => props.theme.colors.themeColor};
+ background-color: ${props => props.theme.colors.warningColor};
margin-top: 12px;
padding: 12px;
flex-direction: row;
diff --git a/native/src/components/Selector.tsx b/native/src/components/Selector.tsx
index 8185e4f1dc..10253c02b0 100644
--- a/native/src/components/Selector.tsx
+++ b/native/src/components/Selector.tsx
@@ -15,7 +15,7 @@ export const Wrapper = styled.View`
`
type SelectorProps = {
- items: Array
+ items: SelectorItemModel[]
selectedItemCode: string | null
}
diff --git a/native/src/components/SettingItem.tsx b/native/src/components/SettingItem.tsx
index f73dac6eba..fb47868e76 100644
--- a/native/src/components/SettingItem.tsx
+++ b/native/src/components/SettingItem.tsx
@@ -45,7 +45,7 @@ const Badge = styled.View<{ enabled: boolean }>`
type SettingItemProps = {
title: string
description?: string
- onPress: () => void
+ onPress: () => Promise
bigTitle?: boolean
role?: Role
hasSwitch?: boolean
diff --git a/native/src/components/__tests__/News.spec.tsx b/native/src/components/__tests__/News.spec.tsx
index 78ca6ee904..461dc16584 100644
--- a/native/src/components/__tests__/News.spec.tsx
+++ b/native/src/components/__tests__/News.spec.tsx
@@ -68,7 +68,7 @@ describe('News', () => {
localNewsEnabled = true,
}: {
newsId?: number | null
- data?: Array
+ data?: (LocalNewsModel | TunewsModel)[]
loadingMore?: boolean
selectedNewsType?: TuNewsType | LocalNewsType
tuNewsEnabled?: boolean
diff --git a/native/src/constants/NavigationTypes.ts b/native/src/constants/NavigationTypes.ts
index 0e888387c6..1f452a1c05 100644
--- a/native/src/constants/NavigationTypes.ts
+++ b/native/src/constants/NavigationTypes.ts
@@ -100,8 +100,8 @@ export type RoutesParamsType = {
}
[LICENSES_ROUTE]: undefined
[CHANGE_LANGUAGE_MODAL_ROUTE]: {
- languages: Array
- availableLanguages: Array
+ languages: LanguageModel[]
+ availableLanguages: string[]
}
[PDF_VIEW_MODAL_ROUTE]: {
url: string
diff --git a/native/src/contexts/AppContextProvider.tsx b/native/src/contexts/AppContextProvider.tsx
index 14f72b140e..47cdc7ebde 100644
--- a/native/src/contexts/AppContextProvider.tsx
+++ b/native/src/contexts/AppContextProvider.tsx
@@ -34,6 +34,7 @@ type AppContextProviderProps = {
const AppContextProvider = ({ children }: AppContextProviderProps): ReactElement | null => {
const [settings, setSettings] = useState(null)
+ const allowPushNotifications = !!settings?.allowPushNotifications
const cityCode = settings?.selectedCity
const languageCode = settings?.contentLanguage
const { i18n } = useTranslation()
@@ -55,10 +56,10 @@ const AppContextProvider = ({ children }: AppContextProviderProps): ReactElement
unsubscribeNews(cityCode, languageCode).catch(reportError)
}
if (languageCode && newCityCode) {
- subscribeNews(newCityCode, languageCode).catch(reportError)
+ subscribeNews({ cityCode: newCityCode, languageCode, allowPushNotifications }).catch(reportError)
}
},
- [updateSettings, cityCode, languageCode],
+ [updateSettings, cityCode, languageCode, allowPushNotifications],
)
const changeLanguageCode = useCallback(
@@ -68,10 +69,10 @@ const AppContextProvider = ({ children }: AppContextProviderProps): ReactElement
unsubscribeNews(cityCode, languageCode).catch(reportError)
}
if (cityCode) {
- subscribeNews(cityCode, newLanguageCode).catch(reportError)
+ subscribeNews({ cityCode, languageCode: newLanguageCode, allowPushNotifications }).catch(reportError)
}
},
- [updateSettings, cityCode, languageCode],
+ [updateSettings, cityCode, languageCode, allowPushNotifications],
)
useEffect(() => {
diff --git a/native/src/contexts/__tests__/AppContextProvider.spec.tsx b/native/src/contexts/__tests__/AppContextProvider.spec.tsx
index 04e0be5ab3..4429802558 100644
--- a/native/src/contexts/__tests__/AppContextProvider.spec.tsx
+++ b/native/src/contexts/__tests__/AppContextProvider.spec.tsx
@@ -135,7 +135,7 @@ describe('AppContextProvider', () => {
expect(await appSettings.loadSettings()).toMatchObject({ selectedCity: 'hallo' })
expect(setSettings).toHaveBeenCalledTimes(1)
expect(subscribeNews).toHaveBeenCalledTimes(1)
- expect(subscribeNews).toHaveBeenCalledWith('hallo', 'de')
+ expect(subscribeNews).toHaveBeenCalledWith({ cityCode: 'hallo', languageCode: 'de', allowPushNotifications: true })
})
it('should select city', async () => {
@@ -150,7 +150,11 @@ describe('AppContextProvider', () => {
expect(await appSettings.loadSettings()).toMatchObject({ selectedCity: 'augsburg' })
expect(setSettings).toHaveBeenCalledTimes(1)
expect(subscribeNews).toHaveBeenCalledTimes(1)
- expect(subscribeNews).toHaveBeenCalledWith('augsburg', 'de')
+ expect(subscribeNews).toHaveBeenCalledWith({
+ cityCode: 'augsburg',
+ languageCode: 'de',
+ allowPushNotifications: true,
+ })
expect(unsubscribeNews).not.toHaveBeenCalled()
})
@@ -166,7 +170,11 @@ describe('AppContextProvider', () => {
expect(await appSettings.loadSettings()).toMatchObject({ selectedCity: 'augsburg' })
expect(setSettings).toHaveBeenCalledTimes(1)
expect(subscribeNews).toHaveBeenCalledTimes(1)
- expect(subscribeNews).toHaveBeenCalledWith('augsburg', 'de')
+ expect(subscribeNews).toHaveBeenCalledWith({
+ cityCode: 'augsburg',
+ languageCode: 'de',
+ allowPushNotifications: true,
+ })
expect(unsubscribeNews).toHaveBeenCalledTimes(1)
expect(unsubscribeNews).toHaveBeenCalledWith('muenchen', 'de')
})
@@ -198,7 +206,11 @@ describe('AppContextProvider', () => {
expect(await appSettings.loadSettings()).toMatchObject({ contentLanguage: 'ar' })
expect(setSettings).toHaveBeenCalledTimes(1)
expect(subscribeNews).toHaveBeenCalledTimes(1)
- expect(subscribeNews).toHaveBeenCalledWith('muenchen', 'ar')
+ expect(subscribeNews).toHaveBeenCalledWith({
+ cityCode: 'muenchen',
+ languageCode: 'ar',
+ allowPushNotifications: true,
+ })
expect(unsubscribeNews).toHaveBeenCalledTimes(1)
expect(unsubscribeNews).toHaveBeenCalledWith('muenchen', 'de')
})
diff --git a/native/src/hooks/useCityAppContext.ts b/native/src/hooks/useCityAppContext.ts
index c6d9fe1763..07342b9206 100644
--- a/native/src/hooks/useCityAppContext.ts
+++ b/native/src/hooks/useCityAppContext.ts
@@ -2,13 +2,13 @@ import { useContext } from 'react'
import { AppContext, AppContextType } from '../contexts/AppContextProvider'
-type UseCityAppContextReturn = AppContextType & {
+export type CityAppContext = AppContextType & {
cityCode: string
}
export const useAppContext = (): AppContextType => useContext(AppContext)
-const useCityAppContext = (): UseCityAppContextReturn => {
+const useCityAppContext = (): CityAppContext => {
const { cityCode, ...context } = useAppContext()
if (!cityCode) {
throw new Error('City code not set!')
diff --git a/native/src/navigation/navigateToLanguageChange.ts b/native/src/navigation/navigateToLanguageChange.ts
index df276c860c..7bd627487b 100644
--- a/native/src/navigation/navigateToLanguageChange.ts
+++ b/native/src/navigation/navigateToLanguageChange.ts
@@ -10,8 +10,8 @@ const navigateToLanguageChange = ({
availableLanguages,
}: {
navigation: NavigationProps
- languages: Array
- availableLanguages: Array
+ languages: LanguageModel[]
+ availableLanguages: string[]
}): void => {
sendTrackingSignal({
signal: {
diff --git a/native/src/routes/Events.tsx b/native/src/routes/Events.tsx
index 88aab065d0..26fa75fffa 100644
--- a/native/src/routes/Events.tsx
+++ b/native/src/routes/Events.tsx
@@ -32,7 +32,7 @@ const PageDetailsContainer = styled.View`
export type EventsProps = {
slug?: string
- events: Array
+ events: EventModel[]
cityModel: CityModel
language: string
navigateTo: (routeInformation: RouteInformationType) => void
diff --git a/native/src/routes/Intro.tsx b/native/src/routes/Intro.tsx
index 4dc44f1cee..4bd30d19e4 100644
--- a/native/src/routes/Intro.tsx
+++ b/native/src/routes/Intro.tsx
@@ -124,7 +124,7 @@ const Intro = ({ route, navigation }: IntroProps): ReactElement => {
const renderSlide = ({ item }: { item: SlideContentType }) =>
- const onViewableItemsChanged = useCallback(({ viewableItems }: { viewableItems: Array }) => {
+ const onViewableItemsChanged = useCallback(({ viewableItems }: { viewableItems: ViewToken[] }) => {
const viewableItem = viewableItems[0]
if (viewableItem) {
if (viewableItem.index !== null) {
diff --git a/native/src/routes/Pois.tsx b/native/src/routes/Pois.tsx
index e9843af558..8ef2ff83a2 100644
--- a/native/src/routes/Pois.tsx
+++ b/native/src/routes/Pois.tsx
@@ -46,7 +46,7 @@ const getBottomSheetSnapPoints = (deviceHeight: number): [number, number, number
]
type PoisProps = {
- pois: Array
+ pois: PoiModel[]
cityModel: CityModel
language: string
refresh: () => void
diff --git a/native/src/routes/SearchModal.tsx b/native/src/routes/SearchModal.tsx
index f1b3103427..9e7da908ea 100644
--- a/native/src/routes/SearchModal.tsx
+++ b/native/src/routes/SearchModal.tsx
@@ -28,7 +28,7 @@ const SearchCounter = styled.Text`
`
export type SearchModalProps = {
- allPossibleResults: Array
+ allPossibleResults: SearchResult[]
languageCode: string
cityCode: string
closeModal: (query: string) => void
diff --git a/native/src/routes/Settings.tsx b/native/src/routes/Settings.tsx
index 3a806e5296..37595d1c9d 100644
--- a/native/src/routes/Settings.tsx
+++ b/native/src/routes/Settings.tsx
@@ -1,4 +1,4 @@
-import React, { ReactElement, useContext } from 'react'
+import React, { ReactElement } from 'react'
import { useTranslation } from 'react-i18next'
import { SectionList, SectionListData } from 'react-native'
import styled from 'styled-components/native'
@@ -10,10 +10,8 @@ import Layout from '../components/Layout'
import SettingItem from '../components/SettingItem'
import ItemSeparator from '../components/base/ItemSeparator'
import { NavigationProps } from '../constants/NavigationTypes'
-import { AppContext } from '../contexts/AppContextProvider'
-import { useAppContext } from '../hooks/useCityAppContext'
+import useCityAppContext from '../hooks/useCityAppContext'
import useSnackbar from '../hooks/useSnackbar'
-import { SettingsType } from '../utils/AppSettings'
import createSettingsSections, { SettingsSectionType } from '../utils/createSettingsSections'
import { log, reportError } from '../utils/sentry'
@@ -31,36 +29,27 @@ const SectionHeader = styled.Text`
`
const Settings = ({ navigation }: SettingsProps): ReactElement => {
- const { settings, updateSettings } = useAppContext()
- const { cityCode, languageCode } = useContext(AppContext)
+ const appContext = useCityAppContext()
const showSnackbar = useSnackbar()
const { t } = useTranslation('settings')
+ const { settings } = appContext
- const setSetting = async (
- changeSetting: (settings: SettingsType) => Partial,
- changeAction?: (settings: SettingsType) => Promise,
- ) => {
+ const safeOnPress = (update: () => Promise | void) => async () => {
const oldSettings = settings
- const newSettings = { ...oldSettings, ...changeSetting(settings) }
- updateSettings(newSettings)
-
try {
- const successful = changeAction ? await changeAction(newSettings) : true
-
- if (!successful) {
- updateSettings(oldSettings)
- }
+ await update()
} catch (e) {
log('Failed to persist settings.', 'error')
reportError(e)
- updateSettings(oldSettings)
+ appContext.updateSettings(oldSettings)
+ showSnackbar({ text: t('error:settingsError') })
}
}
const renderItem = ({ item }: { item: SettingsSectionType }) => {
- const { getSettingValue, ...otherProps } = item
+ const { getSettingValue, onPress, ...otherProps } = item
const value = !!(getSettingValue && getSettingValue(settings))
- return
+ return
}
const renderSectionHeader = ({ section: { title } }: { section: SectionType }) => {
@@ -72,13 +61,10 @@ const Settings = ({ navigation }: SettingsProps): ReactElement => {
}
const sections = createSettingsSections({
- setSetting,
- t,
- languageCode,
- cityCode,
+ appContext,
navigation,
- settings,
showSnackbar,
+ t,
})
return (
diff --git a/native/src/testing/TestingAppContext.tsx b/native/src/testing/TestingAppContext.tsx
index 6a991ee9d7..fc1961a187 100644
--- a/native/src/testing/TestingAppContext.tsx
+++ b/native/src/testing/TestingAppContext.tsx
@@ -3,27 +3,30 @@ import React, { ReactElement, ReactNode } from 'react'
import { AppContext, AppContextType } from '../contexts/AppContextProvider'
import { defaultSettings, SettingsType } from '../utils/AppSettings'
-const TestingAppContext = ({
- children,
+type TestingAppContextParams = { settings?: Partial } & Omit, 'settings'>
+
+export const testingAppContext = ({
settings = {},
cityCode = 'augsburg',
languageCode = 'de',
changeCityCode = jest.fn(),
changeLanguageCode = jest.fn(),
updateSettings = jest.fn(),
-}: {
- settings?: Partial
- children: ReactNode
-} & Omit, 'settings'>): ReactElement => {
- const context = {
- settings: { ...defaultSettings, ...settings },
- cityCode,
- languageCode,
- updateSettings,
- changeCityCode,
- changeLanguageCode,
- }
+}: TestingAppContextParams): AppContextType => ({
+ settings: { ...defaultSettings, ...settings },
+ cityCode,
+ languageCode,
+ updateSettings,
+ changeCityCode,
+ changeLanguageCode,
+})
+
+const TestingAppContextProvider = ({
+ children,
+ ...props
+}: { children: ReactNode } & TestingAppContextParams): ReactElement => {
+ const context = testingAppContext(props)
return {children}
}
-export default TestingAppContext
+export default TestingAppContextProvider
diff --git a/native/src/utils/AppSettings.ts b/native/src/utils/AppSettings.ts
index 4f1b776d12..8f650dfae8 100644
--- a/native/src/utils/AppSettings.ts
+++ b/native/src/utils/AppSettings.ts
@@ -14,7 +14,7 @@ export type SettingsType = {
apiUrlOverride: string | null
jpalTrackingEnabled: boolean | null
jpalTrackingCode: string | null
- jpalSignals: Array
+ jpalSignals: SignalType[]
externalSourcePermissions: ExternalSourcePermissions
}
export const defaultSettings: SettingsType = {
diff --git a/native/src/utils/DataContainer.ts b/native/src/utils/DataContainer.ts
index 0f361347fe..b319476b34 100644
--- a/native/src/utils/DataContainer.ts
+++ b/native/src/utils/DataContainer.ts
@@ -15,24 +15,24 @@ export type DataContainer = {
* Returns an Array of PoiModels.
* @throws Will throw an error if the array is null.
*/
- getPois: (city: string, language: string) => Promise>
+ getPois: (city: string, language: string) => Promise
/**
* Sets the pois and persist them ?
*/
- setPois: (city: string, language: string, pois: Array) => Promise
+ setPois: (city: string, language: string, pois: PoiModel[]) => Promise
/**
* Returns an Array of CityModels.
* @throws Will throw an error if the array is null.
*/
- getCities: () => Promise>
+ getCities: () => Promise
/**
* Sets the cities but does not persist them.
* For now switching cities when offline is not possible.
*/
- setCities: (cities: Array) => Promise
+ setCities: (cities: CityModel[]) => Promise
/**
* Returns the CategoriesMapModel.
@@ -49,23 +49,23 @@ export type DataContainer = {
* Returns an Array of events.
* @throws Will throw an error if the array is null.
*/
- getEvents: (city: string, language: string) => Promise>
+ getEvents: (city: string, language: string) => Promise
/**
* Sets the events and persists them.
*/
- setEvents: (city: string, language: string, events: Array) => Promise
+ setEvents: (city: string, language: string, events: EventModel[]) => Promise
/**
* Returns an Array of local news.
* @throws Will throw an error if the array is null.
*/
- getLocalNews: (city: string, language: string) => Promise>
+ getLocalNews: (city: string, language: string) => Promise
/**
* Sets the local news and persists them.
*/
- setLocalNews: (city: string, language: string, events: Array) => Promise
+ setLocalNews: (city: string, language: string, events: LocalNewsModel[]) => Promise
/**
* Returns the ResourceCache.
diff --git a/native/src/utils/DatabaseConnector.ts b/native/src/utils/DatabaseConnector.ts
index 561bdc8a7c..8b2ad9e728 100644
--- a/native/src/utils/DatabaseConnector.ts
+++ b/native/src/utils/DatabaseConnector.ts
@@ -54,7 +54,7 @@ type ContentCategoryJsonType = {
thumbnail: string | null
available_languages: Record
parent_path: string
- children: Array
+ children: string[]
order: number
organization: {
name: string
@@ -279,7 +279,7 @@ class DatabaseConnector {
this._storeMetaCities(metaData)
}
- async _deleteMetaOfCities(cities: Array): Promise {
+ async _deleteMetaOfCities(cities: string[]): Promise {
const metaCities = await this._loadMetaCities()
cities.forEach(city => delete metaCities[city])
await this._storeMetaCities(metaCities)
@@ -342,7 +342,7 @@ class DatabaseConnector {
await this.writeFile(path, JSON.stringify(citiesMetaJson))
}
- async loadLastUsages(): Promise> {
+ async loadLastUsages(): Promise {
const metaData = await this._loadMetaCities()
return map(metaData, (value, key) => ({
city: key,
@@ -437,7 +437,7 @@ class DatabaseConnector {
return this.readFile(path, mapCategoriesJson)
}
- async storePois(pois: Array, context: DatabaseContext): Promise {
+ async storePois(pois: PoiModel[], context: DatabaseContext): Promise {
const jsonModels = pois.map(
(poi: PoiModel): ContentPoiJsonType => ({
path: poi.path,
@@ -484,7 +484,7 @@ class DatabaseConnector {
await this.writeFile(this.getContentPath('pois', context), JSON.stringify(jsonModels))
}
- async loadPois(context: DatabaseContext): Promise> {
+ async loadPois(context: DatabaseContext): Promise {
const path = this.getContentPath('pois', context)
const mapPoisJson = (json: ContentPoiJsonType[]) =>
json.map(jsonObject => {
@@ -569,7 +569,7 @@ class DatabaseConnector {
return this.readFile(path, mapLocalNewsJson)
}
- async storeCities(cities: Array): Promise {
+ async storeCities(cities: CityModel[]): Promise {
const jsonModels = cities.map(
(city: CityModel): ContentCityJsonType => ({
name: city.name,
@@ -592,7 +592,7 @@ class DatabaseConnector {
await this.writeFile(this.getCitiesPath(), JSON.stringify(jsonModels))
}
- async loadCities(): Promise> {
+ async loadCities(): Promise {
const path = this.getCitiesPath()
const mapCityJson = (json: ContentCityJsonType[]) =>
json.map(
@@ -619,7 +619,7 @@ class DatabaseConnector {
return this.readFile(path, mapCityJson)
}
- async storeEvents(events: Array, context: DatabaseContext): Promise {
+ async storeEvents(events: EventModel[], context: DatabaseContext): Promise {
const jsonModels = events.map(
(event: EventModel): ContentEventJsonType => ({
path: event.path,
@@ -663,7 +663,7 @@ class DatabaseConnector {
await this.writeFile(this.getContentPath('events', context), JSON.stringify(jsonModels))
}
- async loadEvents(context: DatabaseContext): Promise> {
+ async loadEvents(context: DatabaseContext): Promise {
const path = this.getContentPath('events', context)
const mapEventsJson = (json: ContentEventJsonType[]) =>
json.map(jsonObject => {
diff --git a/native/src/utils/DefaultDataContainer.ts b/native/src/utils/DefaultDataContainer.ts
index e0fe9cad6c..4151ceb7ea 100644
--- a/native/src/utils/DefaultDataContainer.ts
+++ b/native/src/utils/DefaultDataContainer.ts
@@ -16,11 +16,11 @@ import DatabaseConnector from './DatabaseConnector'
import { log } from './sentry'
type CacheType = {
- pois: Cache>
- cities: Cache>
- events: Cache>
+ pois: Cache
+ cities: Cache
+ events: Cache
categories: Cache
- localNews: Cache>
+ localNews: Cache
resourceCache: Cache
lastUpdate: Cache
}
@@ -33,21 +33,21 @@ class DefaultDataContainer implements DataContainer {
constructor() {
this._databaseConnector = new DatabaseConnector()
this.caches = {
- pois: new Cache>(
+ pois: new Cache(
this._databaseConnector,
(connector: DatabaseConnector, context: DatabaseContext) => connector.loadPois(context),
- (value: Array, connector: DatabaseConnector, context: DatabaseContext) =>
+ (value: PoiModel[], connector: DatabaseConnector, context: DatabaseContext) =>
connector.storePois(value, context),
),
- cities: new Cache>(
+ cities: new Cache(
this._databaseConnector,
(connector: DatabaseConnector) => connector.loadCities(),
- (value: Array, connector: DatabaseConnector) => connector.storeCities(value),
+ (value: CityModel[], connector: DatabaseConnector) => connector.storeCities(value),
),
- events: new Cache>(
+ events: new Cache(
this._databaseConnector,
(connector: DatabaseConnector, context: DatabaseContext) => connector.loadEvents(context),
- (value: Array, connector: DatabaseConnector, context: DatabaseContext) =>
+ (value: EventModel[], connector: DatabaseConnector, context: DatabaseContext) =>
connector.storeEvents(value, context),
),
categories: new Cache(
@@ -94,7 +94,7 @@ class DefaultDataContainer implements DataContainer {
return this.caches[key].isCached(context)
}
- getCities = async (): Promise> => {
+ getCities = async (): Promise => {
const cache = this.caches.cities
return cache.get(new DatabaseContext())
}
@@ -105,15 +105,15 @@ class DefaultDataContainer implements DataContainer {
return cache.get(context)
}
- getEvents = (city: string, language: string): Promise> => {
+ getEvents = (city: string, language: string): Promise => {
const context = new DatabaseContext(city, language)
- const cache: Cache> = this.caches.events
+ const cache: Cache = this.caches.events
return cache.get(context)
}
- getPois = (city: string, language: string): Promise> => {
+ getPois = (city: string, language: string): Promise => {
const context = new DatabaseContext(city, language)
- const cache: Cache> = this.caches.pois
+ const cache: Cache = this.caches.pois
return cache.get(context)
}
@@ -143,9 +143,9 @@ class DefaultDataContainer implements DataContainer {
await cache.cache(categories, context)
}
- setPois = async (city: string, language: string, pois: Array): Promise => {
+ setPois = async (city: string, language: string, pois: PoiModel[]): Promise => {
const context = new DatabaseContext(city, language)
- const cache: Cache> = this.caches.pois
+ const cache: Cache = this.caches.pois
await cache.cache(pois, context)
}
@@ -155,22 +155,21 @@ class DefaultDataContainer implements DataContainer {
await cache.cache(localNews, context)
}
- setCities = async (cities: Array): Promise => {
+ setCities = async (cities: CityModel[]): Promise => {
const cache = this.caches.cities
await cache.cache(cities, new DatabaseContext())
}
- setEvents = async (city: string, language: string, events: Array): Promise => {
+ setEvents = async (city: string, language: string, events: EventModel[]): Promise => {
const context = new DatabaseContext(city, language)
- const cache: Cache> = this.caches.events
+ const cache: Cache = this.caches.events
await cache.cache(events, context)
}
- getFilePathsFromLanguageResourceCache(languageResourceCache: LanguageResourceCacheStateType): Array {
- const pageResourceCaches: Array = Object.values(languageResourceCache)
- return flatMap(
- pageResourceCaches,
- (file: PageResourceCacheStateType): Array => map(file, ({ filePath }) => filePath),
+ getFilePathsFromLanguageResourceCache(languageResourceCache: LanguageResourceCacheStateType): string[] {
+ const pageResourceCaches: PageResourceCacheStateType[] = Object.values(languageResourceCache)
+ return flatMap(pageResourceCaches, (file: PageResourceCacheStateType): string[] =>
+ map(file, ({ filePath }) => filePath),
)
}
diff --git a/native/src/utils/NativeLanguageDetector.ts b/native/src/utils/NativeLanguageDetector.ts
index 3896304886..db5aab9b37 100644
--- a/native/src/utils/NativeLanguageDetector.ts
+++ b/native/src/utils/NativeLanguageDetector.ts
@@ -23,7 +23,7 @@ const NativeLanguageDetector: LanguageDetectorModule = {
)
return acc
},
- [] as Array,
+ [] as (string | undefined)[],
)
// Return the first supported languageTag or our fallback
return supportedKeys.find(it => it !== undefined) ?? config.defaultFallback
diff --git a/native/src/utils/PushNotificationsManager.ts b/native/src/utils/PushNotificationsManager.ts
index 24ca65232e..525e60a1d6 100644
--- a/native/src/utils/PushNotificationsManager.ts
+++ b/native/src/utils/PushNotificationsManager.ts
@@ -9,10 +9,12 @@ import { LOCAL_NEWS_TYPE, NEWS_ROUTE, NonNullableRouteInformationType } from 'sh
import { SnackbarType } from '../components/SnackbarContainer'
import { RoutesType } from '../constants/NavigationTypes'
import buildConfig from '../constants/buildConfig'
+import { AppContextType } from '../contexts/AppContextProvider'
import urlFromRouteInformation from '../navigation/url'
-import appSettings from './AppSettings'
import { log, reportError } from './sentry'
+type UpdateSettingsType = (settings: { allowPushNotifications: boolean }) => void
+
type Message = FirebaseMessagingTypes.RemoteMessage & {
notification: { title: string }
data: {
@@ -30,7 +32,7 @@ const importFirebaseMessaging = async (): Promise<() => FirebaseMessagingTypes.M
export const pushNotificationsEnabled = (): boolean =>
buildConfig().featureFlags.pushNotifications && !buildConfig().featureFlags.floss
-export const requestPushNotificationPermission = async (): Promise => {
+export const requestPushNotificationPermission = async (updateSettings: UpdateSettingsType): Promise => {
if (!pushNotificationsEnabled()) {
log('Push notifications disabled, no permissions requested.')
return false
@@ -41,7 +43,7 @@ export const requestPushNotificationPermission = async (): Promise => {
if (permissionStatus !== RESULTS.GRANTED) {
log(`Permission denied, disabling push notifications in settings.`)
- await appSettings.setSettings({ allowPushNotifications: false })
+ updateSettings({ allowPushNotifications: false })
}
return permissionStatus === RESULTS.GRANTED
@@ -65,15 +67,27 @@ export const unsubscribeNews = async (city: string, language: string): Promise => {
+
+type SubscribeNewsParams = {
+ cityCode: string
+ languageCode: string
+ allowPushNotifications: boolean
+ skipSettingsCheck?: boolean
+}
+
+export const subscribeNews = async ({
+ cityCode,
+ languageCode,
+ allowPushNotifications,
+ skipSettingsCheck = false,
+}: SubscribeNewsParams): Promise => {
try {
- const { allowPushNotifications } = await appSettings.loadSettings()
if (!pushNotificationsEnabled() || (!allowPushNotifications && !skipSettingsCheck)) {
log('Push notifications disabled, subscription skipped.')
return
}
- const topic = newsTopic(city, language)
+ const topic = newsTopic(cityCode, languageCode)
const messaging = await importFirebaseMessaging()
await messaging().subscribeToTopic(topic)
@@ -154,7 +168,6 @@ export const backgroundAppStatePushNotificationListener = (listener: (url: strin
importFirebaseMessaging()
.then(messaging => {
const onReceiveURL = ({ url }: { url: string }) => listener(url)
-
const onReceiveURLListener = Linking.addListener('url', onReceiveURL)
const unsubscribeNotification = messaging().onNotificationOpenedApp(message =>
@@ -175,13 +188,15 @@ export const backgroundAppStatePushNotificationListener = (listener: (url: strin
// Since Android 13 and iOS 17 an explicit permission request is needed, otherwise push notifications are not received.
// Therefore request the permissions once if not yet granted and subscribe to the current channel if successful.
// See https://github.com/digitalfabrik/integreat-app/issues/2438 and https://github.com/digitalfabrik/integreat-app/issues/2655
-export const initialPushNotificationRequest = async (cityCode: string | null, languageCode: string): Promise => {
- const { allowPushNotifications } = await appSettings.loadSettings()
+export const initialPushNotificationRequest = async (appContext: AppContextType): Promise => {
+ const { cityCode, languageCode, settings, updateSettings } = appContext
+ const { allowPushNotifications } = settings
+
const pushNotificationPermissionGranted = (await checkNotifications()).status === RESULTS.GRANTED
if (!pushNotificationPermissionGranted && allowPushNotifications) {
- const success = await requestPushNotificationPermission()
+ const success = await requestPushNotificationPermission(updateSettings)
if (success && cityCode) {
- await subscribeNews(cityCode, languageCode)
+ await subscribeNews({ cityCode, languageCode, allowPushNotifications })
}
}
}
diff --git a/native/src/utils/ResourceURLFinder.ts b/native/src/utils/ResourceURLFinder.ts
index 5beff7e5ac..f73f3b1a7b 100644
--- a/native/src/utils/ResourceURLFinder.ts
+++ b/native/src/utils/ResourceURLFinder.ts
@@ -24,9 +24,9 @@ type InputEntryType = {
export default class ResourceURLFinder {
_parser: Parser | null = null
_foundUrls: Set = new Set()
- _allowedHostNames: Array
+ _allowedHostNames: string[]
- constructor(allowedHostNames: Array) {
+ constructor(allowedHostNames: string[]) {
this._allowedHostNames = allowedHostNames
}
diff --git a/native/src/utils/__tests__/PushNotificationsManager.spec.ts b/native/src/utils/__tests__/PushNotificationsManager.spec.ts
index f31691f04a..985219527a 100644
--- a/native/src/utils/__tests__/PushNotificationsManager.spec.ts
+++ b/native/src/utils/__tests__/PushNotificationsManager.spec.ts
@@ -1,23 +1,20 @@
-import AsyncStorage from '@react-native-async-storage/async-storage'
import messaging, { FirebaseMessagingTypes } from '@react-native-firebase/messaging'
import { mocked } from 'jest-mock'
import { requestNotifications } from 'react-native-permissions'
import buildConfig from '../../constants/buildConfig'
-import appSettings from '../AppSettings'
import * as PushNotificationsManager from '../PushNotificationsManager'
jest.mock('@react-native-firebase/messaging', () => jest.fn(() => ({})))
describe('PushNotificationsManager', () => {
- beforeEach(() => {
- AsyncStorage.clear()
- jest.clearAllMocks()
- })
+ beforeEach(jest.clearAllMocks)
+
const mockedFirebaseMessaging = mocked<() => FirebaseMessagingTypes.Module>(messaging)
const mockedBuildConfig = mocked(buildConfig)
const previousFirebaseMessaging = mockedFirebaseMessaging()
const navigateToDeepLink = jest.fn()
+ const updateSettings = jest.fn()
const mockBuildConfig = (pushNotifications: boolean, floss: boolean) => {
const previous = buildConfig()
@@ -57,7 +54,7 @@ describe('PushNotificationsManager', () => {
return previous
})
- expect(await PushNotificationsManager.requestPushNotificationPermission()).toBeFalsy()
+ expect(await PushNotificationsManager.requestPushNotificationPermission(updateSettings)).toBeFalsy()
expect(mockRequestPermission).not.toHaveBeenCalled()
})
@@ -70,7 +67,7 @@ describe('PushNotificationsManager', () => {
return previous
})
- expect(await PushNotificationsManager.requestPushNotificationPermission()).toBeFalsy()
+ expect(await PushNotificationsManager.requestPushNotificationPermission(updateSettings)).toBeFalsy()
expect(mockRequestPermission).not.toHaveBeenCalled()
})
@@ -78,16 +75,17 @@ describe('PushNotificationsManager', () => {
mockBuildConfig(true, false)
mocked(requestNotifications).mockImplementationOnce(async () => ({ status: 'blocked', settings: {} }))
- expect(await PushNotificationsManager.requestPushNotificationPermission()).toBeFalsy()
- expect((await appSettings.loadSettings()).allowPushNotifications).toBe(false)
+ expect(await PushNotificationsManager.requestPushNotificationPermission(updateSettings)).toBeFalsy()
+ expect(updateSettings).toHaveBeenCalledTimes(1)
+ expect(updateSettings).toHaveBeenCalledWith({ allowPushNotifications: false })
})
it('should request permissions and return true if granted', async () => {
mockBuildConfig(true, false)
mocked(requestNotifications).mockImplementationOnce(async () => ({ status: 'granted', settings: {} }))
- expect(await PushNotificationsManager.requestPushNotificationPermission()).toBeTruthy()
- expect((await appSettings.loadSettings()).allowPushNotifications).toBe(true)
+ expect(await PushNotificationsManager.requestPushNotificationPermission(updateSettings)).toBeTruthy()
+ expect(updateSettings).not.toHaveBeenCalled()
})
})
@@ -143,7 +141,11 @@ describe('PushNotificationsManager', () => {
return previous
})
- await PushNotificationsManager.subscribeNews('augsburg', 'de')
+ await PushNotificationsManager.subscribeNews({
+ cityCode: 'augsburg',
+ languageCode: 'de',
+ allowPushNotifications: true,
+ })
expect(mockSubscribeToTopic).not.toHaveBeenCalled()
})
@@ -156,7 +158,28 @@ describe('PushNotificationsManager', () => {
return previous
})
- await PushNotificationsManager.subscribeNews('augsburg', 'de')
+ await PushNotificationsManager.subscribeNews({
+ cityCode: 'augsburg',
+ languageCode: 'de',
+ allowPushNotifications: true,
+ })
+ expect(mockSubscribeToTopic).not.toHaveBeenCalled()
+ })
+
+ it('should return and do nothing if it is disabled in settings', async () => {
+ mockBuildConfig(true, false)
+ const mockSubscribeToTopic = jest.fn()
+ mockedFirebaseMessaging.mockImplementation(() => {
+ const previous = previousFirebaseMessaging
+ previous.subscribeToTopic = mockSubscribeToTopic
+ return previous
+ })
+
+ await PushNotificationsManager.subscribeNews({
+ cityCode: 'augsburg',
+ languageCode: 'de',
+ allowPushNotifications: false,
+ })
expect(mockSubscribeToTopic).not.toHaveBeenCalled()
})
@@ -169,7 +192,30 @@ describe('PushNotificationsManager', () => {
return previous
})
- await PushNotificationsManager.subscribeNews('augsburg', 'de')
+ await PushNotificationsManager.subscribeNews({
+ cityCode: 'augsburg',
+ languageCode: 'de',
+ allowPushNotifications: true,
+ })
+ expect(mockSubscribeToTopic).toHaveBeenCalledWith('augsburg-de-news')
+ expect(mockSubscribeToTopic).toHaveBeenCalledTimes(1)
+ })
+
+ it('should call subscribeToTopic even if push notifications are disabled but skipSettingsCheck is true', async () => {
+ mockBuildConfig(true, false)
+ const mockSubscribeToTopic = jest.fn()
+ mockedFirebaseMessaging.mockImplementation(() => {
+ const previous = previousFirebaseMessaging
+ previous.subscribeToTopic = mockSubscribeToTopic
+ return previous
+ })
+
+ await PushNotificationsManager.subscribeNews({
+ cityCode: 'augsburg',
+ languageCode: 'de',
+ allowPushNotifications: false,
+ skipSettingsCheck: true,
+ })
expect(mockSubscribeToTopic).toHaveBeenCalledWith('augsburg-de-news')
expect(mockSubscribeToTopic).toHaveBeenCalledTimes(1)
})
diff --git a/native/src/utils/__tests__/createSettingsSections.spec.ts b/native/src/utils/__tests__/createSettingsSections.spec.ts
index 7061ffda6c..bd6eb9f7db 100644
--- a/native/src/utils/__tests__/createSettingsSections.spec.ts
+++ b/native/src/utils/__tests__/createSettingsSections.spec.ts
@@ -1,11 +1,11 @@
import { TFunction } from 'i18next'
import { mocked } from 'jest-mock'
-import { openSettings } from 'react-native-permissions'
import { SettingsRouteType } from 'shared'
+import { testingAppContext } from '../../testing/TestingAppContext'
import createNavigationScreenPropMock from '../../testing/createNavigationPropMock'
-import { defaultSettings, SettingsType } from '../AppSettings'
+import { SettingsType } from '../AppSettings'
import {
pushNotificationsEnabled,
requestPushNotificationPermission,
@@ -31,40 +31,23 @@ const mockUnsubscribeNews = mocked(unsubscribeNews)
const mockSubscribeNews = mocked(subscribeNews)
const mockedPushNotificationsEnabled = mocked(pushNotificationsEnabled)
-type changeSettingFnType = (settings: SettingsType) => Partial
-type changeActionFnType = void | ((newSettings: SettingsType) => Promise)
-
describe('createSettingsSections', () => {
- let changeSetting: changeSettingFnType
- let changeAction: void | ((newSettings: SettingsType) => Promise)
-
- beforeEach(() => {
- jest.clearAllMocks()
- changeSetting = settings => settings
- changeAction = async () => true
- })
-
- const setSetting = async (newChangeSetting: changeSettingFnType, newChangeAction: changeActionFnType) => {
- changeSetting = newChangeSetting
- changeAction = newChangeAction
- }
+ beforeEach(jest.clearAllMocks)
const t = ((key: string) => key) as TFunction
- const languageCode = 'de'
+ const updateSettings = jest.fn()
const cityCode = 'augsburg'
+ const appContext = { ...testingAppContext({}), cityCode, updateSettings }
const navigation = createNavigationScreenPropMock()
const showSnackbar = jest.fn()
- const createSettings = () =>
+ const createSettings = (params: Partial = {}) =>
createSettingsSections({
- t,
- languageCode,
- cityCode,
+ appContext: { ...appContext, settings: { ...appContext.settings, ...params } },
navigation,
- settings: defaultSettings,
- setSetting,
showSnackbar,
+ t,
})[0]!.data
describe('allowPushNotifications', () => {
@@ -75,89 +58,83 @@ describe('createSettingsSections', () => {
expect(sections.find(it => it.title === 'pushNewsTitle')).toBeFalsy()
})
- it('should set correct setting on press', () => {
+ it('should set correct setting on press', async () => {
mockedPushNotificationsEnabled.mockImplementation(() => true)
const sections = createSettings()
- const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')
- // Initialize changeSetting and changeAction
- pushNotificationSection!.onPress()
-
- const settings = defaultSettings
- settings.allowPushNotifications = false
- const changedSettings = changeSetting(settings)
- expect(pushNotificationSection!.getSettingValue!(settings)).toBeFalsy()
- expect(changedSettings.allowPushNotifications).toBeTruthy()
- settings.allowPushNotifications = true
- const changedSettings2 = changeSetting(settings)
- expect(pushNotificationSection!.getSettingValue!(settings)).toBeTruthy()
- expect(changedSettings2.allowPushNotifications).toBeFalsy()
+ const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')!
+ await pushNotificationSection!.onPress()
+ expect(updateSettings).toHaveBeenCalledTimes(1)
+ expect(updateSettings).toHaveBeenCalledWith({ allowPushNotifications: false })
+
+ expect(
+ pushNotificationSection.getSettingValue!({ ...appContext.settings, allowPushNotifications: false }),
+ ).toBeFalsy()
+ expect(
+ pushNotificationSection!.getSettingValue!({ ...appContext.settings, allowPushNotifications: true }),
+ ).toBeTruthy()
})
it('should unsubscribe from push notification topic', async () => {
mockedPushNotificationsEnabled.mockImplementation(() => true)
const sections = createSettings()
- const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')
- // Initialize changeSetting and changeAction
- pushNotificationSection?.onPress()
- const newSettings = defaultSettings
- newSettings.allowPushNotifications = false
-
- const assertedChangeAction = changeAction as (newSettings: SettingsType) => Promise
+ const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')!
expect(mockUnsubscribeNews).not.toHaveBeenCalled()
- const successful = await assertedChangeAction(newSettings)
- expect(successful).toBe(true)
+ await pushNotificationSection.onPress()
+
expect(mockUnsubscribeNews).toHaveBeenCalledTimes(1)
- expect(mockUnsubscribeNews).toHaveBeenCalledWith(cityCode, languageCode)
+ expect(mockUnsubscribeNews).toHaveBeenCalledWith(cityCode, appContext.languageCode)
expect(mockSubscribeNews).not.toHaveBeenCalled()
expect(mockRequestPushNotificationPermission).not.toHaveBeenCalled()
+
+ expect(updateSettings).toHaveBeenCalledTimes(1)
+ expect(updateSettings).toHaveBeenCalledWith({ allowPushNotifications: false })
})
it('should subscribe to push notification topic if permission is granted', async () => {
mockedPushNotificationsEnabled.mockImplementation(() => true)
- const sections = createSettings()
- const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')
- // Initialize changeSetting and changeAction
- pushNotificationSection?.onPress()
- const newSettings = defaultSettings
- newSettings.allowPushNotifications = true
-
- const assertedChangeAction = changeAction as (newSettings: SettingsType) => Promise
+ const sections = createSettings({ allowPushNotifications: false })
+ const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')!
expect(mockRequestPushNotificationPermission).not.toHaveBeenCalled()
expect(mockSubscribeNews).not.toHaveBeenCalled()
mockRequestPushNotificationPermission.mockImplementationOnce(async () => true)
- const successful = await assertedChangeAction(newSettings)
- expect(successful).toBe(true)
+ await pushNotificationSection.onPress()
+
expect(mockRequestPushNotificationPermission).toHaveBeenCalledTimes(1)
expect(mockSubscribeNews).toHaveBeenCalledTimes(1)
- expect(mockSubscribeNews).toHaveBeenCalledWith(cityCode, languageCode, true)
+ expect(mockSubscribeNews).toHaveBeenCalledWith({
+ cityCode,
+ languageCode: appContext.languageCode,
+ allowPushNotifications: true,
+ skipSettingsCheck: true,
+ })
expect(mockUnsubscribeNews).not.toHaveBeenCalled()
+
+ expect(updateSettings).toHaveBeenCalledTimes(1)
+ expect(updateSettings).toHaveBeenCalledWith({ allowPushNotifications: true })
})
it('should open settings and return false if permissions not granted', async () => {
mockedPushNotificationsEnabled.mockImplementation(() => true)
- const sections = createSettings()
- const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')
- // Initialize changeSetting and changeAction
- pushNotificationSection?.onPress()
- const newSettings = defaultSettings
- newSettings.allowPushNotifications = true
-
- const assertedChangeAction = changeAction as (newSettings: SettingsType) => Promise
+ const sections = createSettings({ allowPushNotifications: false })
+ const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')!
expect(mockRequestPushNotificationPermission).not.toHaveBeenCalled()
expect(mockSubscribeNews).not.toHaveBeenCalled()
mockRequestPushNotificationPermission.mockImplementationOnce(async () => false)
- const successful = await assertedChangeAction(newSettings)
- expect(successful).toBe(false)
+ await pushNotificationSection.onPress()
+
expect(mockRequestPushNotificationPermission).toHaveBeenCalledTimes(1)
- expect(openSettings).toHaveBeenCalledTimes(1)
expect(mockSubscribeNews).not.toHaveBeenCalled()
expect(mockUnsubscribeNews).not.toHaveBeenCalled()
+
+ expect(updateSettings).toHaveBeenCalledTimes(2)
+ expect(updateSettings).toHaveBeenLastCalledWith({ allowPushNotifications: false })
+ expect(showSnackbar).toHaveBeenCalledTimes(1)
})
})
})
diff --git a/native/src/utils/createSettingsSections.ts b/native/src/utils/createSettingsSections.ts
index c526f88ff4..b41d09441b 100644
--- a/native/src/utils/createSettingsSections.ts
+++ b/native/src/utils/createSettingsSections.ts
@@ -9,6 +9,7 @@ import { SnackbarType } from '../components/SnackbarContainer'
import NativeConstants from '../constants/NativeConstants'
import { NavigationProps } from '../constants/NavigationTypes'
import buildConfig from '../constants/buildConfig'
+import { CityAppContext } from '../hooks/useCityAppContext'
import { SettingsType } from './AppSettings'
import {
pushNotificationsEnabled,
@@ -19,15 +20,10 @@ import {
import openExternalUrl from './openExternalUrl'
import { initSentry } from './sentry'
-export type SetSettingFunctionType = (
- changeSetting: (settings: SettingsType) => Partial,
- changeAction?: (newSettings: SettingsType) => Promise,
-) => Promise
-
export type SettingsSectionType = {
title: string
description?: string
- onPress: () => void
+ onPress: () => Promise | void
bigTitle?: boolean
role?: Role
hasSwitch?: boolean
@@ -42,24 +38,18 @@ const volatileValues = {
const TRIGGER_VERSION_TAPS = 25
type CreateSettingsSectionsProps = {
- setSetting: SetSettingFunctionType
- t: TFunction
- languageCode: string
- cityCode: string | null | undefined
+ appContext: CityAppContext
navigation: NavigationProps
- settings: SettingsType
showSnackbar: (snackbar: SnackbarType) => void
+ t: TFunction<'error'>
}
const createSettingsSections = ({
- setSetting,
- t,
- languageCode,
- cityCode,
+ appContext: { settings, updateSettings, cityCode, languageCode },
navigation,
- settings,
showSnackbar,
-}: CreateSettingsSectionsProps): Readonly>> => [
+ t,
+}: CreateSettingsSectionsProps): Readonly[]> => [
{
title: null,
data: [
@@ -71,34 +61,34 @@ const createSettingsSections = ({
description: t('pushNewsDescription'),
hasSwitch: true,
getSettingValue: (settings: SettingsType) => settings.allowPushNotifications,
- onPress: () => {
- setSetting(
- settings => ({
- allowPushNotifications: !settings.allowPushNotifications,
- }),
- async (newSettings): Promise => {
- if (!cityCode) {
- // No city selected so nothing to do here (should not ever happen since settings are only available from city content routes)
- return true
- }
+ onPress: async () => {
+ const newAllowPushNotifications = !settings.allowPushNotifications
+ updateSettings({ allowPushNotifications: newAllowPushNotifications })
+ if (!newAllowPushNotifications) {
+ await unsubscribeNews(cityCode, languageCode)
+ return
+ }
- if (newSettings.allowPushNotifications) {
- const status = await requestPushNotificationPermission()
+ const status = await requestPushNotificationPermission(updateSettings)
- if (status) {
- await subscribeNews(cityCode, languageCode, true)
- } else {
- // If the user has rejected the permission once, it can only be changed in the system settings
- openSettings()
- // Not successful, reset displayed setting in app
- return false
- }
- } else {
- await unsubscribeNews(cityCode, languageCode)
- }
- return true
- },
- )
+ if (status) {
+ await subscribeNews({
+ cityCode,
+ languageCode,
+ allowPushNotifications: newAllowPushNotifications,
+ skipSettingsCheck: true,
+ })
+ } else {
+ updateSettings({ allowPushNotifications: false })
+ // If the user has rejected the permission once, it can only be changed in the system settings
+ showSnackbar({
+ text: 'permissionRequired',
+ positiveAction: {
+ label: t('layout:settings'),
+ onPress: openSettings,
+ },
+ })
+ }
},
},
]),
@@ -109,21 +99,16 @@ const createSettingsSections = ({
}),
hasSwitch: true,
getSettingValue: (settings: SettingsType) => settings.errorTracking,
- onPress: () => {
- setSetting(
- settings => ({
- errorTracking: !settings.errorTracking,
- }),
- async newSettings => {
- const client = Sentry.getCurrentHub().getClient()
- if (newSettings.errorTracking && !client) {
- initSentry()
- } else if (client) {
- client.getOptions().enabled = !!newSettings.errorTracking
- }
- return true
- },
- )
+ onPress: async () => {
+ const newErrorTracking = !settings.errorTracking
+ updateSettings({ errorTracking: newErrorTracking })
+
+ const client = Sentry.getClient()
+ if (newErrorTracking && !client) {
+ initSentry()
+ } else if (client) {
+ client.getOptions().enabled = newErrorTracking
+ }
},
},
{
@@ -138,19 +123,19 @@ const createSettingsSections = ({
title: t('about', {
appName: buildConfig().appName,
}),
- onPress: () => {
+ onPress: async () => {
const { aboutUrls } = buildConfig()
const aboutUrl = aboutUrls[languageCode] || aboutUrls.default
- openExternalUrl(aboutUrl, showSnackbar)
+ await openExternalUrl(aboutUrl, showSnackbar)
},
},
{
role: 'link',
title: t('privacyPolicy'),
- onPress: () => {
+ onPress: async () => {
const { privacyUrls } = buildConfig()
const privacyUrl = privacyUrls[languageCode] || privacyUrls.default
- openExternalUrl(privacyUrl, showSnackbar)
+ await openExternalUrl(privacyUrl, showSnackbar)
},
},
{
diff --git a/native/src/utils/helpers.ts b/native/src/utils/helpers.ts
index 12a42ca82c..463729647c 100644
--- a/native/src/utils/helpers.ts
+++ b/native/src/utils/helpers.ts
@@ -33,9 +33,9 @@ export const determineApiUrl = async (): Promise => {
*/
export const forEachTreeNode = (
root: T,
- resolveChildren: (arg0: T) => Array,
+ resolveChildren: (arg0: T) => T[],
depth: number,
- nodeAction: (arg0: T, arg1: Array | null | undefined) => void,
+ nodeAction: (arg0: T, arg1: T[] | null | undefined) => void,
): void => {
if (depth === 0) {
nodeAction(root, null)
diff --git a/native/src/utils/loadResourceCache.ts b/native/src/utils/loadResourceCache.ts
index 4726fb3225..fae1bc7496 100644
--- a/native/src/utils/loadResourceCache.ts
+++ b/native/src/utils/loadResourceCache.ts
@@ -16,7 +16,7 @@ export type FetchMapTargetType = {
urlHash: string
}
-export type FetchMapType = Record>
+export type FetchMapType = Record
const loadResourceCache = async ({
cityCode,
diff --git a/release-notes/unreleased/2805-Banner-to-install-app-from-store.yml b/release-notes/2024.10.3/2805-Banner-to-install-app-from-store.yml
similarity index 100%
rename from release-notes/unreleased/2805-Banner-to-install-app-from-store.yml
rename to release-notes/2024.10.3/2805-Banner-to-install-app-from-store.yml
diff --git a/release-notes/unreleased/2897-refresh-cities.yml b/release-notes/2024.10.3/2897-refresh-cities.yml
similarity index 100%
rename from release-notes/unreleased/2897-refresh-cities.yml
rename to release-notes/2024.10.3/2897-refresh-cities.yml
diff --git a/release-notes/unreleased/2350-pn-permission-settings.yml b/release-notes/unreleased/2350-pn-permission-settings.yml
new file mode 100644
index 0000000000..b1fabb1f09
--- /dev/null
+++ b/release-notes/unreleased/2350-pn-permission-settings.yml
@@ -0,0 +1,7 @@
+issue_key: 2350
+show_in_stores: true
+platforms:
+ - android
+ - ios
+en: Add explanation if push notification permissions are missing.
+de: Es wird nun eine Erklärung angezeigt, wenn die Berechtigungen für Push-Benachrichtigungen fehlen.
diff --git a/release-notes/unreleased/2924-security-error.yml b/release-notes/unreleased/2924-security-error.yml
new file mode 100644
index 0000000000..1c5f94a204
--- /dev/null
+++ b/release-notes/unreleased/2924-security-error.yml
@@ -0,0 +1,5 @@
+issue_key: 2924
+show_in_stores: false
+platforms:
+ - web
+en: Fix crashes if local storage is not available
diff --git a/shared/api/endpoints/__tests__/createChatMessagesEndpoint.spec.ts b/shared/api/endpoints/__tests__/createChatMessagesEndpoint.spec.ts
index 38e5f6fd5a..12aa0ec449 100644
--- a/shared/api/endpoints/__tests__/createChatMessagesEndpoint.spec.ts
+++ b/shared/api/endpoints/__tests__/createChatMessagesEndpoint.spec.ts
@@ -23,6 +23,7 @@ describe('createChatMessagesEndpoint', () => {
id: 2,
body: 'Informationen zu Ihrer Frage finden Sie auf folgenden Seiten:',
user_is_author: false,
+ automatic_answer: false,
},
],
}
@@ -32,6 +33,7 @@ describe('createChatMessagesEndpoint', () => {
id: 2,
body: 'Informationen zu Ihrer Frage finden Sie auf folgenden Seiten:',
userIsAuthor: false,
+ automaticAnswer: false,
}),
])
})
diff --git a/shared/api/endpoints/createCategoriesEndpoint.ts b/shared/api/endpoints/createCategoriesEndpoint.ts
index f9c1f83a2f..918a84ccf4 100644
--- a/shared/api/endpoints/createCategoriesEndpoint.ts
+++ b/shared/api/endpoints/createCategoriesEndpoint.ts
@@ -18,7 +18,7 @@ export default (baseUrl: string): Endpoint =>
.withParamsToUrlMapper(
(params: ParamsType): string => `${baseUrl}/api/${API_VERSION}/${params.city}/${params.language}/pages/`,
)
- .withMapper((json: Array, params: ParamsType): CategoriesMapModel => {
+ .withMapper((json: JsonCategoryType[], params: ParamsType): CategoriesMapModel => {
const basePath = `/${params.city}/${params.language}`
const categories = json.map(category => mapCategoryJson(category, basePath))
categories.push(
diff --git a/shared/api/endpoints/createCategoryChildrenEndpoint.ts b/shared/api/endpoints/createCategoryChildrenEndpoint.ts
index cb4f70d1d3..a520a89291 100644
--- a/shared/api/endpoints/createCategoryChildrenEndpoint.ts
+++ b/shared/api/endpoints/createCategoryChildrenEndpoint.ts
@@ -11,8 +11,8 @@ type ParamsType = {
cityContentPath: string
depth: number
}
-export default (baseUrl: string): Endpoint> =>
- new EndpointBuilder>(CATEGORY_CHILDREN_ENDPOINT_NAME)
+export default (baseUrl: string): Endpoint =>
+ new EndpointBuilder(CATEGORY_CHILDREN_ENDPOINT_NAME)
.withParamsToUrlMapper((params: ParamsType): string => {
const { city, language, cityContentPath, depth } = params
const basePath = `/${city}/${language}`
@@ -20,7 +20,7 @@ export default (baseUrl: string): Endpoint> =>
const query = basePath === cityContentPath ? '' : `&url=${params.cityContentPath}`
return `${baseUrl}/api/${API_VERSION}/${params.city}/${params.language}/children/?depth=${depth}${query}`
})
- .withMapper((json: Array, params: ParamsType): Array => {
+ .withMapper((json: JsonCategoryType[], params: ParamsType): CategoryModel[] => {
const basePath = `/${params.city}/${params.language}`
return json.map(category => mapCategoryJson(category, basePath))
})
diff --git a/shared/api/endpoints/createCategoryParentsEndpoint.ts b/shared/api/endpoints/createCategoryParentsEndpoint.ts
index 868ea60936..97ad4fd9cb 100644
--- a/shared/api/endpoints/createCategoryParentsEndpoint.ts
+++ b/shared/api/endpoints/createCategoryParentsEndpoint.ts
@@ -13,8 +13,8 @@ type ParamsType = {
language: string
cityContentPath: string
}
-export default (baseUrl: string): Endpoint> =>
- new EndpointBuilder>(CATEGORY_PARENTS_ENDPOINT_NAME)
+export default (baseUrl: string): Endpoint =>
+ new EndpointBuilder(CATEGORY_PARENTS_ENDPOINT_NAME)
.withParamsToUrlMapper((params: ParamsType): string => {
const { city, language, cityContentPath } = params
const basePath = `/${city}/${language}`
@@ -25,7 +25,7 @@ export default (baseUrl: string): Endpoint> =>
return `${baseUrl}/api/${API_VERSION}/${city}/${language}/parents/?url=${cityContentPath}`
})
- .withMapper((json: Array, params: ParamsType): Array => {
+ .withMapper((json: JsonCategoryType[], params: ParamsType): CategoryModel[] => {
const basePath = `/${params.city}/${params.language}`
const parents = json.map(category => mapCategoryJson(category, basePath))
parents.push(
diff --git a/shared/api/endpoints/createChatMessagesEndpoint.ts b/shared/api/endpoints/createChatMessagesEndpoint.ts
index 718c803e57..bbe6f037e8 100644
--- a/shared/api/endpoints/createChatMessagesEndpoint.ts
+++ b/shared/api/endpoints/createChatMessagesEndpoint.ts
@@ -11,21 +11,21 @@ type ParamsType = {
deviceId: string
}
-export default (baseUrl: string): Endpoint> =>
- new EndpointBuilder>(CHAT_ENDPOINT_NAME)
+export default (baseUrl: string): Endpoint =>
+ new EndpointBuilder(CHAT_ENDPOINT_NAME)
.withParamsToUrlMapper(
(params: ParamsType): string =>
`${baseUrl}/api/${API_VERSION}/${params.city}/${params.language}/${CHAT_ENDPOINT_NAME}/${params.deviceId}/`,
)
- .withMapper(
- (json: JsonChatMessagesType): Array =>
- json.messages.map(
- chatMessage =>
- new ChatMessageModel({
- id: chatMessage.id,
- body: chatMessage.body,
- userIsAuthor: chatMessage.user_is_author,
- }),
- ),
+ .withMapper((json: JsonChatMessagesType): ChatMessageModel[] =>
+ json.messages.map(
+ chatMessage =>
+ new ChatMessageModel({
+ id: chatMessage.id,
+ body: chatMessage.body,
+ userIsAuthor: chatMessage.user_is_author,
+ automaticAnswer: chatMessage.automatic_answer,
+ }),
+ ),
)
.build()
diff --git a/shared/api/endpoints/createEventsEndpoint.ts b/shared/api/endpoints/createEventsEndpoint.ts
index ce94b40e5d..11408ebcba 100644
--- a/shared/api/endpoints/createEventsEndpoint.ts
+++ b/shared/api/endpoints/createEventsEndpoint.ts
@@ -38,57 +38,56 @@ const eventCompare = (event1: EventModel, event2: EventModel): number => {
return event1.title.localeCompare(event2.title)
}
-export default (baseUrl: string): Endpoint> =>
- new EndpointBuilder>(EVENTS_ENDPOINT_NAME)
+export default (baseUrl: string): Endpoint =>
+ new EndpointBuilder(EVENTS_ENDPOINT_NAME)
.withParamsToUrlMapper(
(params: ParamsType): string =>
`${baseUrl}/api/${API_VERSION}/${params.city}/${params.language}/events/?combine_recurring=True`,
)
- .withMapper(
- (json: Array): Array =>
- json
- .map((event: JsonEventType): EventModel => {
- const eventData = event.event
- const allDay = eventData.all_day
- return new EventModel({
- path: event.path,
- title: event.title,
- content: event.content,
- thumbnail: event.thumbnail,
- date: new DateModel({
- startDate: DateTime.fromISO(eventData.start),
- endDate: DateTime.fromISO(eventData.end),
- allDay,
- recurrenceRule: event.recurrence_rule ? rrulestr(event.recurrence_rule) : null,
- }),
- location:
- event.location.id !== null
- ? new LocationModel({
- id: event.location.id,
- name: event.location.name,
- address: event.location.address,
- town: event.location.town,
- postcode: event.location.postcode,
- country: event.location.country,
- latitude: event.location.latitude,
- longitude: event.location.longitude,
- })
- : null,
- excerpt: decodeHTML(event.excerpt),
- availableLanguages: mapAvailableLanguages(event.available_languages),
- lastUpdate: DateTime.fromISO(event.last_updated),
- featuredImage: event.featured_image
- ? new FeaturedImageModel({
- description: event.featured_image.description,
- thumbnail: event.featured_image.thumbnail[0],
- medium: event.featured_image.medium[0],
- large: event.featured_image.large[0],
- full: event.featured_image.full[0],
+ .withMapper((json: JsonEventType[]): EventModel[] =>
+ json
+ .map((event: JsonEventType): EventModel => {
+ const eventData = event.event
+ const allDay = eventData.all_day
+ return new EventModel({
+ path: event.path,
+ title: event.title,
+ content: event.content,
+ thumbnail: event.thumbnail,
+ date: new DateModel({
+ startDate: DateTime.fromISO(eventData.start),
+ endDate: DateTime.fromISO(eventData.end),
+ allDay,
+ recurrenceRule: event.recurrence_rule ? rrulestr(event.recurrence_rule) : null,
+ }),
+ location:
+ event.location.id !== null
+ ? new LocationModel({
+ id: event.location.id,
+ name: event.location.name,
+ address: event.location.address,
+ town: event.location.town,
+ postcode: event.location.postcode,
+ country: event.location.country,
+ latitude: event.location.latitude,
+ longitude: event.location.longitude,
})
: null,
- poiPath: event.location_path,
- })
+ excerpt: decodeHTML(event.excerpt),
+ availableLanguages: mapAvailableLanguages(event.available_languages),
+ lastUpdate: DateTime.fromISO(event.last_updated),
+ featuredImage: event.featured_image
+ ? new FeaturedImageModel({
+ description: event.featured_image.description,
+ thumbnail: event.featured_image.thumbnail[0],
+ medium: event.featured_image.medium[0],
+ large: event.featured_image.large[0],
+ full: event.featured_image.full[0],
+ })
+ : null,
+ poiPath: event.location_path,
})
- .sort(eventCompare),
+ })
+ .sort(eventCompare),
)
.build()
diff --git a/shared/api/endpoints/createLocalNewsElementEndpoint.ts b/shared/api/endpoints/createLocalNewsElementEndpoint.ts
index 3aac69916a..e4b90520b6 100644
--- a/shared/api/endpoints/createLocalNewsElementEndpoint.ts
+++ b/shared/api/endpoints/createLocalNewsElementEndpoint.ts
@@ -22,7 +22,7 @@ export default (baseUrl: string): Endpoint =>
(params: ParamsType): string =>
`${baseUrl}/api/${API_VERSION}/${params.city}/${params.language}/fcm/?id=${params.id}`,
)
- .withMapper((localNews: Array, params: ParamsType): LocalNewsModel => {
+ .withMapper((localNews: JsonLocalNewsType[], params: ParamsType): LocalNewsModel => {
const localNewsModel = localNews[0]
if (!localNewsModel) {
diff --git a/shared/api/endpoints/createLocalNewsEndpoint.ts b/shared/api/endpoints/createLocalNewsEndpoint.ts
index dce0cadcdd..5667c20b04 100644
--- a/shared/api/endpoints/createLocalNewsEndpoint.ts
+++ b/shared/api/endpoints/createLocalNewsEndpoint.ts
@@ -12,23 +12,22 @@ type ParamsType = {
city: string
language: string
}
-export default (baseUrl: string): Endpoint> =>
- new EndpointBuilder>(LOCAL_NEWS_ENDPOINT_NAME)
+export default (baseUrl: string): Endpoint =>
+ new EndpointBuilder(LOCAL_NEWS_ENDPOINT_NAME)
.withParamsToUrlMapper(
(params: ParamsType): string =>
`${baseUrl}/api/${API_VERSION}/${params.city}/${params.language}/fcm/?channel=news`,
)
- .withMapper(
- (json: Array): Array =>
- json.map(
- (localNews: JsonLocalNewsType) =>
- new LocalNewsModel({
- id: localNews.id,
- timestamp: DateTime.fromISO(localNews.timestamp),
- title: localNews.title,
- content: localNews.message,
- availableLanguages: mapNewsAvailableLanguages(localNews.available_languages),
- }),
- ),
+ .withMapper((json: JsonLocalNewsType[]): LocalNewsModel[] =>
+ json.map(
+ (localNews: JsonLocalNewsType) =>
+ new LocalNewsModel({
+ id: localNews.id,
+ timestamp: DateTime.fromISO(localNews.timestamp),
+ title: localNews.title,
+ content: localNews.message,
+ availableLanguages: mapNewsAvailableLanguages(localNews.available_languages),
+ }),
+ ),
)
.build()
diff --git a/shared/api/endpoints/createPOIsEndpoint.ts b/shared/api/endpoints/createPOIsEndpoint.ts
index cafcf08376..d42fa5bd76 100644
--- a/shared/api/endpoints/createPOIsEndpoint.ts
+++ b/shared/api/endpoints/createPOIsEndpoint.ts
@@ -15,49 +15,48 @@ type ParamsType = {
city: string
language: string
}
-export default (baseUrl: string): Endpoint> =>
- new EndpointBuilder>(POIS_ENDPOINT_NAME)
+export default (baseUrl: string): Endpoint =>
+ new EndpointBuilder(POIS_ENDPOINT_NAME)
.withParamsToUrlMapper(
(params: ParamsType): string =>
`${baseUrl}/api/${API_VERSION}/${params.city}/${params.language}/locations/?on_map=1`,
)
- .withMapper(
- (json: Array): Array =>
- json.map(
- poi =>
- new PoiModel({
- path: poi.path,
- title: poi.title,
- content: poi.content,
- thumbnail: poi.thumbnail,
- availableLanguages: mapAvailableLanguages(poi.available_languages),
- excerpt: poi.excerpt,
- metaDescription: poi.meta_description ? poi.meta_description : null,
- website: poi.website,
- phoneNumber: poi.phone_number,
- email: poi.email,
- temporarilyClosed: poi.temporarily_closed,
- openingHours: poi.opening_hours?.map(openingHour => new OpeningHoursModel(openingHour)) ?? null,
- appointmentUrl: poi.appointment_url,
- category: new PoiCategoryModel({
- id: poi.category.id,
- name: poi.category.name,
- color: poi.category.color,
- iconName: poi.category.icon,
- icon: poi.category.icon_url,
- }),
- location: new LocationModel({
- id: poi.location.id,
- name: poi.location.name,
- address: poi.location.address,
- town: poi.location.town,
- postcode: poi.location.postcode,
- country: poi.location.country,
- latitude: poi.location.latitude,
- longitude: poi.location.longitude,
- }),
- lastUpdate: DateTime.fromISO(poi.last_updated),
+ .withMapper((json: JsonPoiType[]): PoiModel[] =>
+ json.map(
+ poi =>
+ new PoiModel({
+ path: poi.path,
+ title: poi.title,
+ content: poi.content,
+ thumbnail: poi.thumbnail,
+ availableLanguages: mapAvailableLanguages(poi.available_languages),
+ excerpt: poi.excerpt,
+ metaDescription: poi.meta_description ? poi.meta_description : null,
+ website: poi.website,
+ phoneNumber: poi.phone_number,
+ email: poi.email,
+ temporarilyClosed: poi.temporarily_closed,
+ openingHours: poi.opening_hours?.map(openingHour => new OpeningHoursModel(openingHour)) ?? null,
+ appointmentUrl: poi.appointment_url,
+ category: new PoiCategoryModel({
+ id: poi.category.id,
+ name: poi.category.name,
+ color: poi.category.color,
+ iconName: poi.category.icon,
+ icon: poi.category.icon_url,
}),
- ),
+ location: new LocationModel({
+ id: poi.location.id,
+ name: poi.location.name,
+ address: poi.location.address,
+ town: poi.location.town,
+ postcode: poi.location.postcode,
+ country: poi.location.country,
+ latitude: poi.location.latitude,
+ longitude: poi.location.longitude,
+ }),
+ lastUpdate: DateTime.fromISO(poi.last_updated),
+ }),
+ ),
)
.build()
diff --git a/shared/api/endpoints/createSendChatMessageEndpoint.ts b/shared/api/endpoints/createSendChatMessageEndpoint.ts
index 8ef8f56bf1..a5a669531a 100644
--- a/shared/api/endpoints/createSendChatMessageEndpoint.ts
+++ b/shared/api/endpoints/createSendChatMessageEndpoint.ts
@@ -30,6 +30,7 @@ export default (baseUrl: string): Endpoint =>
id: json.id,
body: json.body,
userIsAuthor: json.user_is_author,
+ automaticAnswer: json.automatic_answer,
}),
)
.build()
diff --git a/shared/api/endpoints/createSprungbrettJobsEndpoint.ts b/shared/api/endpoints/createSprungbrettJobsEndpoint.ts
index c016cf64a1..a53603b30e 100644
--- a/shared/api/endpoints/createSprungbrettJobsEndpoint.ts
+++ b/shared/api/endpoints/createSprungbrettJobsEndpoint.ts
@@ -5,21 +5,20 @@ import { JsonSprungbrettJobType } from '../types'
export const SPRUNGBRETT_JOBS_ENDPOINT_NAME = 'sprungbrettJobs'
-export default (baseUrl: string): Endpoint> =>
- new EndpointBuilder>(SPRUNGBRETT_JOBS_ENDPOINT_NAME)
+export default (baseUrl: string): Endpoint =>
+ new EndpointBuilder(SPRUNGBRETT_JOBS_ENDPOINT_NAME)
.withParamsToUrlMapper(() => baseUrl)
- .withMapper(
- (json: { results: Array }): Array =>
- json.results.map(
- (job, index) =>
- new SprungbrettJobModel({
- id: index,
- title: job.title,
- location: `${job.zip} ${job.city}`,
- url: job.url,
- isEmployment: job.employment === '1',
- isApprenticeship: job.apprenticeship === '1',
- }),
- ),
+ .withMapper((json: { results: JsonSprungbrettJobType[] }): SprungbrettJobModel[] =>
+ json.results.map(
+ (job, index) =>
+ new SprungbrettJobModel({
+ id: index,
+ title: job.title,
+ location: `${job.zip} ${job.city}`,
+ url: job.url,
+ isEmployment: job.employment === '1',
+ isApprenticeship: job.apprenticeship === '1',
+ }),
+ ),
)
.build()
diff --git a/shared/api/endpoints/createTunewsElementEndpoint.ts b/shared/api/endpoints/createTunewsElementEndpoint.ts
index de56282ade..3faaf0183c 100644
--- a/shared/api/endpoints/createTunewsElementEndpoint.ts
+++ b/shared/api/endpoints/createTunewsElementEndpoint.ts
@@ -16,7 +16,7 @@ type ParamsType = {
export default (baseUrl: string): Endpoint =>
new EndpointBuilder(TUNEWS_ELEMENT_ENDPOINT_NAME)
.withParamsToUrlMapper((params: ParamsType): string => `${baseUrl}/v1/news/${params.id}`)
- .withMapper((json: JsonTunewsType | Array, params: ParamsType): TunewsModel => {
+ .withMapper((json: JsonTunewsType | void[], params: ParamsType): TunewsModel => {
// The api is not good and returns an empty array if the tunews does not exist
if (Array.isArray(json)) {
throw new NotFoundError({
diff --git a/shared/api/endpoints/createTunewsEndpoint.ts b/shared/api/endpoints/createTunewsEndpoint.ts
index aed043ac00..057837e682 100644
--- a/shared/api/endpoints/createTunewsEndpoint.ts
+++ b/shared/api/endpoints/createTunewsEndpoint.ts
@@ -12,23 +12,22 @@ type ParamsType = {
page: number
count: number
}
-export default (baseUrl: string): Endpoint> =>
- new EndpointBuilder>(TUNEWS_ENDPOINT_NAME)
+export default (baseUrl: string): Endpoint =>
+ new EndpointBuilder(TUNEWS_ENDPOINT_NAME)
.withParamsToUrlMapper(
(params: ParamsType): string => `${baseUrl}/v1/news/${params.language}?page=${params.page}&count=${params.count}`,
)
- .withMapper(
- (json: Array): Array =>
- json.map(
- (tunews: JsonTunewsType) =>
- new TunewsModel({
- id: tunews.id,
- title: tunews.title,
- tags: tunews.tags,
- date: DateTime.fromJSDate(new Date(tunews.date)),
- content: parseHTML(tunews.content),
- eNewsNo: tunews.enewsno,
- }),
- ),
+ .withMapper((json: JsonTunewsType[]): TunewsModel[] =>
+ json.map(
+ (tunews: JsonTunewsType) =>
+ new TunewsModel({
+ id: tunews.id,
+ title: tunews.title,
+ tags: tunews.tags,
+ date: DateTime.fromJSDate(new Date(tunews.date)),
+ content: parseHTML(tunews.content),
+ eNewsNo: tunews.enewsno,
+ }),
+ ),
)
.build()
diff --git a/shared/api/endpoints/createTunewsLanguagesEndpoint.ts b/shared/api/endpoints/createTunewsLanguagesEndpoint.ts
index 758f0f9f58..fa70584ae0 100644
--- a/shared/api/endpoints/createTunewsLanguagesEndpoint.ts
+++ b/shared/api/endpoints/createTunewsLanguagesEndpoint.ts
@@ -5,10 +5,10 @@ import { JsonTunewsLanguageType } from '../types'
export const TUNEWS_LANGUAGES_ENDPOINT_NAME = 'tunewsLanguages'
-export default (baseUrl: string): Endpoint> =>
- new EndpointBuilder>(TUNEWS_LANGUAGES_ENDPOINT_NAME)
+export default (baseUrl: string): Endpoint =>
+ new EndpointBuilder(TUNEWS_LANGUAGES_ENDPOINT_NAME)
.withParamsToUrlMapper(() => `${baseUrl}/v1/news/languages`)
- .withMapper((json: Array) =>
+ .withMapper((json: JsonTunewsLanguageType[]) =>
json
.map((language: JsonTunewsLanguageType) => new LanguageModel(language.code, language.name))
.sort((lang1, lang2) => lang1.code.localeCompare(lang2.code)),
diff --git a/shared/api/endpoints/testing/CategoriesMapModelBuilder.ts b/shared/api/endpoints/testing/CategoriesMapModelBuilder.ts
index 65bedea698..843292edf3 100644
--- a/shared/api/endpoints/testing/CategoriesMapModelBuilder.ts
+++ b/shared/api/endpoints/testing/CategoriesMapModelBuilder.ts
@@ -27,7 +27,7 @@ class CategoriesMapModelBuilder {
_arity: number
_city: string
_language: string
- _categories: Array = []
+ _categories: CategoryModel[] = []
_resourceCache: Record = {}
_id = 0
diff --git a/shared/api/endpoints/testing/CityModelBuilder.ts b/shared/api/endpoints/testing/CityModelBuilder.ts
index 9fe64f60d9..7116494dc2 100644
--- a/shared/api/endpoints/testing/CityModelBuilder.ts
+++ b/shared/api/endpoints/testing/CityModelBuilder.ts
@@ -128,7 +128,7 @@ class CityModelBuilder {
}
}
- build(): Array {
+ build(): CityModel[] {
return cities.slice(0, this._citiesCount)
}
}
diff --git a/shared/api/endpoints/testing/EventModelBuilder.ts b/shared/api/endpoints/testing/EventModelBuilder.ts
index b3e1111f58..20b27fa70e 100644
--- a/shared/api/endpoints/testing/EventModelBuilder.ts
+++ b/shared/api/endpoints/testing/EventModelBuilder.ts
@@ -35,7 +35,7 @@ class EventModelBuilder {
return seedrandom(index + this._seed)() * max
}
- build(): Array {
+ build(): EventModel[] {
return this.buildAll().map(all => all.event)
}
diff --git a/shared/api/endpoints/testing/LanguageModelBuilder.ts b/shared/api/endpoints/testing/LanguageModelBuilder.ts
index 0f57ffb130..8ce22a0774 100644
--- a/shared/api/endpoints/testing/LanguageModelBuilder.ts
+++ b/shared/api/endpoints/testing/LanguageModelBuilder.ts
@@ -17,7 +17,7 @@ class LanguageModelBuilder {
}
}
- build(): Array {
+ build(): LanguageModel[] {
return languages.slice(0, this._languagesCount)
}
}
diff --git a/shared/api/endpoints/testing/NewsModelBuilder.ts b/shared/api/endpoints/testing/NewsModelBuilder.ts
index e9f0cef57c..8d3f5e9d68 100644
--- a/shared/api/endpoints/testing/NewsModelBuilder.ts
+++ b/shared/api/endpoints/testing/NewsModelBuilder.ts
@@ -15,17 +15,17 @@ class LocalNewsModelBuilder {
this._language = language
}
- build(): Array {
+ build(): LocalNewsModel[] {
return this.buildAll().map(all => all.newsItem)
}
/**
* Builds the requested amount of news. Two builds with an identical seed will yield equal news.
*/
- buildAll(): Array<{
+ buildAll(): {
path: string | null | undefined
newsItem: LocalNewsModel
- }> {
+ }[] {
return Array.from(
{
length: this._newsCount,
diff --git a/shared/api/endpoints/testing/PoiModelBuilder.ts b/shared/api/endpoints/testing/PoiModelBuilder.ts
index 54c8955bf1..dc9386bea0 100644
--- a/shared/api/endpoints/testing/PoiModelBuilder.ts
+++ b/shared/api/endpoints/testing/PoiModelBuilder.ts
@@ -141,7 +141,7 @@ class PoiModelBuilder {
}
}
- build(): Array {
+ build(): PoiModel[] {
return pois.slice(0, this._poisCount)
}
}
diff --git a/shared/api/models/CategoriesMapModel.ts b/shared/api/models/CategoriesMapModel.ts
index 1617a2d726..62c832c1fe 100644
--- a/shared/api/models/CategoriesMapModel.ts
+++ b/shared/api/models/CategoriesMapModel.ts
@@ -13,14 +13,14 @@ class CategoriesMapModel {
* whose parent attributes are first changed from id to path
* @param categories CategoryModel as array
*/
- constructor(categories: Array) {
+ constructor(categories: CategoryModel[]) {
this._categories = new Map(categories.map(category => [category.path, category]))
}
/**
* @return {CategoryModel[]} categories The categories as array
*/
- toArray(): Array {
+ toArray(): CategoryModel[] {
return Array.from(this._categories.values())
}
@@ -38,7 +38,7 @@ class CategoriesMapModel {
* @param category The category
* @return {CategoryModel[]} The children
*/
- getChildren(category: CategoryModel): Array {
+ getChildren(category: CategoryModel): CategoryModel[] {
return this.toArray()
.filter(_category => _category.parentPath === category.path)
.sort((category1, category2) => category1.order - category2.order)
@@ -49,8 +49,8 @@ class CategoriesMapModel {
* @param category The category
* @return {CategoryModel[]} The parents, with the immediate parent last
*/
- getAncestors(category: CategoryModel): Array {
- const parents: Array = []
+ getAncestors(category: CategoryModel): CategoryModel[] {
+ const parents: CategoryModel[] = []
let currentCategory = category
while (!currentCategory.isRoot()) {
diff --git a/shared/api/models/ChatMessageModel.ts b/shared/api/models/ChatMessageModel.ts
index cd367bf351..19dc54a059 100644
--- a/shared/api/models/ChatMessageModel.ts
+++ b/shared/api/models/ChatMessageModel.ts
@@ -2,12 +2,14 @@ class ChatMessageModel {
_id: number
_body: string
_userIsAuthor: boolean
+ _automaticAnswer: boolean
- constructor(params: { id: number; body: string; userIsAuthor: boolean }) {
- const { id, body, userIsAuthor } = params
+ constructor(params: { id: number; body: string; userIsAuthor: boolean; automaticAnswer: boolean }) {
+ const { id, body, userIsAuthor, automaticAnswer } = params
this._id = id
this._body = body
this._userIsAuthor = userIsAuthor
+ this._automaticAnswer = automaticAnswer
}
get id(): number {
@@ -21,6 +23,10 @@ class ChatMessageModel {
get userIsAuthor(): boolean {
return this._userIsAuthor
}
+
+ get isAutomaticAnswer(): boolean {
+ return this._automaticAnswer
+ }
}
export default ChatMessageModel
diff --git a/shared/api/models/CityModel.ts b/shared/api/models/CityModel.ts
index 694c6a0312..8c4174c823 100644
--- a/shared/api/models/CityModel.ts
+++ b/shared/api/models/CityModel.ts
@@ -121,7 +121,7 @@ class CityModel {
return this._aliases
}
- static findCityName(cities: ReadonlyArray, code: string): string {
+ static findCityName(cities: readonly CityModel[], code: string): string {
const city = cities.find(city => city.code === code)
return city ? city.name : code
}
diff --git a/shared/api/models/TunewsModel.ts b/shared/api/models/TunewsModel.ts
index bf3b4b1a1f..d42a5f1a0c 100644
--- a/shared/api/models/TunewsModel.ts
+++ b/shared/api/models/TunewsModel.ts
@@ -4,19 +4,12 @@ import { DateTime } from 'luxon'
class TunewsModel {
_id: number
_title: string
- _tags: Array
+ _tags: string[]
_date: DateTime
_content: string
_eNewsNo: string
- constructor(params: {
- id: number
- title: string
- date: DateTime
- tags: Array
- content: string
- eNewsNo: string
- }) {
+ constructor(params: { id: number; title: string; date: DateTime; tags: string[]; content: string; eNewsNo: string }) {
const { id, date, title, tags, content, eNewsNo } = params
this._id = id
this._title = decodeHTML(title)
@@ -38,7 +31,7 @@ class TunewsModel {
return this._date
}
- get tags(): Array {
+ get tags(): string[] {
return this._tags
}
diff --git a/shared/api/models/__tests__/EventModel.spec.ts b/shared/api/models/__tests__/EventModel.spec.ts
index 08a35035b9..f543199373 100644
--- a/shared/api/models/__tests__/EventModel.spec.ts
+++ b/shared/api/models/__tests__/EventModel.spec.ts
@@ -6,6 +6,8 @@ import DateModel from '../DateModel'
import EventModel from '../EventModel'
import LocationModel from '../LocationModel'
+jest.useFakeTimers({ now: new Date('2023-10-02T15:23:57.443+02:00') })
+
describe('EventModel', () => {
const params = {
path: '/augsburg/de/events/event0',
diff --git a/shared/api/types.ts b/shared/api/types.ts
index a6e5193f7d..362e1ea69f 100644
--- a/shared/api/types.ts
+++ b/shared/api/types.ts
@@ -77,6 +77,7 @@ export type JsonChatMessageType = {
id: number
body: string
user_is_author: boolean
+ automatic_answer: boolean
}
export type JsonChatMessagesType = {
messages: JsonChatMessageType[]
@@ -121,7 +122,7 @@ export type JsonEventType = {
export type JsonTunewsType = {
id: number
title: string
- tags: Array
+ tags: string[]
date: string
content: string
enewsno: string
diff --git a/shared/routes/InternalPathnameParser.ts b/shared/routes/InternalPathnameParser.ts
index ffe2dab6a4..84253e36b6 100644
--- a/shared/routes/InternalPathnameParser.ts
+++ b/shared/routes/InternalPathnameParser.ts
@@ -20,7 +20,7 @@ import { parseQueryParams } from './query'
const ENTITY_ID_INDEX = 3
class InternalPathnameParser {
- _parts: Array
+ _parts: string[]
_length: number
_fallbackLanguageCode: string
_fixedCity: string | null
diff --git a/shared/routes/pathname.ts b/shared/routes/pathname.ts
index ffca183e1d..0c67f0668c 100644
--- a/shared/routes/pathname.ts
+++ b/shared/routes/pathname.ts
@@ -20,7 +20,7 @@ type CityContentRouteUrlType = {
path?: string | null | undefined
}
-const constructPathname = (parts: Array) => {
+const constructPathname = (parts: (string | null | undefined)[]) => {
const pathname = parts
.filter(Boolean)
.map(part => part?.toLowerCase())
diff --git a/translations/translations.json b/translations/translations.json
index 88a7d5f045..0d5b90a26f 100644
--- a/translations/translations.json
+++ b/translations/translations.json
@@ -2338,8 +2338,10 @@
"notSupportedByDevice": "Diese Funktion wird auf diesem Gerät nicht unterstützt.",
"languageSwitchFailedTitle": "Leider ist der Sprachwechsel fehlgeschlagen.",
"languageSwitchFailedMessage": "Die ausgewählte Sprache ist nicht offline verfügbar. Eine aktive Internetverbinding ist notwendig.",
+ "permissionRequired": "Berechtigung erforderlich",
"noCalendarPermission": "Der Zugriff auf den Kalender ist nicht erlaubt.",
- "noCalendarFound": "Es wurde kein geeigneter Kalender gefunden."
+ "noCalendarFound": "Es wurde kein geeigneter Kalender gefunden.",
+ "settingsError": "Fehler beim Anwenden der Einstellungen"
},
"am": {
"notFound": {
@@ -2618,8 +2620,10 @@
"notSupportedByDevice": "This function is not supported on this device.",
"languageSwitchFailedTitle": "Unfortunately, changing the language failed.",
"languageSwitchFailedMessage": "The selected language is not available if you are offline. An active internet connection is required.",
+ "permissionRequired": "Permission required",
"noCalendarPermission": "Access to the calendar is not permitted.",
- "noCalendarFound": "No suitable calendar was found."
+ "noCalendarFound": "No suitable calendar was found.",
+ "settingsError": "Failed to apply settings"
},
"es": {
"notFound": {
@@ -3617,7 +3621,6 @@
"chooseCalendar": "Kalender-Auswahl",
"added": "Hinzugefügt",
"goToCalendar": "Zum Kalender",
- "settings": "Einstellungen",
"recurring": "wiederholend",
"today": "heute",
"todayRecurring": "heute, wiederholend",
@@ -3647,7 +3650,6 @@
"chooseCalendar": "የቀን መቁጠሪያን ይምረጡ",
"added": "ታክሏል",
"goToCalendar": "ወደ ቀን መቁጠሪያ ይሂዱ",
- "settings": "ቅንብሮች",
"recurring": "ተደጋጋሚ",
"today": "ዛሬ",
"todayRecurring": "ዛሬ፣ ተደጋጋሚ",
@@ -3677,7 +3679,6 @@
"chooseCalendar": "اختر تقويمًا",
"added": "تمت الإضافة",
"goToCalendar": "إلى التقويم",
- "settings": "أوضاع الضبط",
"recurring": "مكرر",
"today": "اليوم",
"todayRecurring": "اليوم، مكرر",
@@ -3707,7 +3708,6 @@
"chooseCalendar": "Избери календар",
"added": "Добавен",
"goToCalendar": "Към календара",
- "settings": "Настройки",
"recurring": "повтарящ се",
"today": "днес",
"todayRecurring": "днес, повтарящ се",
@@ -3737,7 +3737,6 @@
"chooseCalendar": "ساڵنامەیەک هەڵبژێرە",
"added": "زیادکرا",
"goToCalendar": "دەسپێگەیشتن بە ساڵنامە",
- "settings": "ڕێکخستنەکان",
"recurring": "دووپاتە",
"today": "ئەمڕۆ",
"todayRecurring": "ئەمڕۆ، دووپاتە",
@@ -3767,7 +3766,6 @@
"chooseCalendar": "Výběr kalendáře",
"added": "Přidáno",
"goToCalendar": "Do kalendáře",
- "settings": "Nastavení",
"recurring": "opakování",
"today": "dnes",
"todayRecurring": "dnes, opakování",
@@ -3797,7 +3795,6 @@
"chooseCalendar": "Valg af kalender",
"added": "Tilføjet",
"goToCalendar": "Til kalenderen",
- "settings": "Indstillinger",
"recurring": "gentagende",
"today": "i dag",
"todayRecurring": "i dag og gentagende",
@@ -3827,7 +3824,6 @@
"chooseCalendar": "Επιλέξτε ένα ημερολόγιο",
"added": "Προστέθηκε",
"goToCalendar": "Μετάβαση στο ημερολόγιο",
- "settings": "Ρυθμίσεις",
"recurring": "επαναλαμβάνεται",
"today": "σήμερα",
"todayRecurring": "σήμερα, επαναλαμβάνεται",
@@ -3857,7 +3853,6 @@
"chooseCalendar": "Calendar choice",
"added": "Added",
"goToCalendar": "Go to calendar",
- "settings": "Settings",
"recurring": "recurring",
"today": "today",
"todayRecurring": "today, recurring",
@@ -3887,7 +3882,6 @@
"chooseCalendar": "Elije un calendario",
"added": "Se añadió",
"goToCalendar": "al calendario",
- "settings": "Ajustes",
"recurring": "repetitivo",
"today": "hoy",
"todayRecurring": "hoy, repetitivo",
@@ -3917,7 +3911,6 @@
"chooseCalendar": "Valitse kalenteri",
"added": "Lisätty",
"goToCalendar": "Kalenteriin",
- "settings": "Asetukset",
"recurring": "toistuva",
"today": "tänään",
"todayRecurring": "tänään, toistuva",
@@ -3947,7 +3940,6 @@
"chooseCalendar": "Choisis un calendrier",
"added": "Ajouté",
"goToCalendar": "Accéder au calendrier",
- "settings": "Réglages",
"recurring": "répétitif",
"today": "aujourd’hui",
"todayRecurring": "aujourd’hui, répétitif",
@@ -3977,7 +3969,6 @@
"chooseCalendar": "Izaberi kalendar",
"added": "Dodano",
"goToCalendar": "Na kalendar",
- "settings": "Postavke",
"recurring": "ponavljajući",
"today": "danas",
"todayRecurring": "danas, ponavljajući",
@@ -4007,7 +3998,6 @@
"chooseCalendar": "Válasszon ki egy naptárat",
"added": "Hozzáadva",
"goToCalendar": "A naptárhoz",
- "settings": "Beállítások",
"recurring": "ismétlődő",
"today": "ma",
"todayRecurring": "ma, ismétlődve",
@@ -4037,7 +4027,6 @@
"chooseCalendar": "Seleziona un calendario",
"added": "Aggiunto",
"goToCalendar": "Al calendario",
- "settings": "Impostazioni",
"recurring": "ricorrente",
"today": "oggi",
"todayRecurring": "oggi, ricorrente",
@@ -4067,7 +4056,6 @@
"chooseCalendar": "აირჩიე კალენდარი",
"added": "დამატებულია",
"goToCalendar": "კალენდარზე გადასვლა",
- "settings": "პარამეტრები",
"recurring": "განმეორებით",
"today": "დღეს",
"todayRecurring": "დღეს, განმეორებით",
@@ -4097,7 +4085,6 @@
"chooseCalendar": "Rojhejmêrekê bibijêre",
"added": "Zêdekirin",
"goToCalendar": "Li rojhejmêrê",
- "settings": "Eyarkirin",
"recurring": "Dubarebûyî",
"today": "Îro",
"todayRecurring": "Îro, dubarebûyî",
@@ -4127,7 +4114,6 @@
"chooseCalendar": "Избери календар",
"added": "Додадено",
"goToCalendar": "Кон календарот",
- "settings": "Поставки",
"recurring": "повторувачки",
"today": "утре",
"todayRecurring": "утре, повторувачки",
@@ -4157,7 +4143,6 @@
"chooseCalendar": "Selecteer een kalender",
"added": "Toegevoegd",
"goToCalendar": "Naar de kalender",
- "settings": "Instellingen",
"recurring": "herhalend",
"today": "vandaag",
"todayRecurring": "vandaag, herhalend",
@@ -4187,7 +4172,6 @@
"chooseCalendar": "Filannoo Kaalanderii",
"added": "Dabalameera",
"goToCalendar": "Gara kaalanderii",
- "settings": "Saajoo",
"recurring": "Irra deddeebii",
"today": "Hara",
"todayRecurring": "Irra deebii haraa",
@@ -4217,7 +4201,6 @@
"chooseCalendar": "یک تقویم را انتخاب کنید",
"added": "افزوده شد",
"goToCalendar": "دسترسی به تقویم",
- "settings": "تنظیمات",
"recurring": "تکراری",
"today": "امروز",
"todayRecurring": "امروز، تکراری",
@@ -4247,7 +4230,6 @@
"chooseCalendar": "Wybierz kalendarz",
"added": "Dodano",
"goToCalendar": "Do kalendarza",
- "settings": "Ustawienia",
"recurring": "powtarzający się",
"today": "dzisiaj",
"todayRecurring": "dzisiaj, powtarzający się",
@@ -4277,7 +4259,6 @@
"chooseCalendar": "یک تقویم را انتخاب کنید",
"added": "اضافه شد",
"goToCalendar": "دسترسی به تقویم",
- "settings": "تنظیمات",
"recurring": "تکراری",
"today": "امروز",
"todayRecurring": "امروز، تکراری",
@@ -4307,7 +4288,6 @@
"chooseCalendar": "يوه جنتري غوره کړئ",
"added": "اضافه شو",
"goToCalendar": "جنتري ته لاسرسی",
- "settings": "تنظيمات",
"recurring": "تکراري",
"today": "نن ورځ",
"todayRecurring": "نن ورځ، تکراري",
@@ -4337,7 +4317,6 @@
"chooseCalendar": "Selecione um calendário",
"added": "Adicionado",
"goToCalendar": "Para o calendário",
- "settings": "Definições",
"recurring": "repetitivo",
"today": "hoje",
"todayRecurring": "hoje, repetitivo",
@@ -4367,7 +4346,6 @@
"chooseCalendar": "Selectați un calendar",
"added": "Adăugat",
"goToCalendar": "La calendar",
- "settings": "Setări",
"recurring": "repetarea",
"today": "astăzi",
"todayRecurring": "astăzi, repetând",
@@ -4397,7 +4375,6 @@
"chooseCalendar": "Выбери календарь",
"added": "Добавлено",
"goToCalendar": "К календарю",
- "settings": "Настройки",
"recurring": "периодически",
"today": "сегодня",
"todayRecurring": "сегодня, периодически",
@@ -4427,7 +4404,6 @@
"chooseCalendar": "Výber kalendára",
"added": "Pridané",
"goToCalendar": "Ku kalendáru",
- "settings": "Nastavenia",
"recurring": "opakujúce sa",
"today": "dnes",
"todayRecurring": "dnes, opakujúce sa",
@@ -4457,7 +4433,6 @@
"chooseCalendar": "Dooro jadwalka",
"added": "Ku daray",
"goToCalendar": "Jadwalka",
- "settings": "Dejinta",
"recurring": "Soo noqnoqda",
"today": "Maanta",
"todayRecurring": "Maanta, soo noqnoqonaysa",
@@ -4487,7 +4462,6 @@
"chooseCalendar": "Zgjidh një kalendar",
"added": "U shtua",
"goToCalendar": "Shko te kalendari",
- "settings": "Cilësimet",
"recurring": "periodik",
"today": "sot",
"todayRecurring": "sot, periodik",
@@ -4517,7 +4491,6 @@
"chooseCalendar": "Изабери календар",
"added": "Додато",
"goToCalendar": "На календар",
- "settings": "Подешавања",
"recurring": "понављајући",
"today": "данас",
"todayRecurring": "данас, понављајући",
@@ -4547,7 +4520,6 @@
"chooseCalendar": "Izaberi kalendar",
"added": "Dodato",
"goToCalendar": "Na kalendar",
- "settings": "Podešavanja",
"recurring": "ponavljajući",
"today": "danas",
"todayRecurring": "danas, ponavljajući",
@@ -4577,7 +4549,6 @@
"chooseCalendar": "ዓውደ ኣዋርሕ ምረጽ",
"added": "ተወሰኸ",
"goToCalendar": "ናብ ዓውደ ኣዋርሕ",
- "settings": "ቅጥዕታት",
"recurring": "ተደጋጋሚ",
"today": "ሎምዓንቲ",
"todayRecurring": "ሎሚ፣ ተደጋጋሚ",
@@ -4607,7 +4578,6 @@
"chooseCalendar": "Bir takvim seç",
"added": "Eklendi",
"goToCalendar": "Takvime git",
- "settings": "Ayarlar",
"recurring": "tekrarlayan",
"today": "bugün",
"todayRecurring": "bugün, tekrarlayan",
@@ -4637,7 +4607,6 @@
"chooseCalendar": "Виберіть календар",
"added": "Додано",
"goToCalendar": "До календаря",
- "settings": "Налаштування",
"recurring": "періодичні",
"today": "сьогодні",
"todayRecurring": "сьогодні, періодичні",
@@ -4667,7 +4636,6 @@
"chooseCalendar": "ایک کیلنڈر منتخب کریں",
"added": "شامل کیا گیا",
"goToCalendar": "کیلنڈر پر جائیں",
- "settings": "ترتیبات",
"recurring": "مکرر",
"today": "آج",
"todayRecurring": "آج، مکرر",
@@ -4697,7 +4665,6 @@
"chooseCalendar": "选择日历",
"added": "添加到日历",
"goToCalendar": "转到日历",
- "settings": "设置",
"recurring": "重复",
"today": "今天",
"todayRecurring": "今天,重复",
diff --git a/version.json b/version.json
index a308dcc617..d0a6272a38 100644
--- a/version.json
+++ b/version.json
@@ -1 +1 @@
-{"versionName":"2024.10.2","versionCode":100042399}
\ No newline at end of file
+{"versionName":"2024.11.1","versionCode":100042405}
\ No newline at end of file
diff --git a/web/src/assets/index.ts b/web/src/assets/index.ts
index 406ccc500c..e45ff89a45 100644
--- a/web/src/assets/index.ts
+++ b/web/src/assets/index.ts
@@ -12,6 +12,8 @@ import CalendarTodayRecurringIcon from '../../../assets/icons/calendar-today-rec
import CalendarTodayIcon from '../../../assets/icons/calendar-today.svg'
import CalendarIcon from '../../../assets/icons/calendar.svg'
import CategoriesIcon from '../../../assets/icons/categories.svg'
+import ChatBot from '../../../assets/icons/chat-bot.svg'
+import ChatPerson from '../../../assets/icons/chat-person.svg'
import ChatIcon from '../../../assets/icons/chat.svg'
import ClockIcon from '../../../assets/icons/clock.svg'
import CloseIcon from '../../../assets/icons/close.svg'
@@ -66,6 +68,8 @@ export {
CalendarTodayIcon,
CalendarTodayRecurringIcon,
CategoriesIcon,
+ ChatPerson,
+ ChatBot,
ChatIcon,
ClockIcon,
CloseIcon,
diff --git a/web/src/components/Breadcrumbs.tsx b/web/src/components/Breadcrumbs.tsx
index 33781a3aca..0b0d42f48e 100644
--- a/web/src/components/Breadcrumbs.tsx
+++ b/web/src/components/Breadcrumbs.tsx
@@ -48,7 +48,7 @@ const StyledLink = styled(Link)`
`
type BreadcrumbsProps = {
- ancestorBreadcrumbs: Array
+ ancestorBreadcrumbs: BreadcrumbModel[]
currentBreadcrumb: BreadcrumbModel
}
diff --git a/web/src/components/ChatMessage.tsx b/web/src/components/ChatMessage.tsx
index 06e328a322..4b494c6d01 100644
--- a/web/src/components/ChatMessage.tsx
+++ b/web/src/components/ChatMessage.tsx
@@ -1,3 +1,4 @@
+import { TFunction } from 'i18next'
import React, { ReactElement } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
@@ -5,7 +6,7 @@ import styled from 'styled-components'
import ChatMessageModel from 'shared/api/models/ChatMessageModel'
-import buildConfig from '../constants/buildConfig'
+import { ChatBot, ChatPerson } from '../assets'
import RemoteContent from './RemoteContent'
import Icon from './base/Icon'
@@ -49,17 +50,22 @@ const Circle = styled.div`
type ChatMessageProps = { message: ChatMessageModel; showIcon: boolean }
+const getIcon = (userIsAuthor: boolean, isAutomaticAnswer: boolean, t: TFunction<'chat'>): ReactElement => {
+ if (userIsAuthor) {
+ return {t('user')}
+ }
+ const icon = isAutomaticAnswer ? ChatBot : ChatPerson
+ return
+}
+
const ChatMessage = ({ message, showIcon }: ChatMessageProps): ReactElement => {
- // TODO 2799 Check if Remote content is really needed here or how external links will be delivered via api
- const { icons } = buildConfig()
const navigate = useNavigate()
const { t } = useTranslation('chat')
- const { body, userIsAuthor } = message
+ const { body, userIsAuthor, isAutomaticAnswer } = message
+
return (
-
- {userIsAuthor ?