diff --git a/assets/icons/uiIcons/favorite.svg b/assets/icons/uiIcons/favorite.svg
new file mode 100644
index 0000000000..2e4f176af0
--- /dev/null
+++ b/assets/icons/uiIcons/favorite.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/app/modules.ts b/src/app/modules.ts
index 366617b8de..d6ae464652 100755
--- a/src/app/modules.ts
+++ b/src/app/modules.ts
@@ -9,7 +9,7 @@ import {
loadModules,
ModuleArray,
NavigableModule,
- NavigableModuleArray
+ NavigableModuleArray,
} from '~/framework/util/moduleTool';
// We first imports all modules and their code hierarchy. Registrations are executed,
@@ -26,7 +26,7 @@ export default () => {
require('~/framework/modules/timeline'),
require('~/framework/modules/audience').default,
require('~/framework/modules/explorer').default,
-
+ require('~/framework/modules/myapps'),
// Included modules from override
...(IncludedModules || []),
diff --git a/src/app/store.tsx b/src/app/store.tsx
index dff2d33ed5..ea838cfb43 100755
--- a/src/app/store.tsx
+++ b/src/app/store.tsx
@@ -13,8 +13,8 @@
import * as React from 'react';
import { connect } from 'react-redux';
-import { Action, applyMiddleware, combineReducers, compose, createStore, Reducer, Store } from 'redux';
-import { thunk } from 'redux-thunk';
+import { Action, applyMiddleware, combineReducers, compose, createStore, Reducer, Store, UnknownAction } from 'redux';
+import { thunk, ThunkDispatch } from 'redux-thunk';
declare var window: any;
@@ -64,6 +64,7 @@ export function createMainStore() {
}
export type IGlobalState = any; // Todo: Make any TS logic that can get composed state from module definitions IF POSSIBLE
+export type AppDispatch = ThunkDispatch;
/** === Store getter === */
diff --git a/src/framework/components/picture/svg/index.tsx b/src/framework/components/picture/svg/index.tsx
index ffa871e1db..ad051a51aa 100755
--- a/src/framework/components/picture/svg/index.tsx
+++ b/src/framework/components/picture/svg/index.tsx
@@ -188,6 +188,7 @@ const imports = {
'ui-externalLink': async () => import('ASSETS/icons/uiIcons/externalLink.svg'),
'ui-eye': async () => import('ASSETS/icons/uiIcons/eye.svg'),
'ui-eyeSlash': async () => import('ASSETS/icons/uiIcons/eyeSlash.svg'),
+ 'ui-favorite': async () => import('ASSETS/icons/uiIcons/favorite.svg'),
'ui-filter': async () => import('ASSETS/icons/uiIcons/filter.svg'),
'ui-flag': async () => import('ASSETS/icons/uiIcons/flag.svg'),
'ui-folder': async () => import('ASSETS/icons/uiIcons/folder.svg'),
diff --git a/src/framework/modules/myAppMenu/components/my-apps-card/component.tsx b/src/framework/modules/myAppMenu/components/my-apps-card/component.tsx
new file mode 100644
index 0000000000..ebf3fd0356
--- /dev/null
+++ b/src/framework/modules/myAppMenu/components/my-apps-card/component.tsx
@@ -0,0 +1,84 @@
+import React, { useCallback, useMemo } from 'react';
+import { Pressable, TouchableOpacity, View } from 'react-native';
+
+import { MyAppsCardProps } from './types';
+import { useStyles } from './useStyles';
+
+import { UI_SIZES } from '~/framework/components/constants';
+import { Svg } from '~/framework/components/picture';
+import { BodyText } from '~/framework/components/text';
+import { Image } from '~/framework/util/media-deprecated';
+
+const HTTP_REGEX: RegExp = /^https?:\/\//;
+
+export const MyAppsCard = ({ app, onLongPress, onPress }: MyAppsCardProps) => {
+ const styles = useStyles(app);
+ const isImageDistant = (icon: string): boolean => HTTP_REGEX.test(icon) || icon.startsWith('/workspace/');
+ const icon = app.icon;
+
+ const svgIconName = useMemo(() => {
+ if (!icon) return null;
+ if (isImageDistant(icon)) return null;
+
+ return icon.replace(/-large$/, ''); //might be replaced in the future
+ }, [icon]);
+
+ const isImageIcon = !!icon && !svgIconName;
+
+ const isWebApp = useMemo(() => {
+ if (app.type === 'connector') return true;
+ if (app.target === '_blank') return true;
+ if (!app.address) return false;
+
+ return HTTP_REGEX.test(app.address) || app.address.includes('#') || app.address.startsWith('/pages#');
+ }, [app]);
+
+ console.debug('APP_INFOS', {
+ DN: app.displayName,
+ iconNormalized: svgIconName,
+ isImageIcon,
+ isWebApp,
+ ...app,
+ });
+
+ const renderIcon = useCallback(() => {
+ if (!icon) return null;
+
+ if (svgIconName) {
+ return ;
+ }
+
+ return ;
+ }, [icon, svgIconName, styles.image]);
+
+ const renderFavoriteBadge = useCallback(() => {
+ if (!app.isFavorite) return null;
+
+ return (
+
+
+
+ );
+ }, [app.isFavorite, styles.favoriteIcon]);
+
+ return (
+
+
+ {renderFavoriteBadge()}
+ {renderIcon()}
+
+
+
+
+ {app.displayName}
+
+
+ {isWebApp && (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/framework/modules/myAppMenu/components/my-apps-card/index.ts b/src/framework/modules/myAppMenu/components/my-apps-card/index.ts
new file mode 100644
index 0000000000..f5bfd29d48
--- /dev/null
+++ b/src/framework/modules/myAppMenu/components/my-apps-card/index.ts
@@ -0,0 +1,3 @@
+import { MyAppsCard } from './component';
+
+export default MyAppsCard;
diff --git a/src/framework/modules/myAppMenu/components/my-apps-card/types.ts b/src/framework/modules/myAppMenu/components/my-apps-card/types.ts
new file mode 100644
index 0000000000..8d518233c1
--- /dev/null
+++ b/src/framework/modules/myAppMenu/components/my-apps-card/types.ts
@@ -0,0 +1,7 @@
+import { AppsInfoAggregated } from '~/framework/modules/myapps/types';
+
+export type MyAppsCardProps = {
+ app: AppsInfoAggregated;
+ onPress?: () => void;
+ onLongPress?: () => void;
+};
diff --git a/src/framework/modules/myAppMenu/components/my-apps-card/useStyles.ts b/src/framework/modules/myAppMenu/components/my-apps-card/useStyles.ts
new file mode 100644
index 0000000000..dc22e65a9a
--- /dev/null
+++ b/src/framework/modules/myAppMenu/components/my-apps-card/useStyles.ts
@@ -0,0 +1,60 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+
+import theme from '~/app/theme';
+import { getScaleWidth, UI_SIZES } from '~/framework/components/constants';
+import { AppsInfoAggregated } from '~/framework/modules/myapps/types';
+
+export const useStyles = (app: AppsInfoAggregated) => {
+ const appColor = app.color;
+ const themeMainColor = theme.palette.complementary;
+ const backgroundColor = appColor && themeMainColor[appColor] ? themeMainColor[appColor].regular : undefined;
+
+ const styles = React.useMemo(
+ () =>
+ StyleSheet.create({
+ card: {
+ alignItems: 'center',
+ backgroundColor,
+ borderColor: theme.palette.grey.cloudy,
+ borderRadius: UI_SIZES.radius.newCard,
+ borderWidth: getScaleWidth(0.85),
+ height: getScaleWidth(120),
+ justifyContent: 'center',
+ // overflow: 'visible',
+ position: 'relative',
+ width: getScaleWidth(120),
+ },
+ favoriteIcon: {
+ left: -getScaleWidth(20),
+ padding: UI_SIZES.spacing.tiny,
+ position: 'absolute',
+ top: -UI_SIZES.spacing.medium,
+ zIndex: UI_SIZES.spacing.tinyExtra,
+ },
+ image: {
+ borderRadius: UI_SIZES.radius.newCard,
+ height: '100%',
+ objectFit: 'fill',
+ width: '100%',
+ },
+ title: {
+ textAlign: 'center',
+ },
+ titleRow: {
+ alignContent: 'space-between',
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: UI_SIZES.spacing.tiny,
+ marginTop: UI_SIZES.spacing.small,
+ padding: 0,
+ },
+ wrapper: {
+ alignItems: 'center',
+ },
+ }),
+ [backgroundColor],
+ );
+
+ return styles;
+};
diff --git a/src/framework/modules/myAppMenu/screens/MyAppsHomeScreen.tsx b/src/framework/modules/myAppMenu/screens/MyAppsHomeScreen.tsx
index ba4e5dbf86..73c5f3ea34 100755
--- a/src/framework/modules/myAppMenu/screens/MyAppsHomeScreen.tsx
+++ b/src/framework/modules/myAppMenu/screens/MyAppsHomeScreen.tsx
@@ -3,7 +3,10 @@ import { StyleSheet, View } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import MyAppsCard from '../components/my-apps-card';
+
import { I18n } from '~/app/i18n';
+import { getStore } from '~/app/store';
import SecondaryButton from '~/framework/components/buttons/secondary';
import { TouchableSelectorPictureCard } from '~/framework/components/card/pictureCard';
import { UI_SIZES } from '~/framework/components/constants';
@@ -14,6 +17,8 @@ import ScrollView from '~/framework/components/scrollView';
import { HeadingSText } from '~/framework/components/text';
import OtherModuleElement from '~/framework/modules/myAppMenu/components/other-module';
import { IMyAppsNavigationParams, myAppsRouteNames } from '~/framework/modules/myAppMenu/navigation';
+import { selectAggregatedApps } from '~/framework/modules/myapps/reducer';
+import { AppsInfoAggregated } from '~/framework/modules/myapps/types';
import { AnyNavigableModule, NavigableModuleArray } from '~/framework/util/moduleTool';
import { isEmpty } from '~/framework/util/object';
@@ -37,6 +42,39 @@ const styles = StyleSheet.create({
});
const MyAppsHomeScreen = (props: MyAppsHomeScreenProps) => {
+ const [apps, setApps] = React.useState([]);
+ React.useEffect(() => {
+ const store = getStore();
+
+ const updateApps = () => {
+ const state = store.getState();
+ const aggregatedApps = selectAggregatedApps(state);
+ setApps(aggregatedApps);
+ };
+
+ updateApps();
+ const unsubscribe = store.subscribe(updateApps);
+ return unsubscribe;
+ }, []);
+
+ const renderNewMyAppsGrid = () => {
+ return (
+ item.name}
+ gap={UI_SIZES.spacing.big}
+ gapOutside={UI_SIZES.spacing.big}
+ renderItem={({ item }) => (
+ console.debug('PRESS', item.name)}
+ onLongPress={() => console.debug('LONG PRESS', item.name)}
+ />
+ )}
+ />
+ );
+ };
+
const renderGrid = () => {
const allModules = (props.modules ?? [])?.sort((a, b) =>
I18n.get(a.config.displayI18n).localeCompare(I18n.get(b.config.displayI18n)),
@@ -115,6 +153,8 @@ const MyAppsHomeScreen = (props: MyAppsHomeScreenProps) => {
return (
+ {renderNewMyAppsGrid()}
+
{renderGrid()}
{renderOtherModules()}
diff --git a/src/framework/modules/myapps/apply-apps-to-modules.ts b/src/framework/modules/myapps/apply-apps-to-modules.ts
new file mode 100644
index 0000000000..54d795b2ed
--- /dev/null
+++ b/src/framework/modules/myapps/apply-apps-to-modules.ts
@@ -0,0 +1,56 @@
+import { AppsInfo } from './types';
+
+import { PictureProps } from '~/framework/components/picture';
+import {
+ AnyNavigableModule,
+ AnyNavigableModuleConfig,
+ dynamiclyRegisterModules,
+ INavigableModuleConfigDeclaration,
+ NavigableModuleArray,
+} from '~/framework/util/moduleTool';
+
+function buildDisplayPicture(app: AppsInfo): PictureProps | undefined {
+ if (!app.icon) return undefined;
+
+ if (app.icon.startsWith('http') || app.icon.startsWith('/')) {
+ return { source: { uri: app.icon }, type: 'Image' } as const;
+ }
+
+ return { name: app.icon, type: 'Icon' } as const;
+}
+
+export function applyAppsToModules(modules: NavigableModuleArray, apps: AppsInfo[]) {
+ const appsByName = new Map(apps.map(a => [a.name, a]));
+
+ modules.forEach(module => {
+ const app = appsByName.get(module.config.name);
+ const config = module.config as AnyNavigableModuleConfig;
+
+ const navConfig = config as {
+ assignValues: (values: Partial>) => void;
+ };
+
+ if (!app?.display) {
+ navConfig.assignValues({ displayAs: undefined });
+ return;
+ }
+
+ let displayAs;
+
+ if (app.type === 'connector') {
+ displayAs = 'myAppsConnector';
+ } else if (app.isMobile) {
+ displayAs = 'myAppsMobileModule';
+ } else {
+ displayAs = 'myAppsWebModule';
+ }
+
+ navConfig.assignValues({
+ displayAs,
+ displayI18n: app.displayName,
+ displayPicture: buildDisplayPicture(app),
+ });
+ });
+
+ dynamiclyRegisterModules(modules);
+}
diff --git a/src/framework/modules/myapps/build-apps-info.ts b/src/framework/modules/myapps/build-apps-info.ts
new file mode 100644
index 0000000000..856a8323e9
--- /dev/null
+++ b/src/framework/modules/myapps/build-apps-info.ts
@@ -0,0 +1,37 @@
+import { AppBookmarks, AppsInfo, AppType } from './types';
+
+import { IEntcoreApp } from '~/framework/util/moduleTool';
+
+/**
+ * type:
+ * - connector: external or non-integrated apps (blank, http, no prefix, hash routing, external flag, _blank)
+ * - application: default
+ * isMobile is resolved later (needs loaded modules)
+ */
+export function buildAppsInfo(entcoreApps: IEntcoreApp[], favorites: AppBookmarks): Omit[] {
+ return entcoreApps.map(app => {
+ const beginsWithHttp = /^https?:\/\//i;
+
+ const isConnector =
+ app.target === '_blank' ||
+ beginsWithHttp.test(app.address) ||
+ app.address.includes('#/') ||
+ !app.prefix ||
+ app.isExternal === true;
+
+ const type: AppType = isConnector ? 'connector' : 'application';
+
+ return {
+ address: app.address,
+ display: app.display,
+ displayName: app.displayName,
+ icon: app.icon,
+ isFavorite: favorites.bookmarks.includes(app.name),
+ isPinned: favorites.applications.includes(app.name),
+ name: app.name,
+ prefix: app.prefix,
+ target: app.target ?? undefined,
+ type,
+ };
+ });
+}
diff --git a/src/framework/modules/myapps/index.ts b/src/framework/modules/myapps/index.ts
new file mode 100644
index 0000000000..ee76435f7a
--- /dev/null
+++ b/src/framework/modules/myapps/index.ts
@@ -0,0 +1,13 @@
+import { Action } from 'redux';
+
+import './init';
+import config from './module-config';
+import reducer from './reducer/reducer';
+import { AppsInfoState } from './types';
+
+import { Module } from '~/framework/util/moduleTool';
+
+module.exports = new Module<'myapps', typeof config, AppsInfoState, Action>({
+ config,
+ reducer,
+});
diff --git a/src/framework/modules/myapps/init.ts b/src/framework/modules/myapps/init.ts
new file mode 100644
index 0000000000..95470bcaa1
--- /dev/null
+++ b/src/framework/modules/myapps/init.ts
@@ -0,0 +1,11 @@
+import { initMesAppliAtLogin } from './reducer';
+
+import { AppDispatch, getStore } from '~/app/store';
+import { callAtLogin } from '~/framework/modules/auth/calls-at-login';
+
+callAtLogin(() => {
+ const store = getStore();
+ const dispatch = store.dispatch as AppDispatch;
+
+ dispatch(initMesAppliAtLogin());
+});
diff --git a/src/framework/modules/myapps/module-config.ts b/src/framework/modules/myapps/module-config.ts
new file mode 100644
index 0000000000..9d8ec8f833
--- /dev/null
+++ b/src/framework/modules/myapps/module-config.ts
@@ -0,0 +1,13 @@
+import type { AppsInfoState } from './types';
+
+import { ModuleConfig } from '~/framework/util/moduleTool';
+
+export default new ModuleConfig<'myapps', AppsInfoState>({
+ entcoreScope: [],
+ hasRight: () => true,
+ matchEntcoreApp: () => true,
+
+ matchEntcoreWidget: () => false,
+ name: 'myapps',
+ storageName: 'myapps',
+});
diff --git a/src/framework/modules/myapps/reducer/actions.ts b/src/framework/modules/myapps/reducer/actions.ts
new file mode 100644
index 0000000000..466ee78197
--- /dev/null
+++ b/src/framework/modules/myapps/reducer/actions.ts
@@ -0,0 +1,81 @@
+import { Action, UnknownAction } from 'redux';
+import { ThunkAction } from 'redux-thunk';
+
+import { setUpModulesAccess } from '~/app/modules';
+import { IGlobalState } from '~/app/store';
+import { assertSession, getSession } from '~/framework/modules/auth/reducer';
+import { applyAppsToModules } from '~/framework/modules/myapps/apply-apps-to-modules';
+import { buildAppsInfo } from '~/framework/modules/myapps/build-apps-info';
+import moduleConfig from '~/framework/modules/myapps/module-config';
+import { myAppsService } from '~/framework/modules/myapps/service';
+import { ApplicationsConfig, AppsInfo, AppsInfoActionPayloads } from '~/framework/modules/myapps/types';
+import { AnyNavigableModule, IEntcoreApp, NavigableModule, NavigableModuleArray } from '~/framework/util/moduleTool';
+
+export interface FetchStartAction extends Action {
+ type: typeof appsInfoActionTypes.fetchStart;
+}
+export interface FetchSuccessAction extends Action {
+ type: typeof appsInfoActionTypes.fetchSuccess;
+ payload: AppsInfoActionPayloads['fetchSuccess'];
+}
+export interface FetchErrorAction extends Action {
+ type: typeof appsInfoActionTypes.fetchError;
+ error: string;
+}
+
+export const appsInfoActionTypes = {
+ fetchError: moduleConfig.namespaceActionType('FETCH_ERROR'),
+ fetchStart: moduleConfig.namespaceActionType('FETCH_START'),
+ fetchSuccess: moduleConfig.namespaceActionType('FETCH_SUCCESS'),
+};
+
+export const appInfoActions = {
+ fetchError: (error: string) => ({ error, type: appsInfoActionTypes.fetchError }),
+ fetchStart: () => ({ type: appsInfoActionTypes.fetchStart }),
+ fetchSuccess: (payload: { appsInfo: AppsInfo[]; appsConfig: ApplicationsConfig[]; entcoreApps: IEntcoreApp[] }) => ({
+ payload,
+ type: appsInfoActionTypes.fetchSuccess,
+ }),
+};
+
+export const afterLoginSetup =
+ (session): ThunkAction, IGlobalState, unknown, UnknownAction> =>
+ async dispatch => {
+ dispatch(appInfoActions.fetchStart());
+
+ try {
+ const modules = setUpModulesAccess(session) as AnyNavigableModule[];
+ const navigableModules = new NavigableModuleArray(...modules.filter(m => m instanceof NavigableModule));
+
+ const [entcoreApps, appsConfig, bookmarks] = await Promise.all([
+ myAppsService.list(session),
+ myAppsService.config(session),
+ myAppsService.bookmarks(session),
+ ]);
+
+ const baseAppsInfo = buildAppsInfo(entcoreApps, bookmarks);
+
+ const appsInfo: AppsInfo[] = baseAppsInfo.map(app => {
+ const entcoreApp = entcoreApps.find(e => e.name === app.name);
+ const isMobile =
+ app.type === 'application' &&
+ !!entcoreApp &&
+ navigableModules.some(m => m.config.matchEntcoreApp?.(entcoreApp, entcoreApps));
+
+ return { ...app, isMobile };
+ });
+
+ dispatch(appInfoActions.fetchSuccess({ appsConfig, appsInfo, entcoreApps }));
+
+ applyAppsToModules(navigableModules, appsInfo);
+ } catch (e) {
+ console.error('[afterLoginSetup] ERROR', e);
+ dispatch(appInfoActions.fetchError('APPS_FETCH_ERROR'));
+ }
+ };
+
+export const initMesAppliAtLogin = (): ThunkAction, IGlobalState, unknown, UnknownAction> => async dispatch => {
+ const session = getSession();
+ if (!session) return;
+ await dispatch(afterLoginSetup(assertSession()));
+};
diff --git a/src/framework/modules/myapps/reducer/index.ts b/src/framework/modules/myapps/reducer/index.ts
new file mode 100644
index 0000000000..b9c396b0de
--- /dev/null
+++ b/src/framework/modules/myapps/reducer/index.ts
@@ -0,0 +1,3 @@
+export * from './actions';
+export * from './reducer';
+export * from './selectors';
diff --git a/src/framework/modules/myapps/reducer/reducer.ts b/src/framework/modules/myapps/reducer/reducer.ts
new file mode 100644
index 0000000000..227176b520
--- /dev/null
+++ b/src/framework/modules/myapps/reducer/reducer.ts
@@ -0,0 +1,49 @@
+import { appsInfoActionTypes, FetchErrorAction, FetchSuccessAction } from './actions';
+
+import { Reducers } from '~/app/store';
+import moduleConfig from '~/framework/modules/myapps/module-config';
+import { AppsInfoState } from '~/framework/modules/myapps/types';
+import createReducer from '~/framework/util/redux/reducerFactory';
+
+export const appsInfoInitialState: AppsInfoState = {
+ appsConfig: [],
+ appsInfo: [],
+ entcoreApps: [],
+ loading: false,
+};
+
+const reducer = createReducer(appsInfoInitialState, {
+ [appsInfoActionTypes.fetchStart]: state => {
+ return {
+ ...state,
+ error: undefined,
+ loading: true,
+ };
+ },
+
+ [appsInfoActionTypes.fetchSuccess]: (state, action) => {
+ const { appsConfig, appsInfo, entcoreApps } = (action as unknown as FetchSuccessAction).payload;
+
+ return {
+ ...state,
+ appsConfig,
+ appsInfo,
+ entcoreApps,
+ loading: false,
+ };
+ },
+
+ [appsInfoActionTypes.fetchError]: (state, action) => {
+ const { error } = action as unknown as FetchErrorAction;
+
+ return {
+ ...state,
+ error,
+ loading: false,
+ };
+ },
+});
+
+Reducers.register(moduleConfig.reducerName, reducer);
+
+export default reducer;
diff --git a/src/framework/modules/myapps/reducer/selectors.ts b/src/framework/modules/myapps/reducer/selectors.ts
new file mode 100644
index 0000000000..e78cba8a52
--- /dev/null
+++ b/src/framework/modules/myapps/reducer/selectors.ts
@@ -0,0 +1,40 @@
+import { IGlobalState } from '~/app/store';
+import moduleConfig from '~/framework/modules/myapps/module-config';
+import { appsInfoInitialState } from '~/framework/modules/myapps/reducer/reducer';
+import { AppsInfoAggregated } from '~/framework/modules/myapps/types';
+
+export const selectAppsState = (state: IGlobalState) => {
+ return moduleConfig.getState(state) ?? appsInfoInitialState;
+};
+
+export const selectAggregatedApps = (state: IGlobalState): AppsInfoAggregated[] => {
+ const slice = selectAppsState(state);
+ const appsConfig = slice?.appsConfig ?? [];
+ const appsInfo = slice?.appsInfo ?? [];
+
+ const configByName = new Map(appsConfig.map(c => [c.name, c]));
+
+ return appsInfo
+ .map(app => {
+ const config = configByName.get(app.name);
+
+ return {
+ ...app,
+ category: config?.category,
+ color: config?.color,
+ help: config?.help,
+ libraries: config?.libraries,
+ };
+ })
+ .sort((a, b) => (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name))
+ .filter(app => app.display);
+};
+
+export const selectAppsRaw = (state: IGlobalState) => {
+ const slice = selectAppsState(state);
+ return {
+ appsConfig: slice.appsConfig,
+ appsInfo: slice.appsInfo,
+ entcoreApps: slice.entcoreApps,
+ };
+};
diff --git a/src/framework/modules/myapps/service/index.ts b/src/framework/modules/myapps/service/index.ts
new file mode 100644
index 0000000000..0c18a8999f
--- /dev/null
+++ b/src/framework/modules/myapps/service/index.ts
@@ -0,0 +1,36 @@
+import { AuthActiveAccount } from '~/framework/modules/auth/model';
+import { AppBookmarks, ApplicationsConfig, ApplicationsList } from '~/framework/modules/myapps/types';
+import { IEntcoreApp } from '~/framework/util/moduleTool';
+import { signedFetch } from '~/infra/fetchWithCache';
+
+const adaptApplication = (app: ApplicationsList): IEntcoreApp => ({
+ address: app.address,
+ casType: app.casType ?? undefined,
+ display: app.display,
+ displayName: app.displayName,
+ icon: app.icon,
+ isExternal: app.isExternal,
+ name: app.name,
+ prefix: app.prefix ?? undefined,
+ target: app.target ?? undefined,
+});
+
+export const myAppsService = {
+ bookmarks: async (session: AuthActiveAccount): Promise => {
+ const res = await signedFetch(`${session.platform.url}/userbook/preference/apps`);
+ const json = await res.json();
+ return JSON.parse(json.preference);
+ },
+
+ config: async (session: AuthActiveAccount): Promise => {
+ const res = await signedFetch(`${session.platform.url}/myApps/config`);
+ return res.json();
+ },
+
+ list: async (session: AuthActiveAccount): Promise => {
+ const res = await signedFetch(`${session.platform.url}/applications-list`);
+ const json = await res.json();
+ const apps: ApplicationsList[] = Array.isArray(json) ? json : (json.applications ?? json.apps ?? []);
+ return apps.map(adaptApplication);
+ },
+};
diff --git a/src/framework/modules/myapps/types.ts b/src/framework/modules/myapps/types.ts
new file mode 100644
index 0000000000..d19af418cc
--- /dev/null
+++ b/src/framework/modules/myapps/types.ts
@@ -0,0 +1,70 @@
+import { IEntcoreApp } from '~/framework/util/moduleTool';
+
+export type AppType = 'application' | 'connector';
+
+export interface AppsInfo extends Partial> {
+ name: string;
+ displayName: string;
+ display: boolean;
+ address: string;
+ target?: string;
+ prefix?: string;
+ icon?: string;
+ type: AppType;
+ isMobile: boolean;
+ isFavorite: boolean;
+ isPinned: boolean;
+}
+
+export interface ApplicationsConfig {
+ category: string;
+ color: string;
+ help: Record;
+ libraries: string;
+ name: string;
+}
+
+export interface AppBookmarks {
+ applications: string[];
+ bookmarks: string[];
+}
+
+export interface ApplicationsList {
+ address: string;
+ casType: string | null;
+ display: boolean;
+ displayName: string;
+ icon: string;
+ isExternal: boolean;
+ name: string;
+ prefix: string;
+ scope: string[];
+ target: string | null;
+}
+
+export interface AppsInfoState {
+ appsInfo: AppsInfo[];
+ appsConfig: ApplicationsConfig[];
+ entcoreApps: IEntcoreApp[];
+ loading: boolean;
+ error?: string;
+}
+
+export interface AppsInfoActionPayloads {
+ fetchStart: undefined;
+ fetchSuccess: {
+ appsInfo: AppsInfo[];
+ appsConfig: ApplicationsConfig[];
+ entcoreApps: IEntcoreApp[];
+ };
+ fetchError: {
+ error: string;
+ };
+}
+
+export interface AppsInfoAggregated extends AppsInfo {
+ category?: string;
+ color?: string;
+ help?: Record;
+ libraries?: string;
+}
diff --git a/src/framework/modules/timeline/screens/timeline-screen.tsx b/src/framework/modules/timeline/screens/timeline-screen.tsx
index 0e2803dd33..60d06f7081 100755
--- a/src/framework/modules/timeline/screens/timeline-screen.tsx
+++ b/src/framework/modules/timeline/screens/timeline-screen.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Alert, Image, ListRenderItemInfo, RefreshControl, TouchableOpacity, View } from 'react-native';
+import { Alert, ListRenderItemInfo, RefreshControl, TouchableOpacity, View } from 'react-native';
import { NavigationProp, ParamListBase } from '@react-navigation/native';
import { NativeStackNavigationOptions, NativeStackScreenProps } from '@react-navigation/native-stack';
@@ -106,11 +106,12 @@ const getTimelineItems = (flashMessages: FlashMessagesStateData, notifications:
const msgs = flashMessages ?? [];
const notifs = notifications && notifications.data ? notifications.data : [];
const ret = [] as (ITimelineItem & { key: string })[];
- for (const fm of msgs) {
- if (!fm.dismiss) {
- ret.push({ data: fm, key: fm.id.toString(), type: ITimelineItemType.FLASHMSG });
+ if (msgs.length)
+ for (const fm of msgs) {
+ if (!fm.dismiss) {
+ ret.push({ data: fm, key: fm.id.toString(), type: ITimelineItemType.FLASHMSG });
+ }
}
- }
for (const n of notifs) {
ret.push({ data: n, key: n.id, type: ITimelineItemType.NOTIFICATION });
}