cozy-bar 0.0.0-development
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/cozy-bar.min.js +77 -0
- package/dist/cozy-bar.min.js.map +1 -0
- package/package.json +165 -0
- package/src/assets/icons/16/icon-storage-16.svg +3 -0
- package/src/assets/icons/24/icon-arrow-left.svg +3 -0
- package/src/assets/icons/32/icon-claudy.svg +1 -0
- package/src/assets/icons/apps/icon-collect.svg +25 -0
- package/src/assets/icons/apps/icon-drive.svg +17 -0
- package/src/assets/icons/apps/icon-market-soon.svg +25 -0
- package/src/assets/icons/apps/icon-photos.svg +19 -0
- package/src/assets/icons/apps/icon-soon.svg +21 -0
- package/src/assets/icons/apps/icon-store.svg +19 -0
- package/src/assets/icons/claudyActions/icon-bills.svg +6 -0
- package/src/assets/icons/claudyActions/icon-laptop.svg +7 -0
- package/src/assets/icons/claudyActions/icon-phone.svg +8 -0
- package/src/assets/icons/claudyActions/icon-question-mark.svg +6 -0
- package/src/assets/icons/comingsoon/icon-bank.svg +12 -0
- package/src/assets/icons/comingsoon/icon-sante.svg +12 -0
- package/src/assets/icons/comingsoon/icon-store.svg +6 -0
- package/src/assets/icons/icon-cozy.svg +3 -0
- package/src/assets/icons/icon-shield.svg +3 -0
- package/src/assets/icons/spinner.svg +4 -0
- package/src/assets/sprites/icon-apps.svg +1 -0
- package/src/assets/sprites/icon-cozy-home.svg +16 -0
- package/src/components/Apps/AppItem.jsx +117 -0
- package/src/components/Apps/AppItemPlaceholder.jsx +12 -0
- package/src/components/Apps/AppNavButtons.jsx +94 -0
- package/src/components/Apps/AppsContent.jsx +91 -0
- package/src/components/Apps/ButtonCozyHome.jsx +30 -0
- package/src/components/Apps/ButtonCozyHome.spec.jsx +53 -0
- package/src/components/Apps/IconCozyHome.jsx +38 -0
- package/src/components/Apps/index.jsx +72 -0
- package/src/components/Banner.jsx +41 -0
- package/src/components/Bar.jsx +295 -0
- package/src/components/Bar.spec.jsx +133 -0
- package/src/components/Claudy.jsx +81 -0
- package/src/components/ClaudyIcon.jsx +18 -0
- package/src/components/Drawer.jsx +227 -0
- package/src/components/Drawer.spec.jsx +98 -0
- package/src/components/SearchBar.jsx +358 -0
- package/src/components/Settings/SettingsContent.jsx +163 -0
- package/src/components/Settings/StorageData.jsx +29 -0
- package/src/components/Settings/helper.js +8 -0
- package/src/components/Settings/index.jsx +220 -0
- package/src/components/StorageIcon.jsx +16 -0
- package/src/components/SupportModal.jsx +59 -0
- package/src/components/__snapshots__/Bar.spec.jsx.snap +302 -0
- package/src/config/claudyActions.json +20 -0
- package/src/config/persistWhitelist.json +4 -0
- package/src/dom.js +80 -0
- package/src/index.jsx +242 -0
- package/src/index.spec.jsx +34 -0
- package/src/lib/api/helpers.js +13 -0
- package/src/lib/api/index.jsx +145 -0
- package/src/lib/exceptions.js +89 -0
- package/src/lib/expiringMemoize.js +13 -0
- package/src/lib/icon.js +77 -0
- package/src/lib/intents.js +16 -0
- package/src/lib/logger.js +11 -0
- package/src/lib/middlewares/appsI18n.js +57 -0
- package/src/lib/realtime.js +43 -0
- package/src/lib/reducers/apps.js +175 -0
- package/src/lib/reducers/apps.spec.js +59 -0
- package/src/lib/reducers/content.js +50 -0
- package/src/lib/reducers/context.js +86 -0
- package/src/lib/reducers/index.js +73 -0
- package/src/lib/reducers/locale.js +22 -0
- package/src/lib/reducers/settings.js +111 -0
- package/src/lib/reducers/theme.js +48 -0
- package/src/lib/reducers/unserializable.js +26 -0
- package/src/lib/stack-client.js +401 -0
- package/src/lib/stack.js +79 -0
- package/src/lib/store/index.js +44 -0
- package/src/locales/de.json +57 -0
- package/src/locales/en.json +57 -0
- package/src/locales/es.json +57 -0
- package/src/locales/fr.json +57 -0
- package/src/locales/it.json +57 -0
- package/src/locales/ja.json +57 -0
- package/src/locales/nl_NL.json +57 -0
- package/src/locales/pl.json +57 -0
- package/src/locales/ru.json +57 -0
- package/src/locales/sq.json +57 -0
- package/src/locales/zh_CN.json +57 -0
- package/src/proptypes/index.js +10 -0
- package/src/queries/index.js +16 -0
- package/src/styles/apps.css +248 -0
- package/src/styles/banner.css +64 -0
- package/src/styles/bar.css +106 -0
- package/src/styles/base.css +21 -0
- package/src/styles/claudy.css +98 -0
- package/src/styles/drawer.css +126 -0
- package/src/styles/index.styl +33 -0
- package/src/styles/indicators.css +58 -0
- package/src/styles/nav.css +81 -0
- package/src/styles/navigation_item.css +34 -0
- package/src/styles/searchbar.css +156 -0
- package/src/styles/settings.css +34 -0
- package/src/styles/storage.css +22 -0
- package/src/styles/supportModal.css +20 -0
- package/src/styles/theme.styl +25 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React, { Component } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
setContent,
|
|
4
|
+
unsetContent,
|
|
5
|
+
setLocale,
|
|
6
|
+
setTheme,
|
|
7
|
+
setWebviewContext
|
|
8
|
+
} from 'lib/reducers'
|
|
9
|
+
|
|
10
|
+
import { locations, getJsApiName, getReactApiName } from 'lib/api/helpers'
|
|
11
|
+
|
|
12
|
+
// The React API need unique IDs, so we will increment this variable
|
|
13
|
+
let idToIncrement = 0
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wraps argument into a React element if it is a string. Is used
|
|
17
|
+
* for setBar{Left,Right,Center} to be able to pass HTML
|
|
18
|
+
*
|
|
19
|
+
* @param {ReactElement|string} v
|
|
20
|
+
* @return {ReactElement}
|
|
21
|
+
*/
|
|
22
|
+
const wrapInElement = v => {
|
|
23
|
+
if (typeof v === 'string') {
|
|
24
|
+
return <span dangerouslySetInnerHTML={{ __html: v }} />
|
|
25
|
+
} else {
|
|
26
|
+
return v
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a React component that enables to access store
|
|
32
|
+
* properties in a declarative way.
|
|
33
|
+
*
|
|
34
|
+
* @param {BarStore} store
|
|
35
|
+
*/
|
|
36
|
+
const barContentComponent = (store, location) =>
|
|
37
|
+
class BarContent extends Component {
|
|
38
|
+
componentDidMount() {
|
|
39
|
+
this.componentId = idToIncrement++
|
|
40
|
+
this.setContent(this.props.children)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setContent(content) {
|
|
44
|
+
try {
|
|
45
|
+
content = React.Children.only(content)
|
|
46
|
+
// eslint-disable-next-line no-empty
|
|
47
|
+
} catch (e) {}
|
|
48
|
+
store.dispatch(setContent(location, content, this.componentId))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
unsetContent() {
|
|
52
|
+
store.dispatch(unsetContent(location, this.componentId))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
componentWillUnmount() {
|
|
56
|
+
this.unsetContent()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
componentDidUpdate(prevProps) {
|
|
60
|
+
if (this.props.children !== prevProps.children) {
|
|
61
|
+
this.setContent(this.props.children)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render() {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a public API
|
|
72
|
+
*
|
|
73
|
+
* - getters/setters for public attributes
|
|
74
|
+
* - React components that act as getters/setters
|
|
75
|
+
*
|
|
76
|
+
* @param {ReduxStore} store - Store on which the API will act
|
|
77
|
+
* @return {object} - Methods of the public API
|
|
78
|
+
*/
|
|
79
|
+
export const createBarAPI = store => {
|
|
80
|
+
// setBar{Left,Right,Center} and <Bar{Left,Right,Center} />
|
|
81
|
+
const methods = {}
|
|
82
|
+
locations.forEach(location => {
|
|
83
|
+
// expose JS API
|
|
84
|
+
methods[getJsApiName(location)] = value =>
|
|
85
|
+
store.dispatch(setContent(location, wrapInElement(value), 'js'))
|
|
86
|
+
|
|
87
|
+
// expose React API
|
|
88
|
+
methods[getReactApiName(location)] = barContentComponent(store, location)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
methods.setLocale = (...args) => {
|
|
92
|
+
store.dispatch(setLocale(...args))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
methods.setTheme = (...args) => {
|
|
96
|
+
store.dispatch(setTheme(...args))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
methods.setWebviewContext = (...args) => {
|
|
100
|
+
store.dispatch(setWebviewContext(...args))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return methods
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle exceptions for API before init
|
|
107
|
+
const showAPIError = name => {
|
|
108
|
+
// eslint-disable-next-line no-console
|
|
109
|
+
console.error(
|
|
110
|
+
`You tried to use the CozyBar API (${name}) but the CozyBar is not initialised yet via cozy.bar.init(...).`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const makeProxyMethodToAPI = (exposedAPI, fnName) => {
|
|
115
|
+
return (...args) => {
|
|
116
|
+
if (exposedAPI[fnName]) {
|
|
117
|
+
return exposedAPI[fnName](...args)
|
|
118
|
+
} else {
|
|
119
|
+
showAPIError(fnName)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Creates an API that swallows error until bar is correctly initialized */
|
|
125
|
+
export const createBarProxiedAPI = exposedAPI => {
|
|
126
|
+
const apiReferences = {}
|
|
127
|
+
|
|
128
|
+
locations.forEach(location => {
|
|
129
|
+
const jsAPIName = getJsApiName(location)
|
|
130
|
+
const reactAPIName = getReactApiName(location)
|
|
131
|
+
apiReferences[jsAPIName] = makeProxyMethodToAPI(exposedAPI, jsAPIName)
|
|
132
|
+
apiReferences[reactAPIName] = props => {
|
|
133
|
+
if (exposedAPI[reactAPIName]) {
|
|
134
|
+
return React.createElement(exposedAPI[reactAPIName], props)
|
|
135
|
+
} else {
|
|
136
|
+
showAPIError(reactAPIName)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
for (let fnName of ['setLocale', 'setTheme', 'setWebviewContext']) {
|
|
142
|
+
apiReferences[fnName] = makeProxyMethodToAPI(exposedAPI, fnName)
|
|
143
|
+
}
|
|
144
|
+
return apiReferences
|
|
145
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
class ForbiddenException extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super()
|
|
4
|
+
|
|
5
|
+
this.name = 'Forbidden'
|
|
6
|
+
this.status = 403
|
|
7
|
+
this.message =
|
|
8
|
+
message ||
|
|
9
|
+
'The application does not have permission to access this resource.'
|
|
10
|
+
this.stack = new Error().stack
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class ServerErrorException extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super()
|
|
17
|
+
|
|
18
|
+
this.name = 'ServerError'
|
|
19
|
+
this.status = 500
|
|
20
|
+
this.message = message || 'A server error occurred'
|
|
21
|
+
this.stack = new Error().stack
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class NotFoundException extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super()
|
|
28
|
+
|
|
29
|
+
this.name = 'NotFound'
|
|
30
|
+
this.status = 404
|
|
31
|
+
this.message = message || 'The ressource was not found'
|
|
32
|
+
this.stack = new Error().stack
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class MethodNotAllowedException extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super()
|
|
39
|
+
|
|
40
|
+
this.name = 'MethodNotAllowed'
|
|
41
|
+
this.status = 405
|
|
42
|
+
this.message = message || 'Method not allowed'
|
|
43
|
+
this.stack = new Error().stack
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class UnavailableStackException extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super()
|
|
50
|
+
|
|
51
|
+
this.name = 'UnavailableStack'
|
|
52
|
+
this.message = message || 'The stack is temporarily unavailable'
|
|
53
|
+
this.stack = new Error().stack
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class UnauthorizedStackException extends Error {
|
|
58
|
+
constructor(message) {
|
|
59
|
+
super()
|
|
60
|
+
|
|
61
|
+
this.name = 'UnauthorizedStack'
|
|
62
|
+
this.status = 401
|
|
63
|
+
this.message =
|
|
64
|
+
message || 'The app is not allowed to access to the requested resource'
|
|
65
|
+
this.stack = new Error().stack
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class UnavailableSettingsException extends Error {
|
|
70
|
+
constructor(message) {
|
|
71
|
+
super()
|
|
72
|
+
|
|
73
|
+
this.name = 'UnavailableSettings'
|
|
74
|
+
this.message =
|
|
75
|
+
message ||
|
|
76
|
+
"The 'Settings' application isn't available or installed in the stack"
|
|
77
|
+
this.stack = new Error().stack
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
ForbiddenException,
|
|
83
|
+
ServerErrorException,
|
|
84
|
+
NotFoundException,
|
|
85
|
+
MethodNotAllowedException,
|
|
86
|
+
UnavailableStackException,
|
|
87
|
+
UnavailableSettingsException,
|
|
88
|
+
UnauthorizedStackException
|
|
89
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default function(fn, duration, keyFn) {
|
|
2
|
+
const memo = {}
|
|
3
|
+
return arg => {
|
|
4
|
+
const key = keyFn(arg)
|
|
5
|
+
const memoInfo = memo[key]
|
|
6
|
+
const uptodate =
|
|
7
|
+
memoInfo && memoInfo.result && memoInfo.date - Date.now() < duration
|
|
8
|
+
if (!uptodate) {
|
|
9
|
+
memo[key] = { result: fn(arg), date: Date.now() }
|
|
10
|
+
}
|
|
11
|
+
return memo[key].result
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/lib/icon.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const cache = {}
|
|
2
|
+
|
|
3
|
+
const mimeTypes = {
|
|
4
|
+
gif: 'image/gif',
|
|
5
|
+
ico: 'image/vnd.microsoft.icon',
|
|
6
|
+
jpeg: 'image/jpeg',
|
|
7
|
+
jpg: 'image/jpeg',
|
|
8
|
+
png: 'image/png',
|
|
9
|
+
svg: 'image/svg+xml'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get an icon URL usable in the HTML page from it's stack path
|
|
14
|
+
*
|
|
15
|
+
* @function
|
|
16
|
+
* @private
|
|
17
|
+
* @param {function} iconFetcher - takes an icon path on the stack
|
|
18
|
+
* and returns a fetch response with the icon
|
|
19
|
+
* @param {object} app - app object with a `links.icon` attribute
|
|
20
|
+
* @param {boolean} useCache
|
|
21
|
+
* @returns {Promise} url string of an icon usable in the HTML page
|
|
22
|
+
* may be empty if the `app` object didn't have an icon path
|
|
23
|
+
*/
|
|
24
|
+
export default async function getIcon(iconFetcher, app = {}, useCache = true) {
|
|
25
|
+
if (useCache && cache.icons && cache.icons[url]) return cache.icons[url]
|
|
26
|
+
|
|
27
|
+
const url = app.links && app.links.icon
|
|
28
|
+
if (!url) return ''
|
|
29
|
+
let icon
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const resp = await iconFetcher(url)
|
|
33
|
+
if (!resp.ok)
|
|
34
|
+
throw new Error(`Error while fetching icon ${resp.statusText}: ${url}`)
|
|
35
|
+
icon = await resp.blob()
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw error
|
|
38
|
+
}
|
|
39
|
+
if (!icon.type) {
|
|
40
|
+
// iOS10 does not set correctly mime type for images, so we assume
|
|
41
|
+
// that an empty mime type could mean that the app is running on iOS10.
|
|
42
|
+
// For regular images like jpeg, png or gif it still works well in the
|
|
43
|
+
// Safari browser but not for SVG.
|
|
44
|
+
// So let's set a mime type manually. We cannot always set it to
|
|
45
|
+
// image/svg+xml and must guess the mime type based on the icon attribute
|
|
46
|
+
// from app/manifest
|
|
47
|
+
// See https://stackoverflow.com/questions/38318411/uiwebview-on-ios-10-beta-not-loading-any-svg-images
|
|
48
|
+
if (!app.icon) {
|
|
49
|
+
throw new Error(`${app.name}: Cannot detect mime type for icon ${url}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const extension = app.icon.split('.').pop()
|
|
53
|
+
|
|
54
|
+
if (!extension) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`${app.name}: Unable to detect icon mime type from extension (${app.icon})`
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!mimeTypes[extension]) {
|
|
61
|
+
throw new Error(`${app.name}: 'Unexpected icon extension (${app.icon})`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
icon = new Blob([icon], { type: mimeTypes[extension] })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (icon.type.match(/^image\/.*$/)) {
|
|
68
|
+
const iconURL = URL.createObjectURL(icon)
|
|
69
|
+
if (useCache) {
|
|
70
|
+
cache.icons = cache.icons || {}
|
|
71
|
+
cache.icons[url] = iconURL
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return iconURL
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`${app.name}: icon ${url} is not an image.`)
|
|
77
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { cozyFetchJSON } from 'lib/stack'
|
|
2
|
+
|
|
3
|
+
// This is a function that does the bare minimum in order to bypass the normal intent flow. To be replaced in th next version of intents.
|
|
4
|
+
export function fetchRawIntent(action, type, data = {}, permissions = []) {
|
|
5
|
+
return cozyFetchJSON(null, 'POST', '/intents', {
|
|
6
|
+
data: {
|
|
7
|
+
type: 'io.cozy.intents',
|
|
8
|
+
attributes: {
|
|
9
|
+
action: action,
|
|
10
|
+
type: type,
|
|
11
|
+
data: data,
|
|
12
|
+
permissions: permissions
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { extend as extendI18n } from 'cozy-ui/react/I18n'
|
|
2
|
+
import { SET_LOCALE } from 'lib/reducers/locale'
|
|
3
|
+
|
|
4
|
+
const extendI18nWithApp = lang => app => {
|
|
5
|
+
const { langs, locales } = app
|
|
6
|
+
|
|
7
|
+
const hasLangs = langs && langs.length
|
|
8
|
+
if (!hasLangs) {
|
|
9
|
+
// TODO The app does not provide langs, we should probably warn the developer
|
|
10
|
+
// when the app is published on the registry.
|
|
11
|
+
return app
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const providesLang = hasLangs && langs.includes(lang)
|
|
15
|
+
const currentLang = providesLang ? lang : langs[0]
|
|
16
|
+
|
|
17
|
+
const localeKeys = locales && Object.keys(locales)
|
|
18
|
+
const providesLocales =
|
|
19
|
+
localeKeys && localeKeys.length && localeKeys.includes(currentLang)
|
|
20
|
+
|
|
21
|
+
if (!providesLocales) {
|
|
22
|
+
// TODO The app does not provide locales, we should probably warn the developer
|
|
23
|
+
// when the app is published on the regisry.
|
|
24
|
+
return app
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
extendI18n({ [app.slug]: locales[currentLang] })
|
|
28
|
+
return app
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const useLang = (apps, lang) => {
|
|
32
|
+
apps && apps.forEach(extendI18nWithApp(lang))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const appsI18nMiddleware = ({ getState }) => next => action => {
|
|
36
|
+
const state = getState()
|
|
37
|
+
switch (action.type) {
|
|
38
|
+
case SET_LOCALE: {
|
|
39
|
+
const apps = state.apps && state.apps.apps
|
|
40
|
+
useLang(apps, action.lang)
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
case 'RECEIVE_APP_LIST':
|
|
44
|
+
action.apps &&
|
|
45
|
+
action.apps.length &&
|
|
46
|
+
action.apps.forEach(extendI18nWithApp(state.locale))
|
|
47
|
+
break
|
|
48
|
+
case 'RECEIVE_APP':
|
|
49
|
+
action.app &&
|
|
50
|
+
extendI18nWithApp(state.locale && state.locale.lang)(action.app)
|
|
51
|
+
break
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return next(action)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default appsI18nMiddleware
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import CozyRealtime from 'cozy-realtime'
|
|
2
|
+
|
|
3
|
+
const APPS_DOCTYPE = 'io.cozy.apps'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initialize realtime sockets
|
|
7
|
+
*
|
|
8
|
+
* @private
|
|
9
|
+
* @param {object}
|
|
10
|
+
* @returns {Promise}
|
|
11
|
+
*/
|
|
12
|
+
function initializeRealtime({ getApp, onCreate, onDelete, cozyClient }) {
|
|
13
|
+
const handleAppCreation = async app => {
|
|
14
|
+
// Fetch directly the app to get attributes `related` as well.
|
|
15
|
+
let fullApp
|
|
16
|
+
try {
|
|
17
|
+
fullApp = await getApp(app.slug)
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new Error(`Cannot fetch app ${app.slug}: ${error.message}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof onCreate === 'function') {
|
|
23
|
+
onCreate(fullApp)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const handleAppRemoval = app => {
|
|
28
|
+
if (typeof onDelete === 'function') {
|
|
29
|
+
onDelete(app)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const realtime = new CozyRealtime({ client: cozyClient })
|
|
35
|
+
realtime.subscribe('created', APPS_DOCTYPE, handleAppCreation)
|
|
36
|
+
realtime.subscribe('deleted', APPS_DOCTYPE, handleAppRemoval)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// eslint-disable-next-line no-console
|
|
39
|
+
console.warn(`Cannot initialize realtime in Cozy-bar: ${error.message}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default initializeRealtime
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import stack from 'lib/stack'
|
|
2
|
+
import unionWith from 'lodash.unionwith'
|
|
3
|
+
|
|
4
|
+
// constants
|
|
5
|
+
const DELETE_APP = 'DELETE_APP'
|
|
6
|
+
const RECEIVE_APP = 'RECEIVE_APP'
|
|
7
|
+
const RECEIVE_APP_LIST = 'RECEIVE_APP_LIST'
|
|
8
|
+
const RECEIVE_HOME_APP = 'RECEIVE_HOME_APP'
|
|
9
|
+
const FETCH_APPS = 'FETCH_APPS'
|
|
10
|
+
const FETCH_APPS_FAILURE = 'FETCH_APPS_FAILURE'
|
|
11
|
+
const SET_INFOS = 'SET_INFOS'
|
|
12
|
+
|
|
13
|
+
export const isCurrentApp = (state, app) => app.slug === state.appSlug
|
|
14
|
+
|
|
15
|
+
// selectors
|
|
16
|
+
export const getApps = state => {
|
|
17
|
+
if (!state.apps) return []
|
|
18
|
+
return state.apps
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const getHomeApp = state => {
|
|
22
|
+
return state.homeApp
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const isFetchingApps = state => {
|
|
26
|
+
return state ? state.isFetching : false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const hasFetched = state => state.hasFetched
|
|
30
|
+
|
|
31
|
+
// actions
|
|
32
|
+
export const deleteApp = app => ({ type: DELETE_APP, app })
|
|
33
|
+
export const receiveApp = app => ({ type: RECEIVE_APP, app })
|
|
34
|
+
const receiveAppList = apps => ({ type: RECEIVE_APP_LIST, apps })
|
|
35
|
+
const receiveHomeApp = homeApp => ({ type: RECEIVE_HOME_APP, homeApp })
|
|
36
|
+
export const setInfos = (appName, appNamePrefix, appSlug) => ({
|
|
37
|
+
type: SET_INFOS,
|
|
38
|
+
appName,
|
|
39
|
+
appNamePrefix,
|
|
40
|
+
appSlug
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// actions async
|
|
44
|
+
export const fetchApps = () => async dispatch => {
|
|
45
|
+
try {
|
|
46
|
+
dispatch({ type: FETCH_APPS })
|
|
47
|
+
const rawAppList = await stack.get.apps()
|
|
48
|
+
const apps = rawAppList.map(mapApp)
|
|
49
|
+
if (!rawAppList.length)
|
|
50
|
+
throw new Error('No installed apps found by the bar')
|
|
51
|
+
// TODO load only one time icons
|
|
52
|
+
await dispatch(setDefaultApp(apps))
|
|
53
|
+
await dispatch(receiveAppList(apps))
|
|
54
|
+
} catch (e) {
|
|
55
|
+
dispatch({ type: FETCH_APPS_FAILURE })
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.warn(e.message ? e.message : e)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
*
|
|
63
|
+
* @param {Array} appsList
|
|
64
|
+
*/
|
|
65
|
+
export const setDefaultApp = appsList => async dispatch => {
|
|
66
|
+
try {
|
|
67
|
+
const context = await stack.get.context()
|
|
68
|
+
const defaultRedirection =
|
|
69
|
+
context.data &&
|
|
70
|
+
context.data.attributes &&
|
|
71
|
+
context.data.attributes.default_redirection
|
|
72
|
+
let homeApp = null
|
|
73
|
+
// self hosted cozy has no context by default
|
|
74
|
+
// so let's use hardcoded home slug if needed
|
|
75
|
+
if (!defaultRedirection) {
|
|
76
|
+
const HOME_APP_SLUG = 'home'
|
|
77
|
+
homeApp = findAppInArray(HOME_APP_SLUG, appsList)
|
|
78
|
+
} else {
|
|
79
|
+
const slugRegexp = /^([^/]+)\/.*/
|
|
80
|
+
const matches = defaultRedirection.match(slugRegexp)
|
|
81
|
+
const defaultAppSlug = matches && matches[1]
|
|
82
|
+
homeApp = findAppInArray(defaultAppSlug, appsList)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (homeApp) {
|
|
86
|
+
return dispatch(receiveHomeApp(homeApp))
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// eslint-disable-next-line no-console
|
|
90
|
+
console.warn(`Cozy-bar cannot fetch home app data: ${error.message}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// reducers
|
|
95
|
+
const defaultState = {
|
|
96
|
+
apps: [],
|
|
97
|
+
homeApp: null,
|
|
98
|
+
isFetching: true,
|
|
99
|
+
appName: null,
|
|
100
|
+
appNamePrefix: null,
|
|
101
|
+
appSlug: null,
|
|
102
|
+
hasFetched: false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const reducer = (state = defaultState, action) => {
|
|
106
|
+
switch (action.type) {
|
|
107
|
+
case FETCH_APPS:
|
|
108
|
+
return { ...state, isFetching: true }
|
|
109
|
+
case FETCH_APPS_FAILURE:
|
|
110
|
+
return { ...state, isFetching: false }
|
|
111
|
+
case RECEIVE_APP:
|
|
112
|
+
return {
|
|
113
|
+
...state,
|
|
114
|
+
apps: unionWith(
|
|
115
|
+
state.apps,
|
|
116
|
+
[mapApp(action.app)],
|
|
117
|
+
(appA, appB) => appA.slug === appB.slug
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
case RECEIVE_APP_LIST: {
|
|
121
|
+
const appsList = action.apps.map(app => ({
|
|
122
|
+
...app,
|
|
123
|
+
isCurrentApp: isCurrentApp(state, app)
|
|
124
|
+
}))
|
|
125
|
+
return { ...state, isFetching: false, hasFetched: true, apps: appsList }
|
|
126
|
+
}
|
|
127
|
+
case RECEIVE_HOME_APP: {
|
|
128
|
+
const homeApp = action.homeApp
|
|
129
|
+
return isCurrentApp(state, homeApp)
|
|
130
|
+
? {
|
|
131
|
+
...state,
|
|
132
|
+
homeApp: { ...homeApp, isCurrentApp: true }
|
|
133
|
+
}
|
|
134
|
+
: { ...state, homeApp }
|
|
135
|
+
}
|
|
136
|
+
case DELETE_APP:
|
|
137
|
+
return {
|
|
138
|
+
...state,
|
|
139
|
+
apps: state.apps.filter(app => app.slug !== action.app.slug)
|
|
140
|
+
}
|
|
141
|
+
case SET_INFOS:
|
|
142
|
+
return {
|
|
143
|
+
...state,
|
|
144
|
+
appName: action.appName,
|
|
145
|
+
appNamePrefix: action.appNamePrefix,
|
|
146
|
+
appSlug: action.appSlug
|
|
147
|
+
}
|
|
148
|
+
default:
|
|
149
|
+
return state
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default reducer
|
|
154
|
+
|
|
155
|
+
// helpers
|
|
156
|
+
const camelCasify = object =>
|
|
157
|
+
!!object &&
|
|
158
|
+
Object.keys(object).reduce((acc, key) => {
|
|
159
|
+
const camelCaseKey = key
|
|
160
|
+
.split('_')
|
|
161
|
+
.map((segment, index) =>
|
|
162
|
+
index ? segment.charAt(0).toUpperCase() + segment.slice(1) : segment
|
|
163
|
+
)
|
|
164
|
+
.join('')
|
|
165
|
+
acc[camelCaseKey] = object[key]
|
|
166
|
+
return acc
|
|
167
|
+
}, {})
|
|
168
|
+
|
|
169
|
+
const mapApp = app => ({
|
|
170
|
+
...app,
|
|
171
|
+
...camelCasify(app.attributes),
|
|
172
|
+
href: app.links && app.links.related
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const findAppInArray = (appSlug, apps) => apps.find(app => app.slug === appSlug)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import CozyClient from 'cozy-client'
|
|
2
|
+
import client from 'lib/stack-client.js'
|
|
3
|
+
import stack from 'lib/stack.js'
|
|
4
|
+
import { setDefaultApp } from './apps'
|
|
5
|
+
|
|
6
|
+
const cozyURL = 'https://test.mycozy.cloud'
|
|
7
|
+
const token = 'mytoken'
|
|
8
|
+
const fakeStackClient = {
|
|
9
|
+
uri: cozyURL,
|
|
10
|
+
token: { token }
|
|
11
|
+
}
|
|
12
|
+
describe('app reducer', () => {
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
jest.clearAllMocks()
|
|
15
|
+
const params = {
|
|
16
|
+
cozyClient: new CozyClient({ fakeStackClient }),
|
|
17
|
+
onCreate: function() {},
|
|
18
|
+
onDelete: function() {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
stack.init(params)
|
|
22
|
+
})
|
|
23
|
+
it('dispatch RECEIVE_HOME_APP if context has default redirection', async () => {
|
|
24
|
+
jest.spyOn(client.get, 'context').mockResolvedValue({
|
|
25
|
+
data: { attributes: { default_redirection: 'home/' } }
|
|
26
|
+
})
|
|
27
|
+
const dispatchMock = jest.fn(x => x)
|
|
28
|
+
|
|
29
|
+
const setted = setDefaultApp([{ slug: 'home' }])
|
|
30
|
+
await setted(dispatchMock)
|
|
31
|
+
expect(dispatchMock).toHaveBeenCalledWith({
|
|
32
|
+
homeApp: { slug: 'home' },
|
|
33
|
+
type: 'RECEIVE_HOME_APP'
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('dispatch RECEIVE_HOME_APP with hardcoded home if context has no default redirection', async () => {
|
|
38
|
+
jest.spyOn(client.get, 'context').mockResolvedValue({})
|
|
39
|
+
const dispatchMock = jest.fn(x => x)
|
|
40
|
+
|
|
41
|
+
const setted = setDefaultApp([{ slug: 'home' }])
|
|
42
|
+
await setted(dispatchMock)
|
|
43
|
+
expect(dispatchMock).toHaveBeenCalledWith({
|
|
44
|
+
homeApp: { slug: 'home' },
|
|
45
|
+
type: 'RECEIVE_HOME_APP'
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('dispatch nothing if the default redirection if not in the apps array', async () => {
|
|
50
|
+
jest.spyOn(client.get, 'context').mockResolvedValue({
|
|
51
|
+
data: { attributes: { default_redirection: 'drive/' } }
|
|
52
|
+
})
|
|
53
|
+
const dispatchMock = jest.fn(x => x)
|
|
54
|
+
|
|
55
|
+
const setted = setDefaultApp([{ slug: 'home' }])
|
|
56
|
+
await setted(dispatchMock)
|
|
57
|
+
expect(dispatchMock).not.toHaveBeenCalled()
|
|
58
|
+
})
|
|
59
|
+
})
|