Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/icons/uiIcons/favorite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/app/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 || []),

Expand Down
5 changes: 3 additions & 2 deletions src/app/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<IGlobalState, unknown, UnknownAction>;

/** === Store getter === */

Expand Down
1 change: 1 addition & 0 deletions src/framework/components/picture/svg/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <Svg name={svgIconName} fill="white" width={UI_SIZES.spacing.huge} height={UI_SIZES.spacing.huge} />;
}

return <Image source={{ uri: icon }} style={styles.image} />;
}, [icon, svgIconName, styles.image]);

const renderFavoriteBadge = useCallback(() => {
if (!app.isFavorite) return null;

return (
<View style={styles.favoriteIcon}>
<Svg name="ui-favorite" width={UI_SIZES.spacing.large} height={UI_SIZES.spacing.large} />
</View>
);
}, [app.isFavorite, styles.favoriteIcon]);

return (
<View style={styles.wrapper}>
<Pressable onPress={onPress} onLongPress={onLongPress} style={styles.card}>
{renderFavoriteBadge()}
{renderIcon()}
</Pressable>

<View style={styles.titleRow}>
<BodyText numberOfLines={2} style={styles.title}>
{app.displayName}
</BodyText>

{isWebApp && (
<TouchableOpacity>
<Svg name="ui-external-link" width={UI_SIZES.spacing.medium} height={UI_SIZES.spacing.medium} fill="black" />
</TouchableOpacity>
)}
</View>
</View>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { MyAppsCard } from './component';

export default MyAppsCard;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AppsInfoAggregated } from '~/framework/modules/myapps/types';

export type MyAppsCardProps = {
app: AppsInfoAggregated;
onPress?: () => void;
onLongPress?: () => void;
};
Original file line number Diff line number Diff line change
@@ -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;
};
40 changes: 40 additions & 0 deletions src/framework/modules/myAppMenu/screens/MyAppsHomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand All @@ -37,6 +42,39 @@ const styles = StyleSheet.create({
});

const MyAppsHomeScreen = (props: MyAppsHomeScreenProps) => {
const [apps, setApps] = React.useState<AppsInfoAggregated[]>([]);
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 (
<GridList
data={apps}
keyExtractor={item => item.name}
gap={UI_SIZES.spacing.big}
gapOutside={UI_SIZES.spacing.big}
renderItem={({ item }) => (
<MyAppsCard
app={item}
onPress={() => 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)),
Expand Down Expand Up @@ -115,6 +153,8 @@ const MyAppsHomeScreen = (props: MyAppsHomeScreenProps) => {
return (
<PageView>
<ScrollView bottomInset={false}>
{renderNewMyAppsGrid()}

{renderGrid()}
{renderOtherModules()}
<View style={styles.webButton}>
Expand Down
56 changes: 56 additions & 0 deletions src/framework/modules/myapps/apply-apps-to-modules.ts
Original file line number Diff line number Diff line change
@@ -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<AnyNavigableModule>, 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<INavigableModuleConfigDeclaration<string>>) => 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);
}
37 changes: 37 additions & 0 deletions src/framework/modules/myapps/build-apps-info.ts
Original file line number Diff line number Diff line change
@@ -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<AppsInfo, 'isMobile'>[] {
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,
};
});
}
13 changes: 13 additions & 0 deletions src/framework/modules/myapps/index.ts
Original file line number Diff line number Diff line change
@@ -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,
});
11 changes: 11 additions & 0 deletions src/framework/modules/myapps/init.ts
Original file line number Diff line number Diff line change
@@ -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());
});
Loading