├── .nvmrc
├── packages
├── ui
│ ├── .gitignore
│ ├── tslint.json
│ ├── src
│ │ ├── index.ts
│ │ ├── typings
│ │ │ └── generic.ts
│ │ ├── icon
│ │ │ └── index.tsx
│ │ ├── loader
│ │ │ └── index.tsx
│ │ ├── badge
│ │ │ ├── ProBadge.tsx
│ │ │ └── index.tsx
│ │ └── headline
│ │ │ └── index.tsx
│ ├── tsconfig.json
│ └── package.json
├── forms
│ ├── .gitignore
│ ├── tslint.json
│ ├── src
│ │ ├── index.ts
│ │ ├── error
│ │ │ ├── styles.ts
│ │ │ └── index.tsx
│ │ ├── label
│ │ │ ├── styles.ts
│ │ │ └── index.tsx
│ │ ├── typings
│ │ │ └── generic.ts
│ │ ├── wrapper
│ │ │ └── index.tsx
│ │ ├── input
│ │ │ └── scorePassword.ts
│ │ └── textarea
│ │ │ └── styles.ts
│ ├── tsconfig.json
│ └── package.json
├── theme
│ ├── .gitignore
│ ├── tslint.json
│ ├── README.md
│ ├── tsconfig.json
│ ├── src
│ │ ├── index.ts
│ │ └── themes
│ │ │ └── legacy
│ │ │ └── index.ts
│ └── package.json
├── typings
│ ├── types
│ │ ├── react-jss.d.ts
│ │ ├── mobx-react-form.d.ts
│ │ ├── react-html-attributes.d.ts
│ │ └── react-loader.d.ts
│ └── package.json
└── misty.yml
├── src
├── i18n
│ ├── locales
│ │ └── whitelist_en-US.json
│ ├── manage-translations.js
│ ├── translations.js
│ └── globalMessages.js
├── dev-app-update.yml
├── actions
│ ├── requests.js
│ ├── news.js
│ ├── recipePreview.js
│ ├── recipe.js
│ ├── ui.js
│ ├── settings.js
│ ├── payment.js
│ ├── app.js
│ ├── user.js
│ ├── lib
│ │ └── actions.js
│ └── index.js
├── features
│ ├── webControls
│ │ └── constants.js
│ ├── todos
│ │ ├── constants.js
│ │ ├── actions.js
│ │ ├── preload.js
│ │ └── index.js
│ ├── basicAuth
│ │ ├── styles.js
│ │ ├── mainIpcHandler.js
│ │ ├── Form.js
│ │ └── index.js
│ ├── desktopCapturer
│ │ ├── config.js
│ │ └── index.js
│ ├── delayApp
│ │ ├── store.js
│ │ └── api.js
│ ├── planSelection
│ │ ├── actions.js
│ │ └── api.js
│ ├── announcements
│ │ ├── actions.js
│ │ ├── index.js
│ │ └── api.js
│ ├── trialStatusBar
│ │ ├── actions.js
│ │ ├── index.js
│ │ └── components
│ │ │ └── ProgressBar.js
│ ├── utils
│ │ ├── ActionBinding.js
│ │ └── FeatureStore.js
│ ├── workspaces
│ │ ├── models
│ │ │ └── Workspace.js
│ │ ├── actions.js
│ │ ├── index.js
│ │ ├── components
│ │ │ └── WorkspaceItem.js
│ │ └── containers
│ │ │ └── WorkspacesScreen.js
│ ├── communityRecipes
│ │ ├── store.js
│ │ └── index.js
│ ├── serviceLimit
│ │ ├── index.js
│ │ └── store.js
│ └── spellchecker
│ │ └── index.js
├── assets
│ ├── images
│ │ ├── sm.png
│ │ ├── emoji
│ │ │ ├── sad.png
│ │ │ ├── star.png
│ │ │ └── dontknow.png
│ │ ├── tray
│ │ │ ├── darwin
│ │ │ │ ├── tray.png
│ │ │ │ ├── tray@2x.png
│ │ │ │ ├── tray-unread.png
│ │ │ │ └── tray-unread@2x.png
│ │ │ ├── linux
│ │ │ │ ├── tray.png
│ │ │ │ ├── tray@2x.png
│ │ │ │ ├── tray-unread.png
│ │ │ │ └── tray-unread@2x.png
│ │ │ ├── win32
│ │ │ │ ├── tray.ico
│ │ │ │ └── tray-unread.ico
│ │ │ └── darwin-dark
│ │ │ │ ├── tray.png
│ │ │ │ ├── tray@2x.png
│ │ │ │ ├── tray-active.png
│ │ │ │ ├── tray-unread.png
│ │ │ │ ├── tray-active@2x.png
│ │ │ │ ├── tray-unread@2x.png
│ │ │ │ ├── tray-unread-active.png
│ │ │ │ └── tray-unread-active@2x.png
│ │ └── taskbar
│ │ │ └── win32
│ │ │ ├── display.ico
│ │ │ ├── taskbar-1.ico
│ │ │ ├── taskbar-10.ico
│ │ │ ├── taskbar-2.ico
│ │ │ ├── taskbar-3.ico
│ │ │ ├── taskbar-4.ico
│ │ │ ├── taskbar-5.ico
│ │ │ ├── taskbar-6.ico
│ │ │ ├── taskbar-7.ico
│ │ │ ├── taskbar-8.ico
│ │ │ ├── taskbar-9.ico
│ │ │ └── taskbar-alert.ico
│ └── fonts
│ │ ├── OpenSans-Bold.ttf
│ │ ├── OpenSans-Light.ttf
│ │ ├── OpenSans-Regular.ttf
│ │ ├── OpenSans-BoldItalic.ttf
│ │ ├── OpenSans-ExtraBold.ttf
│ │ └── OpenSans-ExtraBoldItalic.ttf
├── styles
│ ├── config.scss
│ ├── mixins.scss
│ ├── invite.scss
│ ├── status-bar-target-url.scss
│ ├── subscription-popup.scss
│ ├── tooltip.scss
│ ├── util.scss
│ ├── badge.scss
│ ├── searchInput.scss
│ ├── services.scss
│ ├── subscription.scss
│ ├── radio.scss
│ ├── fonts.scss
│ ├── main.scss
│ ├── infobox.scss
│ ├── toggle.scss
│ ├── info-bar.scss
│ ├── service-table.scss
│ ├── type.scss
│ ├── content-tabs.scss
│ ├── animations.scss
│ ├── recipes.scss
│ └── welcome.scss
├── helpers
│ ├── asar-helpers.js
│ ├── array-helpers.js
│ ├── async-helpers.js
│ ├── routing-helpers.js
│ ├── url-helpers.js
│ ├── visibility-helper.js
│ ├── service-helpers.js
│ ├── password-helpers.js
│ ├── recipe-helpers.js
│ ├── userAgent-helpers.js
│ ├── plan-helpers.js
│ └── i18n-helpers.js
├── electron
│ ├── exception.js
│ ├── deepLinking.js
│ ├── ipc-api
│ │ ├── focusState.js
│ │ ├── settings.js
│ │ ├── serviceCache.js
│ │ ├── macOSPermissions.ts
│ │ ├── fullscreen.js
│ │ ├── cld.js
│ │ ├── index.js
│ │ ├── subscriptionWindow.js
│ │ └── desktopCapturer.ts
│ ├── windowUtils.js
│ └── Settings.js
├── components
│ ├── ui
│ │ ├── Tabs
│ │ │ ├── index.js
│ │ │ └── TabItem.js
│ │ ├── WebviewLoader
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── AppLoader
│ │ │ └── styles.js
│ │ ├── FullscreenLoader
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── Modal
│ │ │ └── styles.js
│ │ ├── StatusBarTargetUrl.js
│ │ ├── PremiumFeatureContainer
│ │ │ └── styles.js
│ │ ├── FeatureItem.js
│ │ ├── Loader.js
│ │ ├── effects
│ │ │ └── Appear.js
│ │ └── ServiceIcon.js
│ ├── util
│ │ └── ErrorBoundary
│ │ │ ├── styles.js
│ │ │ └── index.js
│ ├── services
│ │ └── content
│ │ │ ├── ErrorHandlers
│ │ │ └── styles.js
│ │ │ └── ServiceDisabled.js
│ └── settings
│ │ ├── recipes
│ │ └── RecipeItem.js
│ │ └── SettingsLayout.js
├── api
│ ├── AppApi.js
│ ├── FeaturesApi.js
│ ├── NewsApi.js
│ ├── PaymentApi.js
│ ├── RecipePreviewsApi.js
│ ├── RecipesApi.js
│ ├── LocalApi.js
│ ├── index.js
│ ├── ServicesApi.js
│ ├── utils
│ │ └── auth.js
│ ├── UserApi.js
│ └── server
│ │ └── LocalApi.js
├── models
│ ├── Plan.js
│ ├── RecipePreview.js
│ ├── Order.js
│ ├── News.js
│ └── User.js
├── prop-types.js
├── webview
│ ├── spellchecker.js
│ ├── zoom.js
│ ├── desktopCapturer.js
│ ├── darkmode.js
│ └── notifications.js
├── configVanilla.js
├── stores
│ ├── lib
│ │ ├── Reaction.js
│ │ └── Store.js
│ ├── GlobalErrorStore.js
│ ├── RecipePreviewsStore.js
│ └── NewsStore.js
├── containers
│ ├── auth
│ │ ├── InviteScreen.js
│ │ ├── WelcomeScreen.js
│ │ ├── PasswordScreen.js
│ │ ├── ImportScreen.js
│ │ ├── LoginScreen.js
│ │ └── SignupScreen.js
│ ├── subscription
│ │ └── SubscriptionPopupScreen.js
│ └── settings
│ │ ├── InviteScreen.js
│ │ └── SettingsWindow.js
├── lib
│ ├── Form.js
│ ├── analytics.js
│ └── TouchBar.js
├── I18n.js
├── environment.js
└── theme
│ └── default
│ └── legacy.js
├── .eslintignore
├── uidev
├── tslint.json
├── src
│ ├── stores
│ │ ├── index.ts
│ │ └── stories.ts
│ ├── index.tsx
│ ├── app.html
│ ├── stories
│ │ ├── loader.stories.tsx
│ │ ├── icon.stories.tsx
│ │ ├── badge.stories.tsx
│ │ ├── textarea.stories.tsx
│ │ └── headline.stories.tsx
│ └── withTheme
│ │ └── index.tsx
├── tsconfig.json
└── webpack.config.js
├── jest.config.js
├── .npmrc
├── types.d.ts
├── misty.yml
├── docs
├── example-feature
│ ├── api.js
│ ├── state.js
│ ├── actions.js
│ ├── store.js
│ └── index.js
└── linux.md
├── .editorconfig
├── .gitignore
├── lerna.json
├── tslint.json
├── tsconfig.json
├── .vscode
└── tasks.json
├── .github
├── FEATURE_PROPOSAL_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── stale.yml
├── PULL_REQUEST_TEMPLATE.md
└── ISSUE_TEMPLATE.md
├── tsconfig.settings.json
├── webpack.config.base.js
├── .babelrc
└── CODE_OF_CONDUCT.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 18.0.0
2 |
--------------------------------------------------------------------------------
/packages/ui/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | lib
3 |
--------------------------------------------------------------------------------
/src/i18n/locales/whitelist_en-US.json:
--------------------------------------------------------------------------------
1 | [
2 | ]
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/
2 | out/
3 | packages/*/lib
4 |
--------------------------------------------------------------------------------
/packages/forms/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | lib
3 |
--------------------------------------------------------------------------------
/packages/theme/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | lib
3 |
--------------------------------------------------------------------------------
/uidev/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tslint.json"
3 | }
4 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | roots: ['src'],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/typings/types/react-jss.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-jss';
2 |
--------------------------------------------------------------------------------
/packages/ui/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json"
3 | }
4 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact = true
2 | legacy-peer-deps = true
3 | # python = python2.7
--------------------------------------------------------------------------------
/packages/forms/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/theme/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/typings/types/mobx-react-form.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'mobx-react-form';
2 |
--------------------------------------------------------------------------------
/src/dev-app-update.yml:
--------------------------------------------------------------------------------
1 | owner: meetfranz
2 | repo: franz
3 | provider: github
4 |
--------------------------------------------------------------------------------
/types.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module 'react-jss'
4 |
--------------------------------------------------------------------------------
/src/actions/requests.js:
--------------------------------------------------------------------------------
1 | export default {
2 | retryRequiredRequests: {},
3 | };
4 |
--------------------------------------------------------------------------------
/packages/typings/types/react-html-attributes.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-html-attributes';
2 |
--------------------------------------------------------------------------------
/src/features/webControls/constants.js:
--------------------------------------------------------------------------------
1 | export const CUSTOM_WEBSITE_ID = 'franz-custom-website';
2 |
--------------------------------------------------------------------------------
/src/assets/images/sm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/sm.png
--------------------------------------------------------------------------------
/src/assets/images/emoji/sad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/emoji/sad.png
--------------------------------------------------------------------------------
/src/assets/images/emoji/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/emoji/star.png
--------------------------------------------------------------------------------
/misty.yml:
--------------------------------------------------------------------------------
1 | code:
2 | cmd: npm run dev
3 |
4 | app:
5 | cmd: npx electron ./build
6 | waitOn: http://localhost:8000
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-Bold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-Light.ttf
--------------------------------------------------------------------------------
/src/assets/images/emoji/dontknow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/emoji/dontknow.png
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin/tray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin/tray.png
--------------------------------------------------------------------------------
/src/assets/images/tray/linux/tray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/linux/tray.png
--------------------------------------------------------------------------------
/src/assets/images/tray/win32/tray.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/win32/tray.ico
--------------------------------------------------------------------------------
/src/styles/config.scss:
--------------------------------------------------------------------------------
1 | @import './colors.scss';
2 |
3 | $windows-title-bar-height: to-number($raw-windows-title-bar-height);
--------------------------------------------------------------------------------
/docs/example-feature/api.js:
--------------------------------------------------------------------------------
1 | export default {
2 | async getName() {
3 | return Promise.resolve('Franz');
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-BoldItalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-ExtraBold.ttf
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin/tray@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin/tray@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/linux/tray@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/linux/tray@2x.png
--------------------------------------------------------------------------------
/src/helpers/asar-helpers.js:
--------------------------------------------------------------------------------
1 | export function asarPath(dir = '') {
2 | return dir.replace('app.asar', 'app.asar.unpacked');
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/display.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/display.ico
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray.png
--------------------------------------------------------------------------------
/uidev/src/stores/index.ts:
--------------------------------------------------------------------------------
1 | import { storyStore } from './stories';
2 |
3 | export const store = {
4 | stories: storyStore,
5 | };
6 |
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-1.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-1.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-10.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-10.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-2.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-2.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-3.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-3.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-4.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-4.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-5.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-5.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-6.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-6.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-7.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-7.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-8.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-8.ico
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-9.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-9.ico
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin/tray-unread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin/tray-unread.png
--------------------------------------------------------------------------------
/src/assets/images/tray/linux/tray-unread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/linux/tray-unread.png
--------------------------------------------------------------------------------
/src/assets/images/tray/win32/tray-unread.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/win32/tray-unread.ico
--------------------------------------------------------------------------------
/src/electron/exception.js:
--------------------------------------------------------------------------------
1 | process.on('uncaughtException', (err) => {
2 | // handle the error safely
3 | console.error(err);
4 | });
5 |
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin/tray-unread@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin/tray-unread@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/linux/tray-unread@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/linux/tray-unread@2x.png
--------------------------------------------------------------------------------
/src/assets/images/taskbar/win32/taskbar-alert.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/taskbar/win32/taskbar-alert.ico
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-active.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-unread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-unread.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-active@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-active@2x.png
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-unread@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-unread@2x.png
--------------------------------------------------------------------------------
/src/components/ui/Tabs/index.js:
--------------------------------------------------------------------------------
1 | import Tabs from './Tabs';
2 | import TabItem from './TabItem';
3 |
4 | export default Tabs;
5 |
6 | export { TabItem };
7 |
--------------------------------------------------------------------------------
/src/actions/news.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | hide: {
5 | newsId: PropTypes.string.isRequired,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/features/todos/constants.js:
--------------------------------------------------------------------------------
1 | export const IPC = {
2 | TODOS_HOST_CHANNEL: 'TODOS_HOST_CHANNEL',
3 | TODOS_CLIENT_CHANNEL: 'TODOS_CLIENT_CHANNEL',
4 | };
5 |
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-unread-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-unread-active.png
--------------------------------------------------------------------------------
/src/helpers/array-helpers.js:
--------------------------------------------------------------------------------
1 | export const shuffleArray = arr => arr
2 | .map(a => [Math.random(), a])
3 | .sort((a, b) => a[0] - b[0])
4 | .map(a => a[1]);
5 |
--------------------------------------------------------------------------------
/src/actions/recipePreview.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | search: {
5 | needle: PropTypes.string.isRequired,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/assets/images/tray/darwin-dark/tray-unread-active@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meetfranz/franz/HEAD/src/assets/images/tray/darwin-dark/tray-unread-active@2x.png
--------------------------------------------------------------------------------
/src/helpers/async-helpers.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 |
3 | export function sleep(ms = 0) {
4 | return new Promise(r => setTimeout(r, ms));
5 | }
6 |
--------------------------------------------------------------------------------
/packages/theme/README.md:
--------------------------------------------------------------------------------
1 | # `theme`
2 |
3 | > TODO: description
4 |
5 | ## Usage
6 |
7 | ```
8 | const theme = require('theme');
9 |
10 | // TODO: DEMONSTRATE API
11 | ```
12 |
--------------------------------------------------------------------------------
/packages/misty.yml:
--------------------------------------------------------------------------------
1 | theme:
2 | cwd: ./theme
3 | cmd: npm run dev
4 |
5 | forms:
6 | cwd: ./forms
7 | cmd: npm run dev
8 |
9 | ui:
10 | cwd: ./ui
11 | cmd: npm run dev
12 |
--------------------------------------------------------------------------------
/src/helpers/routing-helpers.js:
--------------------------------------------------------------------------------
1 | import RouteParser from 'route-parser';
2 |
3 | // eslint-disable-next-line
4 | export const matchRoute = (pattern, path) => new RouteParser(pattern).match(path);
5 |
--------------------------------------------------------------------------------
/packages/theme/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.settings.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src",
6 | "allowJs": true
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/src/api/AppApi.js:
--------------------------------------------------------------------------------
1 | export default class AppApi {
2 | constructor(server) {
3 | this.server = server;
4 | }
5 |
6 | health() {
7 | return this.server.healthCheck();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/mixins.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | @mixin formLabel {
4 | color: $theme-gray-light;
5 | display: block;
6 | margin-bottom: 5px;
7 | order: 0;
8 | width: 100%;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/forms/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Input } from './input';
2 | export { Textarea } from './textarea';
3 | export { Toggle } from './toggle';
4 | export { Button } from './button';
5 | export { Select } from './select';
6 |
--------------------------------------------------------------------------------
/src/actions/recipe.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | install: {
5 | recipeId: PropTypes.string.isRequired,
6 | update: PropTypes.bool,
7 | },
8 | update: {},
9 | };
10 |
--------------------------------------------------------------------------------
/src/styles/invite.scss:
--------------------------------------------------------------------------------
1 | .invite__form {
2 | align-items: center;
3 | align-self: center;
4 | justify-content: center;
5 | }
6 |
7 | .invite__embed { text-align: center; }
8 | .invite__embed--button { width: 100%; }
9 |
--------------------------------------------------------------------------------
/src/electron/deepLinking.js:
--------------------------------------------------------------------------------
1 | export default function handleDeepLink(window, rawUrl) {
2 | const url = rawUrl.replace('franz://', '');
3 |
4 | if (!url) return;
5 |
6 | window.webContents.send('navigateFromDeepLink', { url });
7 | }
8 |
--------------------------------------------------------------------------------
/uidev/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { App } from './app';
4 |
5 | const app = () => (
6 |
7 | );
8 |
9 | render(app(), document.getElementById('root'));
10 |
--------------------------------------------------------------------------------
/packages/ui/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Icon } from './icon';
2 | export { Infobox } from './infobox';
3 | export * from './headline';
4 | export { Loader } from './loader';
5 | export { Badge } from './badge';
6 | export { ProBadge } from './badge/ProBadge';
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/packages/forms/src/error/styles.ts:
--------------------------------------------------------------------------------
1 | import { Theme } from '../../../theme/lib';
2 |
3 | export default (theme: Theme) => ({
4 | message: {
5 | color: theme.brandDanger,
6 | margin: '5px 0 0',
7 | fontSize: theme.uiFontSize,
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/packages/forms/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.settings.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src"
6 | },
7 | "references": [
8 | {
9 | "path": "../theme"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.settings.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src"
6 | },
7 | "references": [
8 | {
9 | "path": "../theme"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | flow-typed
4 | out
5 | .DS_Store
6 | .idea
7 | build
8 | .tmp
9 | .stage
10 | .env
11 | yarn-error.log
12 | npm-debug.log*
13 | lerna-debug.log
14 | uidev/lib
15 | *.tsbuildinfo
16 | src/i18n/messages/
17 | extensions
18 |
--------------------------------------------------------------------------------
/src/components/ui/WebviewLoader/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | component: {
3 | background: theme.colorWebviewLoaderBackground,
4 | padding: 20,
5 | width: 'auto',
6 | margin: [0, 'auto'],
7 | borderRadius: 6,
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/src/models/Plan.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export default class Plan {
4 | month = {
5 | id: '',
6 | price: 0,
7 | }
8 |
9 | year = {
10 | id: '',
11 | price: 0,
12 | }
13 |
14 | constructor(data) {
15 | Object.assign(this, data);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/uidev/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIDev
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/api/FeaturesApi.js:
--------------------------------------------------------------------------------
1 | export default class FeaturesApi {
2 | constructor(server) {
3 | this.server = server;
4 | }
5 |
6 | default() {
7 | return this.server.getDefaultFeatures();
8 | }
9 |
10 | features() {
11 | return this.server.getFeatures();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/docs/linux.md:
--------------------------------------------------------------------------------
1 | # Linux distribution specific dependencies
2 |
3 | ## Debian/Ubuntu
4 | ```bash
5 | $ apt install libx11-dev libxext-dev libxss-dev libxkbfile-dev
6 | ```
7 |
8 | ## Fedora
9 | ```bash
10 | $ dnf install libX11-devel libXext-devel libXScrnSaver-devel libxkbfile-devel
11 | ```
12 |
--------------------------------------------------------------------------------
/src/features/basicAuth/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | container: {
3 | padding: 20,
4 | color: theme.colorText,
5 | },
6 | buttons: {
7 | display: 'flex',
8 | justifyContent: 'space-between',
9 | },
10 | form: {
11 | marginTop: 15,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/src/features/desktopCapturer/config.js:
--------------------------------------------------------------------------------
1 | export const REQUEST_DESKTOP_CAPTURER_SOURCES_IPC_KEY = 'get-desktop-capturer-sources';
2 | export const RELAY_DESKTOP_CAPTURER_SOURCES_IPC_KEY = 'relay-desktop-capturer-sources';
3 | export const SET_DESKTOP_CAPTURER_SOURCES_IPC_KEY = 'set-desktop-capturer-sources';
4 |
--------------------------------------------------------------------------------
/packages/forms/src/label/styles.ts:
--------------------------------------------------------------------------------
1 | import { Theme } from '../../../theme/lib';
2 |
3 | export default (theme: Theme) => ({
4 | content: {},
5 | label: {
6 | color: theme.labelColor,
7 | fontSize: theme.uiFontSize,
8 | },
9 | hasError: {
10 | color: theme.brandDanger,
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/src/actions/ui.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | openSettings: {
5 | path: PropTypes.string,
6 | },
7 | closeSettings: {},
8 | toggleServiceUpdatedInfoBar: {
9 | visible: PropTypes.bool,
10 | },
11 | hideServices: {},
12 | showServices: {},
13 | };
14 |
--------------------------------------------------------------------------------
/src/api/NewsApi.js:
--------------------------------------------------------------------------------
1 | export default class NewsApi {
2 | constructor(server, local) {
3 | this.server = server;
4 | this.local = local;
5 | }
6 |
7 | latest() {
8 | return this.server.getLatestNews();
9 | }
10 |
11 | hide(id) {
12 | return this.server.hideNews(id);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": [
3 | "packages/theme",
4 | "packages/forms",
5 | "packages/ui",
6 | "packages/typings"
7 | ],
8 | "version": "independent",
9 | "ignoreChanges": [
10 | "**/*.md",
11 | "**/.eslintrc.{js,json,yaml,yml}",
12 | "**/package-lock.json"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/uidev/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.settings.json",
3 | "compilerOptions": {
4 | "baseUrl": "..",
5 | "outDir": "lib",
6 | "rootDir": "src",
7 | },
8 | "references": [{
9 | "path": "../packages/theme"
10 | },
11 | {
12 | "path": "../packages/forms"
13 | }]
14 | }
15 |
--------------------------------------------------------------------------------
/packages/ui/src/typings/generic.ts:
--------------------------------------------------------------------------------
1 | import { Theme } from '@meetfranz/theme/lib';
2 |
3 | export interface IWithStyle {
4 | classes: any;
5 | theme: Theme;
6 | }
7 |
8 | export type Merge = Omit> & N;
9 | export type Omit = Pick>;
10 |
--------------------------------------------------------------------------------
/src/actions/settings.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | update: {
5 | type: PropTypes.string.isRequired,
6 | data: PropTypes.object.isRequired,
7 | },
8 | remove: {
9 | type: PropTypes.string.isRequired,
10 | key: PropTypes.string.isRequired,
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/focusState.js:
--------------------------------------------------------------------------------
1 | export default (params) => {
2 | params.mainWindow.on('focus', () => {
3 | params.mainWindow.webContents.send('isWindowFocused', true);
4 | });
5 |
6 | params.mainWindow.on('blur', () => {
7 | params.mainWindow.webContents.send('isWindowFocused', false);
8 | });
9 | };
10 |
--------------------------------------------------------------------------------
/src/features/basicAuth/mainIpcHandler.js:
--------------------------------------------------------------------------------
1 | const debug = require('debug')('Franz:feature:basicAuth:main');
2 |
3 | export default function mainIpcHandler(mainWindow, authInfo) {
4 | debug('Sending basic auth call', authInfo);
5 |
6 | mainWindow.webContents.send('feature:basic-auth', {
7 | authInfo,
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/src/api/PaymentApi.js:
--------------------------------------------------------------------------------
1 | export default class PaymentApi {
2 | constructor(server, local) {
3 | this.server = server;
4 | this.local = local;
5 | }
6 |
7 | plans() {
8 | return this.server.getPlans();
9 | }
10 |
11 | getHostedPage(planId) {
12 | return this.server.getHostedPage(planId);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint-config-airbnb"],
3 | "rules": {
4 | "import-name": false,
5 | "variable-name": false,
6 | "class-name": false,
7 | "prefer-array-literal": false,
8 | "semicolon": [true, "always"],
9 | "max-line-length": false,
10 | "ordered-imports": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/util/ErrorBoundary/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | component: {
3 | display: 'flex',
4 | width: '100%',
5 | alignItems: 'center',
6 | justifyContent: 'center',
7 | flexDirection: 'column',
8 | },
9 | title: {
10 | fontSize: 20,
11 | color: theme.colorText,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/src/i18n/manage-translations.js:
--------------------------------------------------------------------------------
1 | require('@babel/register');
2 | const manageTranslations = require('react-intl-translations-manager').default;
3 |
4 | manageTranslations({
5 | messagesDirectory: 'src/i18n/messages',
6 | translationsDirectory: 'src/i18n/locales',
7 | singleMessagesFile: true,
8 | languages: ['en-US'],
9 | });
10 |
--------------------------------------------------------------------------------
/src/components/ui/AppLoader/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | component: {
3 | color: '#FFF',
4 | },
5 | slogan: {
6 | display: 'block',
7 | opacity: 0,
8 | transition: 'opacity 1s ease',
9 | position: 'absolute',
10 | textAlign: 'center',
11 | width: '100%',
12 | },
13 | visible: {
14 | opacity: 1,
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/features/delayApp/store.js:
--------------------------------------------------------------------------------
1 | import {
2 | computed,
3 | } from 'mobx';
4 |
5 | import { FeatureStore } from '../utils/FeatureStore';
6 | import { getPoweredByRequest } from './api';
7 |
8 | export class DelayAppStore extends FeatureStore {
9 | @computed get poweredBy() {
10 | return getPoweredByRequest.execute().result || {};
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/features/planSelection/actions.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions';
3 |
4 | export const planSelectionActions = createActionsFromDefinitions({
5 | downgradeAccount: {},
6 | hideOverlay: {},
7 | }, PropTypes.checkPropTypes);
8 |
9 | export default planSelectionActions;
10 |
--------------------------------------------------------------------------------
/src/actions/payment.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | createHostedPage: {
5 | planId: PropTypes.string.isRequired,
6 | },
7 | upgradeAccount: {
8 | planId: PropTypes.string.isRequired,
9 | onCloseWindow: PropTypes.func,
10 | overrideParent: PropTypes.number,
11 | },
12 | createDashboardUrl: {},
13 | };
14 |
--------------------------------------------------------------------------------
/docs/example-feature/state.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 |
3 | const defaultState = {
4 | name: null,
5 | isFeatureActive: false,
6 | };
7 |
8 | export const exampleFeatureState = observable(defaultState);
9 |
10 | export function resetState() {
11 | Object.assign(exampleFeatureState, defaultState);
12 | }
13 |
14 | export default exampleFeatureState;
15 |
--------------------------------------------------------------------------------
/src/features/announcements/actions.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions';
3 |
4 | export const announcementActions = createActionsFromDefinitions({
5 | show: {
6 | targetVersion: PropTypes.string,
7 | },
8 | }, PropTypes.checkPropTypes);
9 |
10 | export default announcementActions;
11 |
--------------------------------------------------------------------------------
/docs/example-feature/actions.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { createActionsFromDefinitions } from '../../src/actions/lib/actions';
3 |
4 | export const exampleFeatureActions = createActionsFromDefinitions({
5 | greet: {
6 | name: PropTypes.string.isRequired,
7 | },
8 | }, PropTypes.checkPropTypes);
9 |
10 | export default exampleFeatureActions;
11 |
--------------------------------------------------------------------------------
/src/styles/status-bar-target-url.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .status-bar-target-url {
4 | background: $theme-gray-lighter;
5 | border-top-left-radius: 5px;
6 | bottom: 0;
7 | box-shadow: 0 0 8px rgba(black, .2);
8 | color: $theme-gray-dark;
9 | font-size: 12px;
10 | height: auto;
11 | right: 0;
12 | padding: 4px;
13 | position: absolute;
14 | }
15 |
--------------------------------------------------------------------------------
/src/features/basicAuth/Form.js:
--------------------------------------------------------------------------------
1 | import Form from '../../lib/Form';
2 |
3 | export default new Form({
4 | fields: {
5 | user: {
6 | label: 'user',
7 | placeholder: 'Username',
8 | value: '',
9 | },
10 | password: {
11 | label: 'Password',
12 | placeholder: 'Password',
13 | value: '',
14 | type: 'password',
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/uidev/src/stories/loader.stories.tsx:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 | import { observer } from 'mobx-react';
3 | import React from 'react';
4 | import uuid from 'uuid/v4';
5 |
6 | import { Loader } from '@meetfranz/ui';
7 | import { storiesOf } from '../stores/stories';
8 |
9 | storiesOf('Loader')
10 | .add('Basic', () => (
11 | <>
12 |
13 | >
14 | ));
15 |
--------------------------------------------------------------------------------
/src/api/RecipePreviewsApi.js:
--------------------------------------------------------------------------------
1 | export default class ServicesApi {
2 | constructor(server) {
3 | this.server = server;
4 | }
5 |
6 | all() {
7 | return this.server.getRecipePreviews();
8 | }
9 |
10 | featured() {
11 | return this.server.getFeaturedRecipePreviews();
12 | }
13 |
14 | search(needle) {
15 | return this.server.searchRecipePreviews(needle);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/api/RecipesApi.js:
--------------------------------------------------------------------------------
1 | export default class RecipesApi {
2 | constructor(server) {
3 | this.server = server;
4 | }
5 |
6 | all() {
7 | return this.server.getInstalledRecipes();
8 | }
9 |
10 | install(recipeId) {
11 | return this.server.getRecipePackage(recipeId);
12 | }
13 |
14 | update(recipes) {
15 | return this.server.getRecipeUpdates(recipes);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/models/RecipePreview.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export default class RecipePreview {
4 | id = '';
5 |
6 | name = '';
7 |
8 | icon = '';
9 |
10 | // TODO: check if this isn't replaced by `icons`
11 | featured = false;
12 |
13 | constructor(data) {
14 | if (!data.id) {
15 | throw Error('RecipePreview requires Id');
16 | }
17 |
18 | Object.assign(this, data);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "node",
4 | "target": "esnext",
5 | "module": "commonjs",
6 | "outDir": ".tstmp",
7 | "rootDir": "./src",
8 | "allowJs": true,
9 | "strict": false,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "experimentalDecorators": true
13 | },
14 | "include": [
15 | "src/**/*"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/src/prop-types.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | // eslint-disable-next-line
4 | export const oneOrManyChildElements = PropTypes.oneOfType([
5 | PropTypes.arrayOf(PropTypes.element),
6 | PropTypes.element,
7 | PropTypes.array,
8 | ]);
9 |
10 | export const globalError = PropTypes.shape({
11 | status: PropTypes.number,
12 | message: PropTypes.string,
13 | code: PropTypes.string,
14 | });
15 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "npm",
6 | "script": "dev",
7 | "group": {
8 | "kind": "build",
9 | "isDefault": true
10 | }
11 | },
12 | {
13 | "type": "npm",
14 | "script": "lint",
15 | "group": "test"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/src/electron/windowUtils.js:
--------------------------------------------------------------------------------
1 | /* eslint import/prefer-default-export: 0 */
2 |
3 | import { screen } from 'electron';
4 |
5 | export function isPositionValid(position) {
6 | const displays = screen.getAllDisplays();
7 | const { x, y } = position;
8 | return displays.some(({
9 | workArea,
10 | }) => x >= workArea.x && x <= workArea.x + workArea.width && y >= workArea.y && y <= workArea.y + workArea.height);
11 | }
12 |
--------------------------------------------------------------------------------
/src/i18n/translations.js:
--------------------------------------------------------------------------------
1 | import { APP_LOCALES } from './languages';
2 |
3 | const translations = [];
4 | Object.keys(APP_LOCALES).forEach((key) => {
5 | try {
6 | const translation = require(`./locales/${key}.json`); // eslint-disable-line
7 | translations[key] = translation;
8 | } catch (err) {
9 | console.warn(`Can't find translations for ${key}`);
10 | }
11 | });
12 |
13 | module.exports = translations;
14 |
--------------------------------------------------------------------------------
/src/helpers/url-helpers.js:
--------------------------------------------------------------------------------
1 | import { URL } from 'url';
2 |
3 | import { ALLOWED_PROTOCOLS } from '../config';
4 |
5 | const debug = require('debug')('Franz:Helpers:url');
6 |
7 | export function isValidExternalURL(url) {
8 | const parsedUrl = new URL(url);
9 |
10 | const isAllowed = ALLOWED_PROTOCOLS.includes(parsedUrl.protocol);
11 |
12 | debug('protocol check is', isAllowed, 'for:', url);
13 |
14 | return isAllowed;
15 | }
16 |
--------------------------------------------------------------------------------
/src/webview/spellchecker.js:
--------------------------------------------------------------------------------
1 | import { SPELLCHECKER_LOCALES } from '../i18n/languages';
2 |
3 | export function getSpellcheckerLocaleByFuzzyIdentifier(identifier) {
4 | const locales = Object.keys(SPELLCHECKER_LOCALES).filter(key => key.toLocaleLowerCase() === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase());
5 |
6 | if (locales.length >= 1) {
7 | return locales[0];
8 | }
9 |
10 | return null;
11 | }
12 |
--------------------------------------------------------------------------------
/uidev/src/stories/icon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { mdiAccountCircle } from '@mdi/js';
2 | import React from 'react';
3 |
4 | import { Icon } from '@meetfranz/ui';
5 | import { storiesOf } from '../stores/stories';
6 |
7 | storiesOf('Icon')
8 | .add('Basic', () => (
9 | <>
10 |
11 |
12 |
13 | >
14 | ));
15 |
--------------------------------------------------------------------------------
/src/styles/subscription-popup.scss:
--------------------------------------------------------------------------------
1 | .subscription-popup {
2 | height: 100%;
3 |
4 | &__content { height: calc(100% - 60px); }
5 | &__webview {
6 | height: 100%;
7 | background: #FFF;
8 | }
9 |
10 | &__toolbar {
11 | background: $theme-gray-lightest;
12 | border-top: 1px solid $theme-gray-lighter;
13 | display: flex;
14 | height: 60px;
15 | justify-content: space-between;
16 | padding: 10px;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/ui/Tabs/TabItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 |
3 | import { oneOrManyChildElements } from '../../../prop-types';
4 |
5 | export default class TabItem extends Component {
6 | static propTypes = {
7 | children: oneOrManyChildElements.isRequired,
8 | }
9 |
10 | render() {
11 | const { children } = this.props;
12 |
13 | return (
14 | {children}
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/styles/tooltip.scss:
--------------------------------------------------------------------------------
1 | .__react_component_tooltip {
2 | height: auto;
3 | padding: 4px !important;
4 | font-size: 8px !important;
5 | }
6 |
7 | .sidebar .__react_component_tooltip {
8 | width: $theme-sidebar-width - 4px !important;
9 | margin-top: -10px !important;
10 | margin-left: 2px !important;
11 |
12 | &.place-right {
13 | margin-left: 2px !important;
14 | }
15 |
16 | &.place-top {
17 | margin-top: 10px !important;
18 | }
19 | }
--------------------------------------------------------------------------------
/.github/FEATURE_PROPOSAL_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ### Feature Description
4 |
5 |
6 | ### Motivation and Context
7 |
12 |
13 | ### Mockups, Screenshots (if available):
14 |
15 |
--------------------------------------------------------------------------------
/packages/forms/src/typings/generic.ts:
--------------------------------------------------------------------------------
1 | import { Theme } from '@meetfranz/theme/lib';
2 |
3 | export interface IFormField {
4 | showLabel?: boolean;
5 | label?: string;
6 | error?: string;
7 | required?: boolean;
8 | noMargin?: boolean;
9 | }
10 |
11 | export interface IWithStyle {
12 | classes: any;
13 | theme: Theme;
14 | }
15 |
16 | export type Merge = Omit> & N;
17 | export type Omit = Pick>;
18 |
--------------------------------------------------------------------------------
/src/features/trialStatusBar/actions.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions';
3 |
4 | export const trialStatusBarActions = createActionsFromDefinitions({
5 | upgradeAccount: {
6 | planId: PropTypes.string.isRequired,
7 | onCloseWindow: PropTypes.func.isRequired,
8 | },
9 | downgradeAccount: {},
10 | hideOverlay: {},
11 | }, PropTypes.checkPropTypes);
12 |
13 | export default trialStatusBarActions;
14 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/settings.js:
--------------------------------------------------------------------------------
1 | import { ipcMain } from 'electron';
2 | import { GET_SETTINGS, SEND_SETTINGS } from '../../ipcChannels';
3 |
4 | export default (params) => {
5 | ipcMain.on(GET_SETTINGS, (event, type) => {
6 | event.sender.send(SEND_SETTINGS, {
7 | type,
8 | data: params.settings[type]?.allSerialized,
9 | });
10 | });
11 |
12 | ipcMain.on('updateAppSettings', (event, args) => {
13 | params.settings[args.type].set(args.data);
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/src/models/Order.js:
--------------------------------------------------------------------------------
1 | export default class Order {
2 | id = '';
3 |
4 | subscriptionId = '';
5 |
6 | name = '';
7 |
8 | invoiceUrl = '';
9 |
10 | price = '';
11 |
12 | date = '';
13 |
14 | constructor(data) {
15 | this.id = data.id;
16 | this.subscriptionId = data.subscriptionId;
17 | this.name = data.name || this.name;
18 | this.invoiceUrl = data.invoiceUrl || this.invoiceUrl;
19 | this.price = data.price || this.price;
20 | this.date = data.date || this.date;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/api/LocalApi.js:
--------------------------------------------------------------------------------
1 | export default class LocalApi {
2 | constructor(server, local) {
3 | this.server = server;
4 | this.local = local;
5 | }
6 |
7 | getAppSettings(type) {
8 | return this.local.getAppSettings(type);
9 | }
10 |
11 | updateAppSettings(type, data) {
12 | return this.local.updateAppSettings(type, data);
13 | }
14 |
15 | getAppCacheSize() {
16 | return this.local.getAppCacheSize();
17 | }
18 |
19 | clearAppCache() {
20 | return this.local.clearAppCache();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/serviceCache.js:
--------------------------------------------------------------------------------
1 | import { ipcMain } from 'electron';
2 |
3 | const debug = require('debug')('Franz:ipcApi:serviceCache');
4 |
5 | export default () => {
6 | ipcMain.handle('clearServiceCache', ({ sender: webContents }) => {
7 | debug('Clearing cache for service');
8 | const { session } = webContents;
9 |
10 | session.flushStorageData();
11 | session.clearStorageData({
12 | storages: ['appcache', 'serviceworkers', 'cachestorage', 'websql', 'indexdb'],
13 | });
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/src/models/News.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export default class News {
4 | id = '';
5 |
6 | message = '';
7 |
8 | meta = {};
9 |
10 | type = 'primary';
11 |
12 | sticky = false;
13 |
14 | constructor(data) {
15 | if (!data.id) {
16 | throw Error('News requires Id');
17 | }
18 |
19 | this.id = data.id;
20 | this.message = data.message || this.message;
21 | this.meta = data.meta || this.meta;
22 | this.type = data.type || this.type;
23 | this.sticky = data.sticky !== undefined ? data.sticky : this.sticky;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/features/desktopCapturer/index.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 |
3 | export { default as Component } from './Component';
4 |
5 | const debug = require('debug')('Franz:feature:desktopCapturer');
6 |
7 | const defaultState = {
8 | isModalVisible: false,
9 | sources: [],
10 | selectedSource: null,
11 | webview: null,
12 | };
13 |
14 | export const state = observable(defaultState);
15 |
16 | export default function initialize() {
17 | debug('Initialize shareFranz feature');
18 |
19 | window.franz.features.desktopCapturer = {
20 | state,
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/ui/FullscreenLoader/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | wrapper: {
3 | display: 'flex',
4 | alignItems: 'center',
5 | position: ({ isAbsolutePositioned }) => (isAbsolutePositioned ? 'absolute' : 'relative'),
6 | width: '100%',
7 | },
8 | component: {
9 | width: '100%',
10 | display: 'flex',
11 | flexDirection: 'column',
12 | alignItems: 'center',
13 | textAlign: 'center',
14 | height: 'auto',
15 | },
16 | title: {
17 | fontSize: 35,
18 | },
19 | content: {
20 | marginTop: 20,
21 | width: '100%',
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/packages/theme/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as darkThemeConfig from './themes/dark';
2 | import * as defaultThemeConfig from './themes/default';
3 | import * as legacyStyles from './themes/legacy';
4 |
5 | export enum ThemeType {
6 | default = 'default',
7 | dark = 'dark',
8 | }
9 |
10 | export function theme(themeId: ThemeType) {
11 | if (themeId === ThemeType.dark) {
12 | return Object.assign({}, defaultThemeConfig, darkThemeConfig, { legacyStyles });
13 | }
14 |
15 | return Object.assign({}, defaultThemeConfig, { legacyStyles });
16 | }
17 |
18 | export type Theme = typeof defaultThemeConfig;
19 |
--------------------------------------------------------------------------------
/packages/forms/src/error/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import injectSheet from 'react-jss';
3 |
4 | import styles from './styles';
5 |
6 | interface IProps {
7 | classes: any;
8 | message: string;
9 | }
10 |
11 | class ErrorComponent extends Component {
12 | render() {
13 | const {
14 | classes,
15 | message,
16 | } = this.props;
17 |
18 | return (
19 |
22 | {message}
23 |
24 | );
25 | }
26 | }
27 |
28 | export const Error = injectSheet(styles)(ErrorComponent);
29 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/macOSPermissions.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow, ipcMain } from 'electron';
2 | import { isMac } from '../../environment';
3 | import { CHECK_MACOS_PERMISSIONS } from '../../ipcChannels';
4 |
5 | export default ({ mainWindow }: { mainWindow: BrowserWindow }) => {
6 | // workaround to not break app on non macOS systems
7 | if (isMac) {
8 | ipcMain.on(CHECK_MACOS_PERMISSIONS, () => {
9 | // eslint-disable-next-line global-require
10 | const { default: askFormacOSPermissions } = require('../macOSPermissions');
11 | askFormacOSPermissions(mainWindow);
12 | });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/styles/util.scss:
--------------------------------------------------------------------------------
1 | .scroll-container {
2 | flex: 1;
3 | height: 100%;
4 | overflow-x: hidden;
5 | overflow-y: scroll;
6 | }
7 |
8 | .loader {
9 | display: block;
10 | height: 40px;
11 | position: relative;
12 | width: 100%;
13 | z-index: 9999;
14 | }
15 |
16 | .align-middle {
17 | display: flex;
18 | flex-direction: column;
19 | justify-content: center;
20 | }
21 |
22 | .pulsating {
23 | animation: pulse-animation 1s alternate infinite ease-in-out;
24 | }
25 |
26 | @keyframes pulse-animation {
27 | 0% {
28 | transform: scale(0.7)
29 | }
30 | 100% {
31 | transform: scale(1)
32 | }
33 | }
--------------------------------------------------------------------------------
/src/configVanilla.js:
--------------------------------------------------------------------------------
1 | export const DEFAULT_APP_SETTINGS_VANILLA = {
2 | autoLaunchInBackground: false,
3 | runInBackground: true,
4 | enableSystemTray: true,
5 | minimizeToSystemTray: false,
6 | showDisabledServices: true,
7 | showMessageBadgeWhenMuted: true,
8 | enableSpellchecking: true,
9 | spellcheckerLanguage: 'en-US',
10 | darkMode: process.type === 'renderer' ? window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches : false,
11 | locale: '',
12 | fallbackLocale: 'en-US',
13 | beta: false,
14 | isAppMuted: false,
15 | enableGPUAcceleration: true,
16 | serviceLimit: 5,
17 | };
18 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/fullscreen.js:
--------------------------------------------------------------------------------
1 | import { ipcMain } from "electron";
2 | import { TOGGLE_FULL_SCREEN } from "../../ipcChannels";
3 |
4 | export const UPDATE_FULL_SCREEN_STATUS = 'set-full-screen-status';
5 |
6 | export default ({ mainWindow }) => {
7 | ipcMain.on(TOGGLE_FULL_SCREEN, (e) => {
8 | mainWindow.setFullScreen(!mainWindow.isFullScreen());
9 | })
10 |
11 | mainWindow.on('enter-full-screen', () => {
12 | mainWindow.webContents.send(UPDATE_FULL_SCREEN_STATUS, true);
13 | });
14 | mainWindow.on('leave-full-screen', () => {
15 | mainWindow.webContents.send(UPDATE_FULL_SCREEN_STATUS, false);
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/services/content/ErrorHandlers/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | component: {
3 | left: 0,
4 | position: 'absolute',
5 | top: 0,
6 | width: '100%',
7 | zIndex: 0,
8 | alignItems: 'center',
9 | background: theme.colorWebviewErrorHandlerBackground,
10 | display: 'flex',
11 | flexDirection: 'column',
12 | justifyContent: 'center',
13 | textAlign: 'center',
14 | },
15 | buttonContainer: {
16 | display: 'flex',
17 | flexDirection: 'row',
18 | height: 'auto',
19 | margin: [40, 0, 20],
20 |
21 | '& button': {
22 | margin: [0, 10, 0, 10],
23 | },
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/src/features/utils/ActionBinding.js:
--------------------------------------------------------------------------------
1 | export default class ActionBinding {
2 | action;
3 |
4 | isActive = false;
5 |
6 | constructor(action) {
7 | this.action = action;
8 | }
9 |
10 | start() {
11 | if (!this.isActive) {
12 | const { action } = this;
13 | action[0].listen(action[1]);
14 | this.isActive = true;
15 | }
16 | }
17 |
18 | stop() {
19 | if (this.isActive) {
20 | const { action } = this;
21 | action[0].off(action[1]);
22 | this.isActive = false;
23 | }
24 | }
25 | }
26 |
27 | export const createActionBindings = actions => (
28 | actions.map(a => new ActionBinding(a))
29 | );
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "esnext",
5 | "module": "commonjs",
6 | "lib": [
7 | "es2015",
8 | "es2017",
9 | "dom"
10 | ],
11 | "jsx": "react",
12 | "sourceMap": true,
13 | "strict": true,
14 | "allowSyntheticDefaultImports": true,
15 | "experimentalDecorators": true,
16 | "composite": true,
17 | "esModuleInterop": true,
18 | "typeRoots": ["packages/typings/types", "node_modules/@types"],
19 | "paths": {
20 | "@types/*": ["packages/typings/types/*.d.ts"],
21 | "*": ["packages/typings/types/*.d.ts"]
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/helpers/visibility-helper.js:
--------------------------------------------------------------------------------
1 | export function onVisibilityChange(cb) {
2 | let isVisible = true;
3 |
4 | if (!cb) {
5 | throw new Error('no callback given');
6 | }
7 |
8 | function focused() {
9 | if (!isVisible) {
10 | cb(isVisible = true);
11 | }
12 | }
13 |
14 | function unfocused() {
15 | if (isVisible) {
16 | cb(isVisible = false);
17 | }
18 | }
19 |
20 | document.addEventListener('visibilitychange', () => { (document.hidden ? unfocused : focused)(); });
21 |
22 | window.onpageshow = focused;
23 | window.onfocus = focused;
24 |
25 | window.onpagehid = unfocused;
26 | window.onblur = unfocused;
27 | }
28 |
--------------------------------------------------------------------------------
/webpack.config.base.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TerserPlugin = require('terser-webpack-plugin');
3 |
4 | const IS_DEV = process.env.NODE_ENV === 'development';
5 |
6 | module.exports = dir => ({
7 | context: dir,
8 | entry: path.join(dir, '/src/index.ts'),
9 | module: {
10 | rules: [{
11 | test: /\.tsx?$/,
12 | loader: 'ts-loader',
13 | exclude: /node_modules/,
14 | }],
15 | },
16 | resolve: {
17 | extensions: ['.tsx', '.ts', '.js'],
18 | },
19 | devtool: 'inline-source-map',
20 | mode: IS_DEV ? 'development' : 'production',
21 | optimization: {
22 | minimizer: !IS_DEV ? [new TerserPlugin()] : [],
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/src/features/workspaces/models/Workspace.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 |
3 | export default class Workspace {
4 | id = null;
5 |
6 | @observable name = null;
7 |
8 | @observable order = null;
9 |
10 | @observable services = [];
11 |
12 | @observable userId = null;
13 |
14 | @observable isActive = false
15 |
16 | constructor(data) {
17 | if (!data.id) {
18 | throw Error('Workspace requires Id');
19 | }
20 |
21 | this.id = data.id;
22 | this.name = data.name;
23 | this.order = data.order;
24 | this.services.replace(data.services);
25 | this.userId = data.userId;
26 | this.isActive = data.isActive ?? false;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/typings/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@meetfranz/typings",
3 | "version": "0.0.11",
4 | "description": "TypeScript typings for internal and external projects",
5 | "author": "Stefan Malzner ",
6 | "homepage": "https://github.com/meetfranz/franz",
7 | "license": "Apache-2.0",
8 | "directories": {
9 | "types": "types"
10 | },
11 | "publishConfig": {
12 | "access": "public"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/meetfranz/franz.git"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/meetfranz/franz/issues"
20 | },
21 | "gitHead": "e9b9079dc921e85961954727a7b2a8eabe5b9798"
22 | }
23 |
--------------------------------------------------------------------------------
/uidev/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: './src/index.tsx',
6 | module: {
7 | rules: [{
8 | test: /\.tsx?$/,
9 | use: 'ts-loader',
10 | exclude: /node_modules/,
11 | }],
12 | },
13 | resolve: {
14 | extensions: ['.tsx', '.ts', '.js'],
15 | alias: {
16 | react: path.resolve('../node_modules/react'),
17 | },
18 | },
19 | mode: 'none',
20 | plugins: [
21 | new HtmlWebpackPlugin({
22 | template: path.join('src', 'app.html'),
23 | }),
24 | ],
25 | devServer: {
26 | inline: true,
27 | port: 8008,
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/src/helpers/service-helpers.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { app } from '@electron/remote';
3 | import fs from 'fs-extra';
4 |
5 | export function getServicePartitionsDirectory() {
6 | return path.join(app.getPath('userData'), 'Partitions');
7 | }
8 |
9 | export function removeServicePartitionDirectory(id = '', addServicePrefix = false) {
10 | const servicePartition = path.join(getServicePartitionsDirectory(), `${addServicePrefix ? 'service-' : ''}${id}`);
11 |
12 | return fs.remove(servicePartition);
13 | }
14 |
15 | export async function getServiceIdsFromPartitions() {
16 | const files = await fs.readdir(getServicePartitionsDirectory());
17 | return files.filter(n => n !== '__chrome_extension');
18 | }
19 |
--------------------------------------------------------------------------------
/src/stores/lib/Reaction.js:
--------------------------------------------------------------------------------
1 | import { autorun } from 'mobx';
2 |
3 | export default class Reaction {
4 | reaction;
5 |
6 | options;
7 |
8 | isRunning = false;
9 |
10 | dispose;
11 |
12 | constructor(reaction, options = {}) {
13 | this.reaction = reaction;
14 | this.options = options;
15 | }
16 |
17 | start() {
18 | if (!this.isRunning) {
19 | this.dispose = autorun(this.reaction, this.options);
20 | this.isRunning = true;
21 | }
22 | }
23 |
24 | stop() {
25 | if (this.isRunning) {
26 | this.dispose();
27 | this.isRunning = false;
28 | }
29 | }
30 | }
31 |
32 | export const createReactions = reactions => (
33 | reactions.map(r => new Reaction(r))
34 | );
35 |
--------------------------------------------------------------------------------
/src/features/basicAuth/index.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 |
3 | import BasicAuthComponent from './Component';
4 |
5 | const debug = require('debug')('Franz:feature:basicAuth');
6 |
7 | export default function initialize() {
8 | debug('Initialize basicAuth feature');
9 | }
10 |
11 | export function sendCredentials(user, password) {
12 | debug('Sending credentials to main', user, password);
13 |
14 | ipcRenderer.send('feature-basic-auth-credentials', {
15 | user,
16 | password,
17 | });
18 | }
19 |
20 | export function cancelLogin() {
21 | debug('Cancel basic auth event');
22 |
23 | ipcRenderer.send('feature-basic-auth-cancel');
24 | }
25 |
26 | export const Component = BasicAuthComponent;
27 |
--------------------------------------------------------------------------------
/src/containers/auth/InviteScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import Invite from '../../components/auth/Invite';
5 |
6 | export default @inject('stores', 'actions') @observer class InviteScreen extends Component {
7 | render() {
8 | const { actions } = this.props;
9 |
10 | return (
11 |
15 | );
16 | }
17 | }
18 |
19 | InviteScreen.wrappedComponent.propTypes = {
20 | actions: PropTypes.shape({
21 | user: PropTypes.shape({
22 | invite: PropTypes.func.isRequired,
23 | }).isRequired,
24 | }).isRequired,
25 | };
26 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/cld.js:
--------------------------------------------------------------------------------
1 | import { loadModule } from 'cld3-asm';
2 | import { ipcMain } from 'electron';
3 |
4 | const debug = require('debug')('Franz:ipcApi:cld');
5 |
6 | export default async () => {
7 | const cldFactory = await loadModule();
8 | const cld = cldFactory.create(0, 1000);
9 | ipcMain.handle('detect-language', async (event, { sample }) => {
10 | try {
11 | const result = cld.findLanguage(sample);
12 | debug('Checking language', result.language);
13 | if (result.is_reliable) {
14 | debug('Language detected reliably, setting spellchecker language to', result.language);
15 |
16 | return result.language;
17 | }
18 | } catch (e) {
19 | console.error(e);
20 | }
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/packages/theme/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@meetfranz/theme",
3 | "version": "1.0.14",
4 | "description": "Theme configuration for Franz",
5 | "author": "Stefan Malzner ",
6 | "homepage": "https://github.com/meetfranz/franz",
7 | "license": "Apache-2.0",
8 | "main": "lib/index.js",
9 | "publishConfig": {
10 | "access": "public"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/meetfranz/franz.git"
15 | },
16 | "scripts": {
17 | "dev": "tsc -w",
18 | "build": "tsc"
19 | },
20 | "bugs": {
21 | "url": "https://github.com/meetfranz/franz/issues"
22 | },
23 | "dependencies": {
24 | "color": "^3.1.0"
25 | },
26 | "gitHead": "9f2ab40b7602bc3df26ebb093b484b9917768f69"
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/ui/Modal/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | component: {
3 | zIndex: 500,
4 | position: 'absolute',
5 | },
6 | overlay: {
7 | background: theme.colorModalOverlayBackground,
8 | position: 'fixed',
9 | top: 0,
10 | left: 0,
11 | right: 0,
12 | bottom: 0,
13 | display: 'flex',
14 | },
15 | modal: {
16 | background: theme.colorModalBackground,
17 | maxWidth: '90%',
18 | height: 'auto',
19 | margin: 'auto auto',
20 | borderRadius: 6,
21 | boxShadow: '0px 13px 40px 0px rgba(0,0,0,0.2)',
22 | position: 'relative',
23 | },
24 | content: {
25 | padding: 20,
26 | },
27 | close: {
28 | position: 'absolute',
29 | top: 0,
30 | right: 0,
31 | padding: 20,
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/src/features/delayApp/api.js:
--------------------------------------------------------------------------------
1 | // import Request from '../../stores/lib/Request';
2 | import { API, API_VERSION } from '../../environment';
3 | import { sendAuthRequest } from '../../api/utils/auth';
4 | import CachedRequest from '../../stores/lib/CachedRequest';
5 |
6 |
7 | const debug = require('debug')('Franz:feature:delayApp:api');
8 |
9 | export const delayAppApi = {
10 | async getPoweredBy() {
11 | debug('fetching release changelog from Github');
12 | const url = `${API}/${API_VERSION}/poweredby`;
13 | const response = await sendAuthRequest(url, {
14 | method: 'GET',
15 | });
16 |
17 | if (!response.ok) return null;
18 | return response.json();
19 | },
20 | };
21 |
22 | export const getPoweredByRequest = new CachedRequest(delayAppApi, 'getPoweredBy');
23 |
--------------------------------------------------------------------------------
/src/actions/app.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | setBadge: {
5 | unreadDirectMessageCount: PropTypes.number.isRequired,
6 | unreadIndirectMessageCount: PropTypes.number,
7 | },
8 | notify: {
9 | title: PropTypes.string.isRequired,
10 | options: PropTypes.object.isRequired,
11 | serviceId: PropTypes.string,
12 | },
13 | launchOnStartup: {
14 | enable: PropTypes.bool.isRequired,
15 | },
16 | openExternalUrl: {
17 | url: PropTypes.string.isRequired,
18 | },
19 | checkForUpdates: {},
20 | resetUpdateStatus: {},
21 | installUpdate: {},
22 | healthCheck: {},
23 | muteApp: {
24 | isMuted: PropTypes.bool.isRequired,
25 | overrideSystemMute: PropTypes.bool,
26 | },
27 | toggleMuteApp: {},
28 | clearAllCache: {},
29 | };
30 |
--------------------------------------------------------------------------------
/src/styles/badge.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark .badge {
4 | background: $dark-theme-gray;
5 | border-radius: $theme-border-radius-small;
6 | color: $dark-theme-gray-lightest;
7 |
8 | &.badge--primary,
9 | &.badge--premium {
10 | background: $theme-brand-primary;
11 | color: $dark-theme-gray-lightest;
12 | }
13 | }
14 |
15 |
16 | .badge {
17 | background: $theme-gray-lighter;
18 | border-radius: $theme-border-radius;
19 | display: inline-block;
20 | font-size: 14px;
21 | padding: 5px 10px;
22 | letter-spacing: 0;
23 |
24 | &.badge--primary,
25 | &.badge--premium {
26 | background: $theme-brand-primary;
27 | color: #FFF;
28 | }
29 |
30 | &.badge--success {
31 | background: $theme-brand-success;
32 | color: #FFF;
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "targets": {
7 | "electron": 4
8 | }
9 | }
10 | ],
11 | "@babel/react"
12 | ],
13 | "plugins": [
14 | "react-require",
15 | [
16 | "@babel/plugin-proposal-decorators",
17 | {
18 | "legacy": true
19 | }
20 | ],
21 | "@babel/proposal-export-default-from",
22 | [
23 | "@babel/proposal-class-properties",
24 | {
25 | "loose": true
26 | }
27 | ],
28 | "@babel/proposal-throw-expressions",
29 | "@babel/syntax-dynamic-import",
30 | ["react-intl", {
31 | "messagesDir": "./src/i18n/messages/",
32 | "enforceDescriptions": false,
33 | "extractSourceLocation": true
34 | }]
35 | ],
36 | "sourceMaps": "inline"
37 | }
38 |
--------------------------------------------------------------------------------
/uidev/src/stories/badge.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Badge, ProBadge } from '@meetfranz/ui';
4 | import { storiesOf } from '../stores/stories';
5 |
6 | storiesOf('Badge')
7 | .add('Basic', () => (
8 | <>
9 | New
10 | >
11 | ))
12 | .add('Styles', () => (
13 | <>
14 | Primary
15 | secondary
16 | success
17 | warning
18 | danger
19 | inverted
20 | >
21 | ))
22 | .add('Pro Badge', () => (
23 | <>
24 |
25 | >
26 | ))
27 | .add('Pro Badge inverted', () => (
28 | <>
29 |
30 | >
31 | ));
32 |
--------------------------------------------------------------------------------
/src/lib/Form.js:
--------------------------------------------------------------------------------
1 | import Form from 'mobx-react-form';
2 |
3 | export default class DefaultForm extends Form {
4 | bindings() {
5 | return {
6 | default: {
7 | id: 'id',
8 | name: 'name',
9 | type: 'type',
10 | value: 'value',
11 | label: 'label',
12 | placeholder: 'placeholder',
13 | disabled: 'disabled',
14 | onChange: 'onChange',
15 | onFocus: 'onFocus',
16 | onBlur: 'onBlur',
17 | error: 'error',
18 | },
19 | };
20 | }
21 |
22 | options() {
23 | return {
24 | validateOnInit: false, // default: true
25 | // validateOnBlur: true, // default: true
26 | // validateOnChange: true // default: false
27 | // // validationDebounceWait: {
28 | // // trailing: true,
29 | // // },
30 | };
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | import AppApi from './AppApi';
2 | import ServicesApi from './ServicesApi';
3 | import RecipePreviewsApi from './RecipePreviewsApi';
4 | import RecipesApi from './RecipesApi';
5 | import UserApi from './UserApi';
6 | import LocalApi from './LocalApi';
7 | import PaymentApi from './PaymentApi';
8 | import NewsApi from './NewsApi';
9 | import FeaturesApi from './FeaturesApi';
10 |
11 | export default (server, local) => ({
12 | app: new AppApi(server, local),
13 | services: new ServicesApi(server, local),
14 | recipePreviews: new RecipePreviewsApi(server, local),
15 | recipes: new RecipesApi(server, local),
16 | features: new FeaturesApi(server, local),
17 | user: new UserApi(server, local),
18 | local: new LocalApi(server, local),
19 | payment: new PaymentApi(server, local),
20 | news: new NewsApi(server, local),
21 | });
22 |
--------------------------------------------------------------------------------
/src/styles/searchInput.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 | @import './mixins.scss';
3 |
4 | .theme__dark .search-input {
5 | @extend %headline__dark;
6 | background: $dark-theme-gray-dark;
7 | border: 1px solid $dark-theme-gray-light;
8 | border-radius: $theme-border-radius;
9 | color: $dark-theme-gray-lightest;
10 |
11 | input { color: $dark-theme-gray-lightest; }
12 | }
13 |
14 | .search-input {
15 | @extend %headline;
16 | align-items: center;
17 | background: $theme-gray-lightest;
18 | border-radius: 30px;
19 | color: $theme-gray-light;
20 | display: flex;
21 | height: auto;
22 | padding: 5px 10px;
23 | width: 100%;
24 |
25 | label {
26 | width: 100%;
27 | }
28 |
29 | input {
30 | background: none;
31 | border: 0;
32 | color: $theme-gray-light;
33 | flex: 1;
34 | padding-left: 10px;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/features/todos/actions.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions';
3 |
4 | export const todoActions = createActionsFromDefinitions({
5 | resize: {
6 | width: PropTypes.number.isRequired,
7 | },
8 | toggleTodosPanel: {},
9 | toggleTodosFeatureVisibility: {},
10 | setTodosWebview: {
11 | webview: PropTypes.instanceOf(Element).isRequired,
12 | },
13 | handleHostMessage: {
14 | action: PropTypes.string.isRequired,
15 | data: PropTypes.object,
16 | },
17 | handleClientMessage: {
18 | channel: PropTypes.string.isRequired,
19 | message: PropTypes.shape({
20 | action: PropTypes.string.isRequired,
21 | data: PropTypes.object,
22 | }),
23 | },
24 | toggleDevTools: {},
25 | reload: {},
26 | }, PropTypes.checkPropTypes);
27 |
28 | export default todoActions;
29 |
--------------------------------------------------------------------------------
/src/features/planSelection/api.js:
--------------------------------------------------------------------------------
1 | import { sendAuthRequest } from '../../api/utils/auth';
2 | import { API, API_VERSION } from '../../environment';
3 | import Request from '../../stores/lib/Request';
4 |
5 | const debug = require('debug')('Franz:feature:planSelection:api');
6 |
7 | export const planSelectionApi = {
8 | downgrade: async () => {
9 | const url = `${API}/${API_VERSION}/payment/downgrade`;
10 | const options = {
11 | method: 'PUT',
12 | };
13 | debug('downgrade UPDATE', url, options);
14 | const result = await sendAuthRequest(url, options);
15 | debug('downgrade RESULT', result);
16 | if (!result.ok) throw result;
17 |
18 | return result.ok;
19 | },
20 | };
21 |
22 | export const downgradeUserRequest = new Request(planSelectionApi, 'downgrade');
23 |
24 | export const resetApiRequests = () => {
25 | downgradeUserRequest.reset();
26 | };
27 |
--------------------------------------------------------------------------------
/src/stores/GlobalErrorStore.js:
--------------------------------------------------------------------------------
1 | import { observable, action } from 'mobx';
2 | import Store from './lib/Store';
3 | import Request from './lib/Request';
4 |
5 | export default class GlobalErrorStore extends Store {
6 | @observable error = null;
7 |
8 | @observable response = {};
9 |
10 | constructor(...args) {
11 | super(...args);
12 |
13 | Request.registerHook(this._handleRequests);
14 | }
15 |
16 | _handleRequests = action(async (request) => {
17 | if (request.isError) {
18 | this.error = request.error;
19 |
20 | if (request.error.json) {
21 | try {
22 | this.response = await request.error.json();
23 | } catch (error) {
24 | this.response = {};
25 | }
26 | if (this.error.status === 401) {
27 | this.actions.user.logout({ serverLogout: true });
28 | }
29 | }
30 | }
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/features/communityRecipes/store.js:
--------------------------------------------------------------------------------
1 | import { computed, observable } from 'mobx';
2 | import { FeatureStore } from '../utils/FeatureStore';
3 |
4 | const debug = require('debug')('Franz:feature:communityRecipes:store');
5 |
6 | export class CommunityRecipesStore extends FeatureStore {
7 | @observable isCommunityRecipesIncludedInCurrentPlan = false;
8 |
9 | start(stores, actions) {
10 | debug('start');
11 | this.stores = stores;
12 | this.actions = actions;
13 | }
14 |
15 | stop() {
16 | debug('stop');
17 | super.stop();
18 | }
19 |
20 | @computed get communityRecipes() {
21 | if (!this.stores) return [];
22 |
23 | return this.stores.recipePreviews.dev.map((r) => {
24 | r.isDevRecipe = !!r.author.find(a => a.email === this.stores.user.data.email);
25 |
26 | return r;
27 | });
28 | }
29 | }
30 |
31 | export default CommunityRecipesStore;
32 |
--------------------------------------------------------------------------------
/src/features/workspaces/actions.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import Workspace from './models/Workspace';
3 | import { createActionsFromDefinitions } from '../../actions/lib/actions';
4 |
5 | export const workspaceActions = createActionsFromDefinitions({
6 | edit: {
7 | workspace: PropTypes.instanceOf(Workspace).isRequired,
8 | },
9 | create: {
10 | name: PropTypes.string.isRequired,
11 | },
12 | delete: {
13 | workspace: PropTypes.instanceOf(Workspace).isRequired,
14 | },
15 | update: {
16 | workspace: PropTypes.instanceOf(Workspace).isRequired,
17 | },
18 | activate: {
19 | workspace: PropTypes.instanceOf(Workspace).isRequired,
20 | },
21 | deactivate: {},
22 | toggleWorkspaceDrawer: {},
23 | openWorkspaceSettings: {},
24 | toggleKeepAllWorkspacesLoadedSetting: {},
25 | }, PropTypes.checkPropTypes);
26 |
27 | export default workspaceActions;
28 |
--------------------------------------------------------------------------------
/src/features/utils/FeatureStore.js:
--------------------------------------------------------------------------------
1 | export class FeatureStore {
2 | _actions = [];
3 |
4 | _reactions = [];
5 |
6 | stop() {
7 | this._stopActions();
8 | this._stopReactions();
9 | }
10 |
11 | // ACTIONS
12 |
13 | _registerActions(actions) {
14 | this._actions = actions;
15 | this._startActions();
16 | }
17 |
18 | _startActions(actions = this._actions) {
19 | actions.forEach(a => a.start());
20 | }
21 |
22 | _stopActions(actions = this._actions) {
23 | actions.forEach(a => a.stop());
24 | }
25 |
26 | // REACTIONS
27 |
28 | _registerReactions(reactions) {
29 | this._reactions = reactions;
30 | this._startReactions();
31 | }
32 |
33 | _startReactions(reactions = this._reactions) {
34 | reactions.forEach(r => r.start());
35 | }
36 |
37 | _stopReactions(reactions = this._reactions) {
38 | reactions.forEach(r => r.stop());
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/api/ServicesApi.js:
--------------------------------------------------------------------------------
1 | export default class ServicesApi {
2 | constructor(server, local) {
3 | this.local = local;
4 | this.server = server;
5 | }
6 |
7 | all() {
8 | return this.server.getServices();
9 | }
10 |
11 | // one(customerId) {
12 | // return this.server.getCustomer(customerId);
13 | // }
14 | //
15 | // search(needle) {
16 | // return this.server.searchCustomers(needle);
17 | // }
18 | //
19 | create(recipeId, data) {
20 | return this.server.createService(recipeId, data);
21 | }
22 |
23 | delete(serviceId) {
24 | return this.server.deleteService(serviceId);
25 | }
26 |
27 | update(serviceId, data) {
28 | return this.server.updateService(serviceId, data);
29 | }
30 |
31 | reorder(data) {
32 | return this.server.reorderService(data);
33 | }
34 |
35 | clearCache(serviceId) {
36 | return this.local.clearCache(serviceId);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/api/utils/auth.js:
--------------------------------------------------------------------------------
1 | import { app } from '@electron/remote';
2 | import localStorage from 'mobx-localstorage';
3 |
4 | export const prepareAuthRequest = (options = { method: 'GET' }, auth = true) => {
5 | const request = Object.assign(options, {
6 | mode: 'cors',
7 | headers: Object.assign({
8 | 'Content-Type': 'application/json',
9 | 'X-Franz-Source': 'desktop',
10 | 'X-Franz-Version': app.getVersion(),
11 | 'X-Franz-platform': process.platform,
12 | 'X-Franz-Timezone-Offset': new Date().getTimezoneOffset(),
13 | 'X-Franz-System-Locale': app.getLocale(),
14 | }, options.headers),
15 | });
16 |
17 | if (auth) {
18 | request.headers.Authorization = `Bearer ${localStorage.getItem('authToken')}`;
19 | }
20 |
21 | return request;
22 | };
23 |
24 | export const sendAuthRequest = (url, options, auth) => (
25 | window.fetch(url, prepareAuthRequest(options, auth))
26 | );
27 |
--------------------------------------------------------------------------------
/src/features/communityRecipes/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import { CommunityRecipesStore } from './store';
3 |
4 | const debug = require('debug')('Franz:feature:communityRecipes');
5 |
6 | export const DEFAULT_SERVICE_LIMIT = 3;
7 |
8 | export const communityRecipesStore = new CommunityRecipesStore();
9 |
10 | export default function initCommunityRecipes(stores, actions) {
11 | const { features } = stores;
12 |
13 | communityRecipesStore.start(stores, actions);
14 |
15 | // Toggle communityRecipe premium status
16 | reaction(
17 | () => (
18 | features.features.isCommunityRecipesIncludedInCurrentPlan
19 | ),
20 | (isPremiumFeature) => {
21 | debug('Community recipes is premium feature: ', isPremiumFeature);
22 | communityRecipesStore.isCommunityRecipesIncludedInCurrentPlan = isPremiumFeature;
23 | },
24 | {
25 | fireImmediately: true,
26 | },
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@meetfranz/ui",
3 | "version": "1.1.0",
4 | "description": "React UI components for Franz",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "dev": "tsc -w",
8 | "build": "tsc"
9 | },
10 | "publishConfig": {
11 | "access": "public"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/meetfranz/franz.git"
16 | },
17 | "keywords": [
18 | "Franz",
19 | "Forms",
20 | "React",
21 | "UI"
22 | ],
23 | "author": "Stefan Malzner ",
24 | "license": "Apache-2.0",
25 | "dependencies": {
26 | "@mdi/react": "^1.1.0",
27 | "@meetfranz/theme": "^1.0.14",
28 | "react-loader": "^2.4.5"
29 | },
30 | "peerDependencies": {
31 | "classnames": "^2.2.6",
32 | "react": "^16.7.0",
33 | "react-dom": "16.7.0",
34 | "react-jss": "^8.6.1"
35 | },
36 | "gitHead": "254da30f801169fac376bda1439b46cabbb491ad"
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/ui/StatusBarTargetUrl.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import classnames from 'classnames';
5 |
6 | import Appear from './effects/Appear';
7 |
8 | export default @observer class StatusBarTargetUrl extends Component {
9 | static propTypes = {
10 | className: PropTypes.string,
11 | text: PropTypes.string,
12 | };
13 |
14 | static defaultProps = {
15 | className: '',
16 | text: '',
17 | };
18 |
19 | render() {
20 | const {
21 | className,
22 | text,
23 | } = this.props;
24 |
25 | return (
26 |
32 |
33 | {text}
34 |
35 |
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/features/serviceLimit/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import { ServiceLimitStore } from './store';
3 |
4 | const debug = require('debug')('Franz:feature:serviceLimit');
5 |
6 | export const DEFAULT_SERVICE_LIMIT = 3;
7 |
8 | let store = null;
9 |
10 | export const serviceLimitStore = new ServiceLimitStore();
11 |
12 | export default function initServiceLimit(stores, actions) {
13 | const { features } = stores;
14 |
15 | // Toggle serviceLimit feature
16 | reaction(
17 | () => (
18 | features.features.isServiceLimitEnabled
19 | ),
20 | (isEnabled) => {
21 | if (isEnabled) {
22 | debug('Initializing `serviceLimit` feature');
23 | store = serviceLimitStore.start(stores, actions);
24 | } else if (store) {
25 | debug('Disabling `serviceLimit` feature');
26 | serviceLimitStore.stop();
27 | }
28 | },
29 | {
30 | fireImmediately: true,
31 | },
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/webview/zoom.js:
--------------------------------------------------------------------------------
1 | const electron = require('electron');
2 |
3 | const { ipcRenderer, webFrame } = electron;
4 |
5 | const maxZoomLevel = 9;
6 | const minZoomLevel = -8;
7 | let zoomLevel = 0;
8 |
9 | ipcRenderer.on('zoomIn', () => {
10 | if (maxZoomLevel > zoomLevel) {
11 | zoomLevel += 1;
12 | }
13 | webFrame.setZoomLevel(zoomLevel);
14 |
15 | ipcRenderer.sendToHost('zoomLevel', { zoom: zoomLevel });
16 | });
17 |
18 | ipcRenderer.on('zoomOut', () => {
19 | if (minZoomLevel < zoomLevel) {
20 | zoomLevel -= 1;
21 | }
22 | webFrame.setZoomLevel(zoomLevel);
23 |
24 | ipcRenderer.sendToHost('zoomLevel', { zoom: zoomLevel });
25 | });
26 |
27 | ipcRenderer.on('zoomReset', () => {
28 | zoomLevel = 0;
29 | webFrame.setZoomLevel(zoomLevel);
30 |
31 | ipcRenderer.sendToHost('zoomLevel', { zoom: zoomLevel });
32 | });
33 |
34 | ipcRenderer.on('setZoom', (e, arg) => {
35 | zoomLevel = arg;
36 | webFrame.setZoomLevel(zoomLevel);
37 | });
38 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/index.js:
--------------------------------------------------------------------------------
1 | import appIndicator from './appIndicator';
2 | import autoUpdate from './autoUpdate';
3 | import browserViewManager from './browserViewManager';
4 | import cld from './cld';
5 | import desktopCapturer from './desktopCapturer';
6 | import focusState from './focusState';
7 | import fullscreenStatus from './fullscreen';
8 | import macOSPermissions from './macOSPermissions';
9 | import overlayWindow from './overlayWindow';
10 | import serviceCache from './serviceCache';
11 | import settings from './settings';
12 | import subscriptionWindow from './subscriptionWindow';
13 |
14 | export default (params) => {
15 | settings(params);
16 | autoUpdate(params);
17 | appIndicator(params);
18 | cld(params);
19 | desktopCapturer();
20 | focusState(params);
21 | fullscreenStatus(params);
22 | subscriptionWindow(params);
23 | serviceCache();
24 | browserViewManager(params);
25 | overlayWindow(params);
26 | macOSPermissions(params);
27 | };
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Desktop (please complete the following information):**
24 | - OS: [e.g. iOS]
25 | - Browser [e.g. chrome, safari]
26 | - Version [e.g. 22]
27 |
28 | **Smartphone (please complete the following information):**
29 | - Device: [e.g. iPhone6]
30 | - OS: [e.g. iOS8.1]
31 | - Browser [e.g. stock browser, safari]
32 | - Version [e.g. 22]
33 |
34 | **Additional context**
35 | Add any other context about the problem here.
36 |
--------------------------------------------------------------------------------
/src/containers/auth/WelcomeScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 |
5 | import Welcome from '../../components/auth/Welcome';
6 | import UserStore from '../../stores/UserStore';
7 | import RecipePreviewsStore from '../../stores/RecipePreviewsStore';
8 |
9 | export default @inject('stores', 'actions') @observer class LoginScreen extends Component {
10 | render() {
11 | const { user, recipePreviews } = this.props.stores;
12 |
13 | return (
14 |
19 | );
20 | }
21 | }
22 |
23 | LoginScreen.wrappedComponent.propTypes = {
24 | stores: PropTypes.shape({
25 | user: PropTypes.instanceOf(UserStore).isRequired,
26 | recipePreviews: PropTypes.instanceOf(RecipePreviewsStore).isRequired,
27 | }).isRequired,
28 | };
29 |
--------------------------------------------------------------------------------
/docs/example-feature/store.js:
--------------------------------------------------------------------------------
1 | import { action, observable, reaction } from 'mobx';
2 | import Store from '../../src/stores/lib/Store';
3 | import Request from '../../src/stores/lib/Request';
4 |
5 | const debug = require('debug')('Franz:feature:EXAMPLE_FEATURE:store');
6 |
7 | export class ExampleFeatureStore extends Store {
8 | @observable getNameRequest = new Request(this.api, 'getName');
9 |
10 | constructor(stores, api, actions, state) {
11 | super(stores, api, actions);
12 | this.state = state;
13 | }
14 |
15 | setup() {
16 | debug('fetching name from api');
17 | this.getNameRequest.execute();
18 |
19 | // Update the name on the state when the request resolved
20 | reaction(
21 | () => this.getNameRequest.result,
22 | name => this._setName(name),
23 | );
24 | }
25 |
26 | @action _setName = (name) => {
27 | debug('setting name', name);
28 | this.state.name = name;
29 | };
30 | }
31 |
32 | export default ExampleFeatureStore;
33 |
--------------------------------------------------------------------------------
/uidev/src/stores/stories.ts:
--------------------------------------------------------------------------------
1 | import { store } from './index';
2 |
3 | export type StorySectionName = string;
4 | export type StoryName = string;
5 | export type StoryComponent = () => JSX.Element;
6 |
7 | export interface IStories {
8 | name: string;
9 | component: StoryComponent;
10 | }
11 |
12 | export interface ISections {
13 | name: StorySectionName;
14 | stories: IStories[];
15 | }
16 |
17 | export interface IStoryStore {
18 | sections: ISections[];
19 | }
20 |
21 | export const storyStore: IStoryStore = {
22 | sections: [],
23 | };
24 |
25 | export const storiesOf = (name: StorySectionName) => {
26 | const length = storyStore.sections.push({
27 | name,
28 | stories: [],
29 | });
30 |
31 | const actions = {
32 | add: (name: StoryName, component: StoryComponent) => {
33 | storyStore.sections[length - 1].stories.push({
34 | name,
35 | component,
36 | });
37 |
38 | return actions;
39 | },
40 | };
41 |
42 | return actions;
43 | };
44 |
--------------------------------------------------------------------------------
/src/features/trialStatusBar/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import TrialStatusBarStore from './store';
3 |
4 | const debug = require('debug')('Franz:feature:trialStatusBar');
5 |
6 | export const GA_CATEGORY_TRIAL_STATUS_BAR = 'trialStatusBar';
7 |
8 | export const trialStatusBarStore = new TrialStatusBarStore();
9 |
10 | export default function initTrialStatusBar(stores, actions) {
11 | stores.trialStatusBar = trialStatusBarStore;
12 | const { features } = stores;
13 |
14 | // Toggle trialStatusBar feature
15 | reaction(
16 | () => features.features.isTrialStatusBarEnabled,
17 | (isEnabled) => {
18 | if (isEnabled) {
19 | debug('Initializing `trialStatusBar` feature');
20 | trialStatusBarStore.start(stores, actions);
21 | } else if (trialStatusBarStore.isFeatureActive) {
22 | debug('Disabling `trialStatusBar` feature');
23 | trialStatusBarStore.stop();
24 | }
25 | },
26 | {
27 | fireImmediately: true,
28 | },
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/styles/services.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark {
4 | .services {
5 | background: $dark-theme-gray-darkest;
6 |
7 | .services__webview-wrapper { background: $dark-theme-gray-darkest; }
8 | }
9 |
10 | .services__no-service,
11 | .services__info-layer {
12 | background: $dark-theme-gray-darkest;
13 |
14 | h1 { color: $dark-theme-gray-lightest; }
15 | }
16 | }
17 |
18 |
19 | .services {
20 | background: #FFF;
21 | flex: 1;
22 | height: 100%;
23 | // order: 5;
24 | overflow: hidden;
25 | position: relative;
26 |
27 | .services__webview-wrapper { background: $theme-gray-lighter; }
28 |
29 | }
30 |
31 | .services__no-service,
32 | .services__info-layer {
33 | align-items: center;
34 | display: flex;
35 | flex: 1;
36 | flex-direction: column;
37 | justify-content: center;
38 | text-align: center;
39 |
40 | h1 {
41 | color: $theme-gray-dark;
42 | margin: 25px 0 40px;
43 | }
44 |
45 | a.button,
46 | button { margin: 40px 0 20px; }
47 | }
--------------------------------------------------------------------------------
/src/components/ui/PremiumFeatureContainer/styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | container: {
3 | background: theme.colorSubscriptionContainerBackground,
4 | border: theme.colorSubscriptionContainerBorder,
5 | margin: [0, 0, 20, -20],
6 | padding: 20,
7 | 'border-radius': theme.borderRadius,
8 | pointerEvents: 'none',
9 | height: 'auto',
10 | },
11 | titleContainer: {
12 | display: 'flex',
13 | },
14 | title: {
15 | 'font-weight': 'bold',
16 | color: theme.colorSubscriptionContainerTitle,
17 | },
18 | actionButton: {
19 | background: theme.colorSubscriptionContainerActionButtonBackground,
20 | color: theme.colorSubscriptionContainerActionButtonColor,
21 | 'margin-left': 'auto',
22 | 'border-radius': theme.borderRadiusSmall,
23 | padding: [4, 8],
24 | 'font-size': 12,
25 | pointerEvents: 'initial',
26 | },
27 | content: {
28 | opacity: 0.5,
29 | 'margin-top': 20,
30 | '& > :last-child': {
31 | 'margin-bottom': 0,
32 | },
33 | },
34 | });
35 |
--------------------------------------------------------------------------------
/src/styles/subscription.scss:
--------------------------------------------------------------------------------
1 | .subscription {
2 | .subscription__premium-features {
3 | margin: 10px 0;
4 |
5 | li {
6 | align-items: center;
7 | display: flex;
8 | height: 30px;
9 |
10 | &:before {
11 | content: "👍";
12 | margin-right: 10px;
13 | }
14 |
15 | .badge { margin-left: 10px; }
16 | }
17 | }
18 |
19 | .subscription__premium-info { margin: 15px 0 25px; }
20 | }
21 |
22 | .paymentTiers .franz-form__radio-wrapper {
23 | flex-flow: wrap;
24 |
25 | .franz-form__radio {
26 | flex: initial;
27 | margin-right: 2%;
28 | width: 32%;
29 |
30 | &:nth-child(3) { margin-right: 0; }
31 |
32 | &:nth-child(4) {
33 | margin-right: 0;
34 | margin-top: 2%;
35 | width: 100%;
36 | }
37 | }
38 | }
39 |
40 | .settings .paymentTiers .franz-form__radio-wrapper .franz-form__radio {
41 | width: 49%;
42 |
43 | &:nth-child(2) { margin-right: 0; }
44 |
45 | &:nth-child(3) {
46 | margin-top: 2%;
47 | width: 100%;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/packages/forms/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@meetfranz/forms",
3 | "version": "1.2.1",
4 | "description": "React form components for Franz",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "dev": "tsc -w",
8 | "build": "tsc"
9 | },
10 | "publishConfig": {
11 | "access": "public"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/meetfranz/franz.git"
16 | },
17 | "keywords": [
18 | "Franz",
19 | "Forms",
20 | "React",
21 | "UI"
22 | ],
23 | "author": "Stefan Malzner ",
24 | "license": "Apache-2.0",
25 | "dependencies": {
26 | "@mdi/js": "^3.3.92",
27 | "@mdi/react": "^1.1.0",
28 | "@meetfranz/theme": "^1.0.14",
29 | "react-html-attributes": "^1.4.3",
30 | "react-loader": "^2.4.5"
31 | },
32 | "peerDependencies": {
33 | "classnames": "^2.2.6",
34 | "react": "^16.7.0",
35 | "react-dom": "16.7.0",
36 | "react-jss": "^8.6.1"
37 | },
38 | "gitHead": "00db2bddccb8bb8ad7d29b8d032876c798b8bbf3"
39 | }
40 |
--------------------------------------------------------------------------------
/src/actions/user.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default {
4 | login: {
5 | email: PropTypes.string.isRequired,
6 | password: PropTypes.string.isRequired,
7 | },
8 | logout: {},
9 | signup: {
10 | firstname: PropTypes.string.isRequired,
11 | lastname: PropTypes.string.isRequired,
12 | email: PropTypes.string.isRequired,
13 | password: PropTypes.string.isRequired,
14 | accountType: PropTypes.string,
15 | company: PropTypes.string,
16 | plan: PropTypes.string,
17 | currency: PropTypes.string,
18 | },
19 | retrievePassword: {
20 | email: PropTypes.string.isRequired,
21 | },
22 | activateTrial: {
23 | planId: PropTypes.string.isRequired,
24 | },
25 | invite: {
26 | invites: PropTypes.array.isRequired,
27 | },
28 | update: {
29 | userData: PropTypes.object.isRequired,
30 | },
31 | resetStatus: {},
32 | importLegacyServices: PropTypes.arrayOf(PropTypes.shape({
33 | recipe: PropTypes.string.isRequired,
34 | })).isRequired,
35 | delete: {},
36 | };
37 |
--------------------------------------------------------------------------------
/src/helpers/password-helpers.js:
--------------------------------------------------------------------------------
1 | import crypto from 'crypto';
2 |
3 | export function hash(password) {
4 | return crypto.createHash('sha256').update(password).digest('base64');
5 | }
6 |
7 | export function scorePassword(password) {
8 | let score = 0;
9 | if (!password) {
10 | return score;
11 | }
12 |
13 | // award every unique letter until 5 repetitions
14 | const letters = {};
15 | for (let i = 0; i < password.length; i += 1) {
16 | letters[password[i]] = (letters[password[i]] || 0) + 1;
17 | score += 5.0 / letters[password[i]];
18 | }
19 |
20 | // bonus points for mixing it up
21 | const variations = {
22 | digits: /\d/.test(password),
23 | lower: /[a-z]/.test(password),
24 | upper: /[A-Z]/.test(password),
25 | nonWords: /\W/.test(password),
26 | };
27 |
28 | let variationCount = 0;
29 | Object.keys(variations).forEach((key) => {
30 | variationCount += (variations[key] === true) ? 1 : 0;
31 | });
32 |
33 | score += (variationCount - 1) * 10;
34 |
35 | return parseInt(score, 10);
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/settings/recipes/RecipeItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 |
5 | import RecipePreviewModel from '../../../models/RecipePreview';
6 |
7 | export default @observer class RecipeItem extends Component {
8 | static propTypes = {
9 | recipe: PropTypes.instanceOf(RecipePreviewModel).isRequired,
10 | onClick: PropTypes.func.isRequired,
11 | };
12 |
13 | render() {
14 | const { recipe, onClick } = this.props;
15 |
16 | return (
17 |
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/ui/FeatureItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import injectSheet from 'react-jss';
3 | import { Icon } from '@meetfranz/ui';
4 | import classnames from 'classnames';
5 | import { mdiCheckCircle } from '@mdi/js';
6 |
7 | const styles = theme => ({
8 | featureItem: {
9 | borderBottom: [1, 'solid', theme.defaultContentBorder],
10 | padding: [8, 0],
11 | display: 'flex',
12 | alignItems: 'center',
13 | textAlign: 'left',
14 | },
15 | featureIcon: {
16 | fill: theme.brandSuccess,
17 | marginRight: 10,
18 | },
19 | });
20 |
21 | export const FeatureItem = injectSheet(styles)(({
22 | classes, className, name, icon,
23 | }) => (
24 |
29 | {icon ? (
30 | {icon}
31 | ) : (
32 |
33 | )}
34 | {name}
35 |
36 | ));
37 |
38 | export default FeatureItem;
39 |
--------------------------------------------------------------------------------
/src/components/ui/Loader.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Loader from 'react-loader';
4 |
5 | import { oneOrManyChildElements } from '../../prop-types';
6 |
7 | export default class LoaderComponent extends Component {
8 | static propTypes = {
9 | children: oneOrManyChildElements,
10 | loaded: PropTypes.bool,
11 | className: PropTypes.string,
12 | color: PropTypes.string,
13 | };
14 |
15 | static defaultProps = {
16 | children: null,
17 | loaded: false,
18 | className: '',
19 | color: '#373a3c',
20 | };
21 |
22 | render() {
23 | const {
24 | children,
25 | loaded,
26 | className,
27 | color,
28 | } = this.props;
29 |
30 | return (
31 |
40 | {children}
41 |
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/styles/radio.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark .franz-form .franz-form__radio {
4 | border: 1px solid $dark-theme-gray-lighter;
5 | color: $dark-theme-gray-lightest;
6 |
7 | &.is-selected {
8 | background: $dark-theme-gray-lighter;
9 | border: 1px solid $dark-theme-gray-lighter;
10 | color: $dark-theme-gray-smoke;
11 | }
12 | }
13 |
14 |
15 | .franz-form {
16 | .franz-form__radio-wrapper { display: flex; }
17 |
18 | .franz-form__radio {
19 | border: 2px solid $theme-gray-lighter;
20 | border-radius: $theme-border-radius-small;
21 | box-shadow: $theme-inset-shadow;
22 | color: $theme-gray;
23 | flex: 1;
24 | margin-right: 20px;
25 | padding: 11px;
26 | text-align: center;
27 | transition: background $theme-transition-time;
28 |
29 | &:last-of-type { margin-right: 0; }
30 |
31 | &.is-selected {
32 | background: #FFF;
33 | border: 2px solid $theme-brand-primary;
34 | color: $theme-brand-primary;
35 | }
36 |
37 | input { display: none; }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/actions/lib/actions.js:
--------------------------------------------------------------------------------
1 | export const createActionsFromDefinitions = (actionDefinitions, validate) => {
2 | const actions = {};
3 | Object.keys(actionDefinitions).forEach((actionName) => {
4 | const action = (params = {}) => {
5 | const schema = actionDefinitions[actionName];
6 | validate(schema, params, actionName);
7 | action.notify(params);
8 | };
9 | actions[actionName] = action;
10 | action.listeners = [];
11 | action.listen = listener => action.listeners.push(listener);
12 | action.off = (listener) => {
13 | const { listeners } = action;
14 | listeners.splice(listeners.indexOf(listener), 1);
15 | };
16 | action.notify = params => action.listeners.forEach(listener => listener(params));
17 | });
18 | return actions;
19 | };
20 |
21 | export default (definitions, validate) => {
22 | const newActions = {};
23 | Object.keys(definitions).forEach((scopeName) => {
24 | newActions[scopeName] = createActionsFromDefinitions(definitions[scopeName], validate);
25 | });
26 | return newActions;
27 | };
28 |
--------------------------------------------------------------------------------
/packages/forms/src/wrapper/index.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { Component } from 'react';
3 | import injectStyle from 'react-jss';
4 | import { IWithStyle } from '../typings/generic';
5 |
6 | interface IProps extends IWithStyle {
7 | children: React.ReactNode;
8 | className?: string;
9 | identifier: string;
10 | noMargin?: boolean;
11 | }
12 |
13 | const styles = {
14 | container: {
15 | marginBottom: (props: IProps) => props.noMargin ? 0 : 20,
16 | },
17 | };
18 |
19 | class WrapperComponent extends Component {
20 | render() {
21 | const {
22 | children,
23 | classes,
24 | className,
25 | identifier,
26 | } = this.props;
27 |
28 | return (
29 |
36 | {children}
37 |
38 | );
39 | }
40 | }
41 |
42 | export const Wrapper = injectStyle(styles)(WrapperComponent);
43 |
--------------------------------------------------------------------------------
/src/helpers/recipe-helpers.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | const { app } = process.type === 'renderer' ? require('@electron/remote') : require('electron');
4 |
5 | export function getRecipeDirectory(id = '') {
6 | return path.join(app.getPath('userData'), 'recipes', id);
7 | }
8 |
9 | export function getDevRecipeDirectory(id = '') {
10 | return path.join(app.getPath('userData'), 'recipes', 'dev', id);
11 | }
12 |
13 | export function loadRecipeConfig(recipeId) {
14 | try {
15 | const configPath = `${recipeId}/package.json`;
16 | // Delete module from cache
17 | delete require.cache[require.resolve(configPath)];
18 |
19 | // eslint-disable-next-line
20 | let config = require(configPath);
21 |
22 | const moduleConfigPath = require.resolve(configPath);
23 | const paths = path.parse(moduleConfigPath);
24 | config.path = paths.dir;
25 |
26 | return config;
27 | } catch (e) {
28 | console.error(e);
29 | return null;
30 | }
31 | }
32 |
33 | module.paths.unshift(
34 | getDevRecipeDirectory(),
35 | getRecipeDirectory(),
36 | );
37 |
--------------------------------------------------------------------------------
/uidev/src/stories/textarea.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import uuid from 'uuid/v4';
3 |
4 | import { Textarea } from '@meetfranz/forms';
5 | import { storiesOf } from '../stores/stories';
6 |
7 | const defaultProps = () => {
8 | const id = uuid();
9 | return {
10 | label: 'Label',
11 | id: `test-${id}`,
12 | name: `test-${id}`,
13 | rows: 5,
14 | onChange: (e: React.ChangeEvent) => console.log('changed event', e),
15 | };
16 | };
17 |
18 | storiesOf('Textarea')
19 | .add('Basic', () => (
20 |
24 | ))
25 | .add('10 rows', () => (
26 |
30 | ))
31 | .add('With error', () => (
32 |
36 | ))
37 | .add('Disabled', () => (
38 |
43 | ));
44 |
--------------------------------------------------------------------------------
/src/features/announcements/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import { AnnouncementsStore } from './store';
3 |
4 | const debug = require('debug')('Franz:feature:announcements');
5 |
6 | export const GA_CATEGORY_ANNOUNCEMENTS = 'Announcements';
7 |
8 | export const announcementsStore = new AnnouncementsStore();
9 |
10 | export const ANNOUNCEMENTS_ROUTES = {
11 | TARGET: '/announcements/:id',
12 | };
13 |
14 | export default function initAnnouncements(stores, actions) {
15 | // const { features } = stores;
16 |
17 | // Toggle workspace feature
18 | reaction(
19 | () => (
20 | true
21 | // features.features.isAnnouncementsEnabled
22 | ),
23 | (isEnabled) => {
24 | if (isEnabled) {
25 | debug('Initializing `announcements` feature');
26 | announcementsStore.start(stores, actions);
27 | } else if (announcementsStore.isFeatureActive) {
28 | debug('Disabling `announcements` feature');
29 | announcementsStore.stop();
30 | }
31 | },
32 | {
33 | fireImmediately: true,
34 | },
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/features/spellchecker/index.js:
--------------------------------------------------------------------------------
1 | import { autorun, observable } from 'mobx';
2 |
3 | import { DEFAULT_FEATURES_CONFIG } from '../../config';
4 |
5 | const debug = require('debug')('Franz:feature:spellchecker');
6 |
7 | export const config = observable({
8 | isIncludedInCurrentPlan: DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan,
9 | });
10 |
11 | export default function init(stores) {
12 | debug('Initializing `spellchecker` feature');
13 |
14 | autorun(() => {
15 | const { isSpellcheckerIncludedInCurrentPlan } = stores.features.features;
16 |
17 | config.isIncludedInCurrentPlan = isSpellcheckerIncludedInCurrentPlan !== undefined ? isSpellcheckerIncludedInCurrentPlan : DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan;
18 |
19 | if (!stores.user.data.isPremium && !config.isIncludedInCurrentPlan && stores.settings.app.enableSpellchecking) {
20 | debug('Override settings.spellcheckerEnabled flag to false');
21 |
22 | Object.assign(stores.settings.app, {
23 | enableSpellchecking: false,
24 | });
25 | }
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/src/helpers/userAgent-helpers.js:
--------------------------------------------------------------------------------
1 | import { isMac, isWindows } from '../environment';
2 |
3 | function macOS() {
4 | // used fixed version (https://bugzilla.mozilla.org/show_bug.cgi?id=1679929)
5 | return 'Macintosh; Intel Mac OS X 10_15_7';
6 | }
7 |
8 | function windows() {
9 | return 'Windows NT 10.0; Win64; x64';
10 | }
11 |
12 | function linux() {
13 | return 'X11; Ubuntu; Linux x86_64';
14 | }
15 |
16 | export default function userAgent(removeChromeVersion = false) {
17 | let platformString = '';
18 |
19 | if (isMac) {
20 | platformString = macOS();
21 | } else if (isWindows) {
22 | platformString = windows();
23 | } else {
24 | platformString = linux();
25 | }
26 |
27 | // TODO: Update AppleWebKit and Safari version after electron update
28 | return `Mozilla/5.0 (${platformString}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome${!removeChromeVersion ? `/${process.versions.chrome}` : ''} Safari/537.36`;
29 | // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) old-airport-include/1.0.0 Chrome Electron/7.1.7 Safari/537.36
30 | }
31 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of the Franz project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
4 |
5 | Communication through GitHub, Slack, email or any other channel must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
6 |
7 | We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the project to do the same.
8 |
9 | If any member of the community violates this code of conduct, the maintainers of the Franz project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
10 |
11 | If you are subject to or witness unacceptable behavior, or have any other concerns, please open an issue or send an email to [Stefan](stefan@adlk.io).
12 |
--------------------------------------------------------------------------------
/src/components/ui/WebviewLoader/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import injectSheet from 'react-jss';
5 | import { defineMessages, intlShape } from 'react-intl';
6 |
7 | import FullscreenLoader from '../FullscreenLoader';
8 | import styles from './styles';
9 |
10 | const messages = defineMessages({
11 | loading: {
12 | id: 'service.webviewLoader.loading',
13 | defaultMessage: '!!!Loading',
14 | },
15 | });
16 |
17 | export default @observer @injectSheet(styles) class WebviewLoader extends Component {
18 | static propTypes = {
19 | name: PropTypes.string.isRequired,
20 | classes: PropTypes.object.isRequired,
21 | };
22 |
23 | static contextTypes = {
24 | intl: intlShape,
25 | };
26 |
27 | render() {
28 | const { classes, name } = this.props;
29 | const { intl } = this.context;
30 | return (
31 |
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/containers/auth/PasswordScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import Password from '../../components/auth/Password';
5 | import UserStore from '../../stores/UserStore';
6 |
7 | export default @inject('stores', 'actions') @observer class PasswordScreen extends Component {
8 | render() {
9 | const { actions, stores } = this.props;
10 |
11 | return (
12 |
19 | );
20 | }
21 | }
22 |
23 | PasswordScreen.wrappedComponent.propTypes = {
24 | actions: PropTypes.shape({
25 | user: PropTypes.shape({
26 | retrievePassword: PropTypes.func.isRequired,
27 | }).isRequired,
28 | }).isRequired,
29 | stores: PropTypes.shape({
30 | user: PropTypes.instanceOf(UserStore).isRequired,
31 | }).isRequired,
32 | };
33 |
--------------------------------------------------------------------------------
/src/i18n/globalMessages.js:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 |
3 | export default defineMessages({
4 | APIUnhealthy: {
5 | id: 'global.api.unhealthy',
6 | defaultMessage: '!!!Can\'t connect to Franz Online Services',
7 | },
8 | notConnectedToTheInternet: {
9 | id: 'global.notConnectedToTheInternet',
10 | defaultMessage: '!!!You are not connected to the internet.',
11 | },
12 | spellcheckerLanguage: {
13 | id: 'global.spellchecking.language',
14 | defaultMessage: '!!!Spell checking language',
15 | },
16 | spellcheckerSystemDefault: {
17 | id: 'global.spellchecker.useDefault',
18 | defaultMessage: '!!!Use System Default ({default})',
19 | },
20 | spellcheckerAutomaticDetection: {
21 | id: 'global.spellchecking.autodetect',
22 | defaultMessage: '!!!Detect language automatically',
23 | },
24 | spellcheckerAutomaticDetectionShort: {
25 | id: 'global.spellchecking.autodetect.short',
26 | defaultMessage: '!!!Automatic',
27 | },
28 | proRequired: {
29 | id: 'global.franzProRequired',
30 | defaultMessage: '!!!Franz Professional Required',
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/src/styles/fonts.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 | // @import './node_modules/mdi/scss/materialdesignicons.scss';
3 |
4 | @font-face {
5 | font-family: 'Open Sans';
6 | src: url('../assets/fonts/OpenSans-Light.ttf');
7 | font-weight: 300;
8 | font-style: normal;
9 | }
10 |
11 | @font-face {
12 | font-family: 'Open Sans';
13 | src: url('../assets/fonts/OpenSans-Regular.ttf');
14 | font-weight: normal;
15 | font-style: normal;
16 | }
17 |
18 | @font-face {
19 | font-family: 'Open Sans';
20 | src: url('../assets/fonts/OpenSans-Bold.ttf');
21 | font-weight: bold;
22 | font-style: normal;
23 | }
24 |
25 | @font-face {
26 | font-family: 'Open Sans';
27 | src: url('../assets/fonts/OpenSans-BoldItalic.ttf');
28 | font-weight: bold;
29 | font-style: italic;
30 | }
31 |
32 | @font-face {
33 | font-family: 'Open Sans';
34 | src: url('../assets/fonts/OpenSans-ExtraBold.ttf');
35 | font-weight: 800;
36 | font-style: normal;
37 | }
38 |
39 | @font-face {
40 | font-family: 'Open Sans';
41 | src: url('../assets/fonts/OpenSans-ExtraBoldItalic.ttf');
42 | font-weight: 800;
43 | font-style: italic;
44 | }
45 |
--------------------------------------------------------------------------------
/src/containers/subscription/SubscriptionPopupScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import SubscriptionPopup from '../../components/subscription/SubscriptionPopup';
5 | import { isDevMode } from '../../environment';
6 |
7 |
8 | export default class SubscriptionPopupScreen extends Component {
9 | static propTypes = {
10 | params: PropTypes.shape({
11 | url: PropTypes.string.isRequired,
12 | }).isRequired,
13 | }
14 |
15 | state = {
16 | complete: false,
17 | };
18 |
19 | completeCheck(event) {
20 | const { url } = event;
21 |
22 | if ((url.includes('recurly') && url.includes('confirmation')) || ((url.includes('meetfranz') || isDevMode) && url.includes('success'))) {
23 | this.setState({
24 | complete: true,
25 | });
26 | }
27 | }
28 |
29 | render() {
30 | return (
31 | window.close()}
34 | completeCheck={e => this.completeCheck(e)}
35 | isCompleted={this.state.complete}
36 | />
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/I18n.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import { IntlProvider } from 'react-intl';
5 |
6 | import { oneOrManyChildElements } from './prop-types';
7 | import translations from './i18n/translations';
8 | import UserStore from './stores/UserStore';
9 |
10 | export default @inject('stores') @observer class I18N extends Component {
11 | // componentDidUpdate() {
12 | // window.franz.menu.rebuild();
13 | // }
14 |
15 | render() {
16 | const { stores, children } = this.props;
17 | const { locale } = stores.app;
18 | return (
19 | { window.franz.intl = intlProvider ? intlProvider.getChildContext().intl : null; }}
22 | >
23 | {children}
24 |
25 | );
26 | }
27 | }
28 |
29 | I18N.wrappedComponent.propTypes = {
30 | stores: PropTypes.shape({
31 | user: PropTypes.instanceOf(UserStore).isRequired,
32 | }).isRequired,
33 | children: oneOrManyChildElements.isRequired,
34 | };
35 |
--------------------------------------------------------------------------------
/src/styles/main.scss:
--------------------------------------------------------------------------------
1 | $mdi-font-path: '../node_modules/mdi/fonts';
2 | @if $env == development {
3 | $mdi-font-path: '../../node_modules/mdi/fonts';
4 | }
5 |
6 | @import './node_modules/mdi/scss/materialdesignicons.scss';
7 |
8 | // modules
9 | @import './reset.scss';
10 | @import './util.scss';
11 | @import './layout.scss';
12 | @import './tabs.scss';
13 | @import './services.scss';
14 | @import './settings.scss';
15 | @import './service-table.scss';
16 | @import './recipes.scss';
17 | @import './fonts.scss';
18 | @import './type.scss';
19 | @import './welcome.scss';
20 | @import './auth.scss';
21 | @import './tooltip.scss';
22 | @import './info-bar.scss';
23 | @import './status-bar-target-url.scss';
24 | @import './animations.scss';
25 | @import './infobox.scss';
26 | @import './badge.scss';
27 | @import './subscription.scss';
28 | @import './subscription-popup.scss';
29 | @import './content-tabs.scss';
30 | @import './invite.scss';
31 |
32 | // form
33 | @import './input.scss';
34 | @import './radio.scss';
35 | @import './toggle.scss';
36 | @import './button.scss';
37 | @import './searchInput.scss';
38 | @import './select.scss';
39 | @import './image-upload.scss';
40 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Configuration for probot-stale - https://github.com/probot/stale
2 |
3 | # Number of days of inactivity before an Issue or Pull Request becomes stale
4 | daysUntilStale: 365 # 1 year
5 |
6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
8 | daysUntilClose: -1 # Close the issue almost immediately. See: https://github.com/probot/stale/issues/131
9 |
10 | # Issues with these labels will never be considered stale
11 | exemptLabels:
12 | - blocker
13 | - security
14 | - feature request
15 | - bug
16 |
17 | # Label to use when marking an issue as stale
18 | staleLabel: "[Status] Stale"
19 |
20 | # Comment to post when marking an issue as stale. Set to `false` to disable
21 | markComment: >
22 | This issue has been automatically marked as stale because it has not had
23 | recent activity. It will be closed if no further activity occurs. Thank you
24 | for your contributions.
25 |
26 | # Comment to post when closing a stale issue. Set to `false` to disable
27 | closeComment: false
28 |
--------------------------------------------------------------------------------
/src/webview/desktopCapturer.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import { SET_DESKTOP_CAPTURER_SOURCES_IPC_KEY } from '../features/desktopCapturer/config';
3 | import { OVERLAY_OPEN } from '../ipcChannels';
4 |
5 | function getDisplayMedia() {
6 | return new Promise(async (resolve, reject) => {
7 | ipcRenderer.once(SET_DESKTOP_CAPTURER_SOURCES_IPC_KEY, async (event, { sourceId }) => {
8 | const stream = await navigator.mediaDevices.getUserMedia({
9 | audio: false,
10 | video: {
11 | mandatory: {
12 | chromeMediaSource: 'desktop',
13 | chromeMediaSourceId: sourceId,
14 | },
15 | },
16 | });
17 |
18 | resolve(stream);
19 | });
20 |
21 | const overlayAction = await ipcRenderer.invoke(OVERLAY_OPEN, {
22 | route: '/screen-share/{webContentsId}',
23 | modal: false,
24 | width: 600,
25 | });
26 |
27 | setTimeout(() => {
28 | if (overlayAction === 'closed') {
29 | reject(new Error('Source selection canceled'));
30 | }
31 | }, 250);
32 | });
33 | }
34 |
35 | window.navigator.mediaDevices.getDisplayMedia = getDisplayMedia;
36 |
--------------------------------------------------------------------------------
/packages/forms/src/input/scorePassword.ts:
--------------------------------------------------------------------------------
1 | interface ILetters {
2 | [key: string]: number;
3 | }
4 |
5 | interface IVariations {
6 | [index: string]: boolean;
7 | digits: boolean;
8 | lower: boolean;
9 | nonWords: boolean;
10 | upper: boolean;
11 | }
12 |
13 | export function scorePasswordFunc(password: string): number {
14 | let score: number = 0;
15 | if (!password) {
16 | return score;
17 | }
18 |
19 | // award every unique letter until 5 repetitions
20 | const letters: ILetters = {};
21 | for (let i = 0; i < password.length; i += 1) {
22 | letters[password[i]] = (letters[password[i]] || 0) + 1;
23 | score += 5.0 / letters[password[i]];
24 | }
25 |
26 | // bonus points for mixing it up
27 | const variations: IVariations = {
28 | digits: /\d/.test(password),
29 | lower: /[a-z]/.test(password),
30 | nonWords: /\W/.test(password),
31 | upper: /[A-Z]/.test(password),
32 | };
33 |
34 | let variationCount = 0;
35 | Object.keys(variations).forEach((key) => {
36 | variationCount += (variations[key] === true) ? 1 : 0;
37 | });
38 |
39 | score += (variationCount - 1) * 10;
40 |
41 | return Math.round(score);
42 | }
43 |
--------------------------------------------------------------------------------
/src/features/trialStatusBar/components/ProgressBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import injectSheet from 'react-jss';
5 |
6 | const styles = theme => ({
7 | root: {
8 | background: theme.trialStatusBar.progressBar.background,
9 | width: '25%',
10 | maxWidth: 200,
11 | height: 8,
12 | display: 'flex',
13 | alignItems: 'center',
14 | borderRadius: theme.borderRadius,
15 | overflow: 'hidden',
16 | },
17 | progress: {
18 | background: theme.trialStatusBar.progressBar.progressIndicator,
19 | width: ({ percent }) => `${percent}%`,
20 | height: '100%',
21 | },
22 | });
23 |
24 | @injectSheet(styles) @observer
25 | class ProgressBar extends Component {
26 | static propTypes = {
27 | classes: PropTypes.object.isRequired,
28 | };
29 |
30 | render() {
31 | const {
32 | classes,
33 | } = this.props;
34 |
35 | return (
36 |
41 | );
42 | }
43 | }
44 |
45 | export default ProgressBar;
46 |
--------------------------------------------------------------------------------
/src/webview/darkmode.js:
--------------------------------------------------------------------------------
1 | /* eslint no-bitwise: ["error", { "int32Hint": true }] */
2 |
3 | import path from 'path';
4 | import fs from 'fs-extra';
5 |
6 | const debug = require('debug')('Franz:DarkMode');
7 |
8 | const chars = [...'abcdefghijklmnopqrstuvwxyz'];
9 |
10 | const ID = [...Array(20)].map(() => chars[Math.random() * chars.length | 0]).join``;
11 |
12 | export function injectDarkModeStyle(recipePath) {
13 | const darkModeStyle = path.join(recipePath, 'darkmode.css');
14 | if (fs.pathExistsSync(darkModeStyle)) {
15 | const data = fs.readFileSync(darkModeStyle);
16 | const styles = document.createElement('style');
17 | styles.id = ID;
18 | styles.innerHTML = data.toString();
19 |
20 | document.querySelector('head').appendChild(styles);
21 |
22 | debug('Injected Dark Mode style with ID', ID);
23 | }
24 | }
25 |
26 | export function removeDarkModeStyle() {
27 | const style = document.querySelector(`#${ID}`);
28 |
29 | if (style) {
30 | style.remove();
31 |
32 | debug('Removed Dark Mode Style with ID', ID);
33 | }
34 | }
35 |
36 | export function isDarkModeStyleInjected() {
37 | return !!document.querySelector(`#${ID}`);
38 | }
39 |
--------------------------------------------------------------------------------
/docs/example-feature/index.js:
--------------------------------------------------------------------------------
1 | import { reaction, runInAction } from 'mobx';
2 | import { ExampleFeatureStore } from './store';
3 | import state, { resetState } from './state';
4 | import api from './api';
5 |
6 | const debug = require('debug')('Franz:feature:EXAMPLE_FEATURE');
7 |
8 | let store = null;
9 |
10 | export default function initAnnouncements(stores, actions) {
11 | const { features } = stores;
12 |
13 | // Toggle workspace feature
14 | reaction(
15 | () => (
16 | features.features.isExampleFeatureEnabled
17 | ),
18 | (isEnabled) => {
19 | if (isEnabled) {
20 | debug('Initializing `EXAMPLE_FEATURE` feature');
21 | store = new ExampleFeatureStore(stores, api, actions, state);
22 | store.initialize();
23 | runInAction(() => { state.isFeatureActive = true; });
24 | } else if (store) {
25 | debug('Disabling `EXAMPLE_FEATURE` feature');
26 | runInAction(() => { state.isFeatureActive = false; });
27 | store.teardown();
28 | store = null;
29 | resetState(); // Reset state to default
30 | }
31 | },
32 | {
33 | fireImmediately: true,
34 | },
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/packages/typings/types/react-loader.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for react-loader 2.4
2 | // Project: https://github.com/quickleft/react-loader
3 | // Definitions by: Sudarsan Balaji
4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5 | // TypeScript Version: 2.8
6 |
7 | import { Component } from 'react';
8 |
9 | interface LoaderOptions {
10 | lines?: number;
11 | length?: number;
12 | width?: number;
13 | radius?: number;
14 | scale?: number;
15 | corners?: number;
16 | color?: string;
17 | opacity?: number;
18 | rotate?: number;
19 | direction?: number;
20 | speed?: number;
21 | trail?: number;
22 | fps?: number;
23 | zIndex?: number;
24 | top?: string;
25 | left?: string;
26 | shadow?: boolean;
27 | hwaccel?: boolean;
28 | position?: string;
29 | loadedClassName?: string;
30 | parentClassName?: string;
31 | }
32 |
33 | interface LoaderProps extends LoaderOptions {
34 | loaded: boolean;
35 | options?: LoaderOptions;
36 | className?: string;
37 | }
38 |
39 | declare class ReactLoader extends Component {
40 | }
41 |
42 | declare namespace ReactLoader {
43 | }
44 |
45 | export = ReactLoader;
46 |
--------------------------------------------------------------------------------
/packages/ui/src/icon/index.tsx:
--------------------------------------------------------------------------------
1 | import MdiIcon from '@mdi/react';
2 | import { Theme } from '@meetfranz/theme';
3 | import classnames from 'classnames';
4 | import React, { Component } from 'react';
5 | import injectStyle from 'react-jss';
6 |
7 | import { IWithStyle } from '../typings/generic';
8 |
9 | interface IProps extends IWithStyle {
10 | icon: string;
11 | size?: number;
12 | className?: string;
13 | }
14 |
15 | const styles = (theme: Theme) => ({
16 | icon: {
17 | fill: theme.colorText,
18 | },
19 | });
20 |
21 | class IconComponent extends Component {
22 | public static defaultProps = {
23 | size: 1,
24 | };
25 |
26 | render() {
27 | const {
28 | classes,
29 | icon,
30 | size,
31 | className,
32 | } = this.props;
33 |
34 | if (!icon) {
35 | console.warn('No Icon specified');
36 | }
37 |
38 | return (
39 |
47 | );
48 | }
49 | }
50 |
51 | export const Icon = injectStyle(styles)(IconComponent);
52 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | import defineActions from './lib/actions';
4 | import service from './service';
5 | import recipe from './recipe';
6 | import recipePreview from './recipePreview';
7 | import ui from './ui';
8 | import app from './app';
9 | import user from './user';
10 | import payment from './payment';
11 | import news from './news';
12 | import settings from './settings';
13 | import requests from './requests';
14 | import announcements from '../features/announcements/actions';
15 | import workspaces from '../features/workspaces/actions';
16 | import todos from '../features/todos/actions';
17 | import planSelection from '../features/planSelection/actions';
18 | import trialStatusBar from '../features/trialStatusBar/actions';
19 |
20 | const actions = Object.assign({}, {
21 | service,
22 | recipe,
23 | recipePreview,
24 | ui,
25 | app,
26 | user,
27 | payment,
28 | news,
29 | settings,
30 | requests,
31 | });
32 |
33 | export default Object.assign(
34 | defineActions(actions, PropTypes.checkPropTypes),
35 | { announcements },
36 | { workspaces },
37 | { todos },
38 | { planSelection },
39 | { trialStatusBar },
40 | );
41 |
--------------------------------------------------------------------------------
/src/features/todos/preload.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | // import { DEFAULT_WEB_CONTENTS_ID } from '../../config';
3 | import { IPC } from './constants';
4 |
5 | const debug = require('debug')('Franz:feature:todos:preload');
6 |
7 | debug('Preloading Todos Webview');
8 |
9 | let hostMessageListener = ({ action }) => {
10 | switch (action) {
11 | case 'todos:initialize-as-service': ipcRenderer.send('hello'); break;
12 | default:
13 | }
14 | };
15 |
16 | ipcRenderer.send('hello');
17 |
18 | // ipcRenderer.on('initialize-recipe', () => {
19 | // // ipcRenderer.sendTo(1, IPC.TODOS_HOST_CHANNEL, { action: 'todos:initialized' });
20 | // });
21 |
22 | window.franz = {
23 | onInitialize(ipcHostMessageListener) {
24 | hostMessageListener = ipcHostMessageListener;
25 | ipcRenderer.send(IPC.TODOS_CLIENT_CHANNEL, { action: 'todos:initialized' });
26 | },
27 | sendToHost(message) {
28 | console.log('send to host', message);
29 | ipcRenderer.send(IPC.TODOS_CLIENT_CHANNEL, message);
30 | },
31 | };
32 |
33 | ipcRenderer.on(IPC.TODOS_HOST_CHANNEL, (event, message) => {
34 | debug('Received host message', event, message);
35 | hostMessageListener(message);
36 | });
37 |
--------------------------------------------------------------------------------
/src/containers/auth/ImportScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import Import from '../../components/auth/Import';
5 | import UserStore from '../../stores/UserStore';
6 |
7 | export default @inject('stores', 'actions') @observer class ImportScreen extends Component {
8 | render() {
9 | const { actions, stores } = this.props;
10 |
11 | if (stores.user.isImportLegacyServicesCompleted) {
12 | stores.router.push(stores.user.inviteRoute);
13 | }
14 |
15 | return (
16 |
22 | );
23 | }
24 | }
25 |
26 | ImportScreen.wrappedComponent.propTypes = {
27 | actions: PropTypes.shape({
28 | user: PropTypes.shape({
29 | importLegacyServices: PropTypes.func.isRequired,
30 | }).isRequired,
31 | }).isRequired,
32 | stores: PropTypes.shape({
33 | user: PropTypes.instanceOf(UserStore).isRequired,
34 | }).isRequired,
35 | };
36 |
--------------------------------------------------------------------------------
/packages/forms/src/textarea/styles.ts:
--------------------------------------------------------------------------------
1 | import { Theme } from '@meetfranz/theme';
2 |
3 | export default (theme: Theme) => ({
4 | label: {
5 | '& > div': {
6 | marginTop: 5,
7 | },
8 | },
9 | disabled: {
10 | opacity: theme.inputDisabledOpacity,
11 | },
12 | formModifier: {
13 | background: 'none',
14 | border: 0,
15 | borderLeft: theme.inputBorder,
16 | padding: '4px 20px 0',
17 | outline: 'none',
18 |
19 | '&:active': {
20 | opacity: 0.5,
21 | },
22 |
23 | '& svg': {
24 | fill: theme.inputModifierColor,
25 | },
26 | },
27 | textarea: {
28 | background: 'none',
29 | border: 0,
30 | fontSize: theme.uiFontSize,
31 | outline: 'none',
32 | padding: 8,
33 | width: '100%',
34 | color: theme.inputColor,
35 |
36 | '&::placeholder': {
37 | color: theme.inputPlaceholderColor,
38 | },
39 | },
40 | wrapper: {
41 | background: theme.inputBackground,
42 | border: theme.inputBorder,
43 | borderRadius: theme.borderRadiusSmall,
44 | boxSizing: 'border-box',
45 | display: 'flex',
46 | order: 1,
47 | width: '100%',
48 | },
49 | hasError: {
50 | borderColor: theme.brandDanger,
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/src/stores/lib/Store.js:
--------------------------------------------------------------------------------
1 | import { computed, observable } from 'mobx';
2 | import Reaction from './Reaction';
3 |
4 | export default class Store {
5 | stores = {};
6 |
7 | api = {};
8 |
9 | actions = {};
10 |
11 | _reactions = [];
12 |
13 | // status implementation
14 | @observable _status = null;
15 |
16 | @computed get actionStatus() {
17 | return this._status || [];
18 | }
19 |
20 | set actionStatus(status) {
21 | this._status = status;
22 | }
23 |
24 | constructor(stores, api, actions) {
25 | this.stores = stores;
26 | this.api = api;
27 | this.actions = actions;
28 | }
29 |
30 | registerReactions(reactions) {
31 | reactions.forEach((reaction) => {
32 | if (Array.isArray(reaction)) {
33 | this._reactions.push(new Reaction(reaction[0], reaction[1]));
34 | } else {
35 | this._reactions.push(new Reaction(reaction));
36 | }
37 | });
38 | }
39 |
40 | setup() {}
41 |
42 | initialize() {
43 | this.setup();
44 | this._reactions.forEach(reaction => reaction.start());
45 | }
46 |
47 | teardown() {
48 | this._reactions.forEach(reaction => reaction.stop());
49 | }
50 |
51 | resetStatus() {
52 | this._status = null;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/features/serviceLimit/store.js:
--------------------------------------------------------------------------------
1 | import { computed, observable } from 'mobx';
2 | import { FeatureStore } from '../utils/FeatureStore';
3 | import { DEFAULT_SERVICE_LIMIT } from '.';
4 |
5 | const debug = require('debug')('Franz:feature:serviceLimit:store');
6 |
7 | export class ServiceLimitStore extends FeatureStore {
8 | @observable isServiceLimitEnabled = false;
9 |
10 | start(stores, actions) {
11 | debug('start');
12 | this.stores = stores;
13 | this.actions = actions;
14 |
15 | this.isServiceLimitEnabled = true;
16 | }
17 |
18 | stop() {
19 | super.stop();
20 |
21 | this.isServiceLimitEnabled = false;
22 | }
23 |
24 | @computed get userHasReachedServiceLimit() {
25 | if (!this.isServiceLimitEnabled) return false;
26 |
27 | return this.serviceLimit !== 0 && this.serviceCount >= this.serviceLimit;
28 | }
29 |
30 | @computed get serviceLimit() {
31 | if (!this.isServiceLimitEnabled || this.stores.features.features.serviceLimitCount === 0) return 0;
32 |
33 | return this.stores.features.features.serviceLimitCount || DEFAULT_SERVICE_LIMIT;
34 | }
35 |
36 | @computed get serviceCount() {
37 | return this.stores.services.all.length;
38 | }
39 | }
40 |
41 | export default ServiceLimitStore;
42 |
--------------------------------------------------------------------------------
/src/features/workspaces/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import WorkspacesStore from './store';
3 | import { resetApiRequests } from './api';
4 |
5 | const debug = require('debug')('Franz:feature:workspaces');
6 |
7 | export const GA_CATEGORY_WORKSPACES = 'Workspaces';
8 | export const DEFAULT_SETTING_KEEP_ALL_WORKSPACES_LOADED = false;
9 |
10 | export const workspaceStore = new WorkspacesStore();
11 |
12 | export default function initWorkspaces(stores, actions) {
13 | stores.workspaces = workspaceStore;
14 | const { features } = stores;
15 |
16 | // Toggle workspace feature
17 | reaction(
18 | () => features.features.isWorkspaceEnabled,
19 | (isEnabled) => {
20 | if (isEnabled && !workspaceStore.isFeatureActive) {
21 | debug('Initializing `workspaces` feature');
22 | workspaceStore.start(stores, actions);
23 | } else if (workspaceStore.isFeatureActive) {
24 | debug('Disabling `workspaces` feature');
25 | workspaceStore.stop();
26 | resetApiRequests();
27 | }
28 | },
29 | {
30 | fireImmediately: true,
31 | },
32 | );
33 | }
34 |
35 | export const WORKSPACES_ROUTES = {
36 | ROOT: '/settings/workspaces',
37 | EDIT: '/settings/workspaces/:action/:id',
38 | };
39 |
--------------------------------------------------------------------------------
/packages/forms/src/label/index.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { Component } from 'react';
3 | import injectSheet from 'react-jss';
4 |
5 | import { IFormField } from '../typings/generic';
6 |
7 | import styles from './styles';
8 |
9 | interface ILabel extends IFormField, React.LabelHTMLAttributes {
10 | classes: any;
11 | isRequired: boolean;
12 | }
13 |
14 | class LabelComponent extends Component {
15 | static defaultProps = {
16 | showLabel: true,
17 | };
18 |
19 | render() {
20 | const {
21 | title,
22 | showLabel,
23 | classes,
24 | className,
25 | children,
26 | htmlFor,
27 | isRequired,
28 | } = this.props;
29 |
30 | if (!showLabel) return children;
31 |
32 | return (
33 |
49 | );
50 | }
51 | }
52 |
53 | export const Label = injectSheet(styles)(LabelComponent);
54 |
--------------------------------------------------------------------------------
/src/helpers/plan-helpers.js:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 | import { PLANS_MAPPING, PLANS } from '../config';
3 |
4 | const messages = defineMessages({
5 | [PLANS.PRO]: {
6 | id: 'pricing.plan.pro',
7 | defaultMessage: '!!!Professional',
8 | },
9 | [PLANS.PERSONAL]: {
10 | id: 'pricing.plan.personal',
11 | defaultMessage: '!!!Personal',
12 | },
13 | [PLANS.FREE]: {
14 | id: 'pricing.plan.free',
15 | defaultMessage: '!!!Free',
16 | },
17 | [PLANS.LEGACY]: {
18 | id: 'pricing.plan.legacy',
19 | defaultMessage: '!!!Premium',
20 | },
21 | });
22 |
23 | export function cleanupPlanId(id) {
24 | return id.replace(/(.*)-x[0-9]/, '$1');
25 | }
26 |
27 | export function i18nPlanName(planId, intl) {
28 | if (!planId) {
29 | throw new Error('planId is required');
30 | }
31 |
32 | if (!intl) {
33 | throw new Error('intl context is required');
34 | }
35 |
36 | const id = cleanupPlanId(planId);
37 |
38 | const plan = PLANS_MAPPING[id];
39 |
40 | return intl.formatMessage(messages[plan]);
41 | }
42 |
43 | export function getPlan(planId) {
44 | if (!planId) {
45 | throw new Error('planId is required');
46 | }
47 |
48 | const id = cleanupPlanId(planId);
49 |
50 | const plan = PLANS_MAPPING[id];
51 |
52 | return plan;
53 | }
54 |
--------------------------------------------------------------------------------
/packages/ui/src/loader/index.tsx:
--------------------------------------------------------------------------------
1 | import { Theme } from '@meetfranz/theme';
2 | import classnames from 'classnames';
3 | import React, { Component } from 'react';
4 | import injectStyle, { withTheme } from 'react-jss';
5 | import ReactLoader from 'react-loader';
6 |
7 | import { IWithStyle } from '../typings/generic';
8 |
9 | interface IProps extends IWithStyle {
10 | className?: string;
11 | color?: string;
12 | }
13 |
14 | const styles = (theme: Theme) => ({
15 | container: {
16 | position: 'relative',
17 | height: 60,
18 | },
19 | });
20 |
21 | class LoaderComponent extends Component {
22 | render() {
23 | const {
24 | classes,
25 | className,
26 | color,
27 | theme,
28 | } = this.props;
29 |
30 | return (
31 |
38 |
45 |
46 | );
47 | }
48 | }
49 |
50 | export const Loader = injectStyle(styles)(withTheme(LoaderComponent));
51 |
--------------------------------------------------------------------------------
/src/styles/infobox.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .infobox {
4 | align-items: center;
5 | border-radius: $theme-border-radius-small;
6 | display: flex;
7 | height: auto;
8 | margin-bottom: 30px;
9 | padding: 15px 20px;
10 |
11 | a { color: #FFF; }
12 |
13 | .infobox__content { flex: 1; }
14 |
15 | &.infobox--success {
16 | background: $theme-brand-success;
17 | color: #FFF;
18 | }
19 |
20 | &.infobox--primary {
21 | background: $theme-brand-primary;
22 | color: #FFF;
23 | }
24 |
25 | &.infobox--danger {
26 | background: $theme-brand-danger;
27 | color: #FFF;
28 | }
29 |
30 | &.infobox--warning {
31 | background: $theme-brand-warning;
32 | color: #FFF;
33 | }
34 |
35 | .mdi { margin-right: 10px; }
36 |
37 | .infobox__cta {
38 | border-color: #FFF;
39 | border-radius: $theme-border-radius-small;
40 | border-style: solid;
41 | border-width: 2px;
42 | color: #FFF;
43 | margin-left: 15px;
44 | padding: 3px 8px;
45 |
46 | .loader {
47 | display: inline-block;
48 | height: 12px;
49 | margin-right: 5px;
50 | position: relative;
51 | width: 20px;
52 | z-index: 9999;
53 | }
54 | }
55 |
56 | .infobox__delete {
57 | color: #FFF;
58 | margin-right: 0;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/environment.js:
--------------------------------------------------------------------------------
1 | import {
2 | DEV_API,
3 | DEV_API_WEBSITE,
4 | GA_ID_DEV,
5 | GA_ID_PROD,
6 | LIVE_API,
7 | LIVE_API_WEBSITE,
8 | LOCAL_API,
9 | LOCAL_API_WEBSITE,
10 | } from './config';
11 |
12 | const { app } = process.type === 'renderer' ? require('@electron/remote') : require('electron');
13 |
14 | export const isDevMode = !app.isPackaged;
15 | export const useLiveAPI = process.env.LIVE_API;
16 | export const useLocalAPI = process.env.LOCAL_API;
17 |
18 | let { platform } = process;
19 | if (process.env.OS_PLATFORM) {
20 | platform = process.env.OS_PLATFORM;
21 | }
22 |
23 | export const isMac = platform === 'darwin';
24 | export const isWindows = platform === 'win32';
25 | export const isLinux = platform === 'linux';
26 |
27 | export const ctrlKey = isMac ? '⌘' : 'Ctrl';
28 | export const cmdKey = isMac ? 'Cmd' : 'Ctrl';
29 |
30 | let api;
31 | let web;
32 | if (!isDevMode || (isDevMode && useLiveAPI)) {
33 | api = LIVE_API;
34 | web = LIVE_API_WEBSITE;
35 | } else if (isDevMode && useLocalAPI) {
36 | api = LOCAL_API;
37 | web = LOCAL_API_WEBSITE;
38 | } else {
39 | api = DEV_API;
40 | web = DEV_API_WEBSITE;
41 | }
42 |
43 | export const API = api;
44 | export const API_VERSION = 'v1';
45 | export const WEBSITE = web;
46 |
47 | export const GA_ID = !isDevMode ? GA_ID_PROD : GA_ID_DEV;
48 |
--------------------------------------------------------------------------------
/src/webview/notifications.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import uuidV1 from 'uuid/v1';
3 |
4 | const debug = require('debug')('Franz:Notifications');
5 |
6 | class Notification {
7 | static permission = 'granted';
8 |
9 | constructor(title = '', options = {}) {
10 | debug('New notification', title, options);
11 | this.title = title;
12 | this.options = options;
13 | this.notificationId = uuidV1();
14 |
15 | ipcRenderer.send('notification', this.onNotify({
16 | title: this.title,
17 | options: this.options,
18 | notificationId: this.notificationId,
19 | }));
20 |
21 | ipcRenderer.once(`notification-onclick:${this.notificationId}`, () => {
22 | if (typeof this.onclick === 'function') {
23 | this.onclick();
24 | }
25 | });
26 | }
27 |
28 | static requestPermission(cb = null) {
29 | if (!cb) {
30 | return new Promise((resolve) => {
31 | resolve(Notification.permission);
32 | });
33 | }
34 |
35 | if (typeof (cb) === 'function') {
36 | return cb(Notification.permission);
37 | }
38 |
39 | return Notification.permission;
40 | }
41 |
42 | onNotify(data) {
43 | return data;
44 | }
45 |
46 | onClick() {}
47 |
48 | close() {}
49 | }
50 |
51 | window.Notification = Notification;
52 |
--------------------------------------------------------------------------------
/src/containers/settings/InviteScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 |
5 | import Invite from '../../components/auth/Invite';
6 | import ErrorBoundary from '../../components/util/ErrorBoundary';
7 |
8 | export default @inject('stores', 'actions') @observer class InviteScreen extends Component {
9 | componentWillUnmount() {
10 | this.props.stores.user.inviteRequest.reset();
11 | }
12 |
13 | render() {
14 | const { actions } = this.props;
15 | const { user } = this.props.stores;
16 |
17 | return (
18 |
19 |
25 |
26 | );
27 | }
28 | }
29 |
30 | InviteScreen.wrappedComponent.propTypes = {
31 | actions: PropTypes.shape({
32 | user: PropTypes.shape({
33 | invite: PropTypes.func.isRequired,
34 | }).isRequired,
35 | }).isRequired,
36 | stores: PropTypes.shape({
37 | user: PropTypes.shape({
38 | inviteRequest: PropTypes.object,
39 | }).isRequired,
40 | }).isRequired,
41 | };
42 |
--------------------------------------------------------------------------------
/src/features/workspaces/components/WorkspaceItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { intlShape } from 'react-intl';
4 | import { observer } from 'mobx-react';
5 | import injectSheet from 'react-jss';
6 |
7 | import Workspace from '../models/Workspace';
8 |
9 | const styles = theme => ({
10 | row: {
11 | height: theme.workspaces.settings.listItems.height,
12 | borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`,
13 | '&:hover': {
14 | background: theme.workspaces.settings.listItems.hoverBgColor,
15 | },
16 | },
17 | columnName: {},
18 | });
19 |
20 | @injectSheet(styles) @observer
21 | class WorkspaceItem extends Component {
22 | static propTypes = {
23 | classes: PropTypes.object.isRequired,
24 | workspace: PropTypes.instanceOf(Workspace).isRequired,
25 | onItemClick: PropTypes.func.isRequired,
26 | };
27 |
28 | static contextTypes = {
29 | intl: intlShape,
30 | };
31 |
32 | render() {
33 | const { classes, workspace, onItemClick } = this.props;
34 |
35 | return (
36 |
37 | | onItemClick(workspace)}>
38 | {workspace.name}
39 | |
40 |
41 | );
42 | }
43 | }
44 |
45 | export default WorkspaceItem;
46 |
--------------------------------------------------------------------------------
/src/api/UserApi.js:
--------------------------------------------------------------------------------
1 | import { hash } from '../helpers/password-helpers';
2 |
3 | export default class UserApi {
4 | constructor(server, local) {
5 | this.server = server;
6 | this.local = local;
7 | }
8 |
9 | login(email, password) {
10 | return this.server.login(email, hash(password));
11 | }
12 |
13 | logout() {
14 | return this;
15 | }
16 |
17 | signup(data) {
18 | Object.assign(data, {
19 | password: hash(data.password),
20 | });
21 | return this.server.signup(data);
22 | }
23 |
24 | password(email) {
25 | return this.server.retrievePassword(email);
26 | }
27 |
28 | activateTrial(data) {
29 | return this.server.activateTrial(data);
30 | }
31 |
32 | invite(data) {
33 | return this.server.inviteUser(data);
34 | }
35 |
36 | getInfo() {
37 | return this.server.userInfo();
38 | }
39 |
40 | updateInfo(data) {
41 | const userData = data;
42 | if (userData.oldPassword && userData.newPassword) {
43 | userData.oldPassword = hash(userData.oldPassword);
44 | userData.newPassword = hash(userData.newPassword);
45 | }
46 |
47 | return this.server.updateUserInfo(userData);
48 | }
49 |
50 | getLegacyServices() {
51 | return this.server.getLegacyServices();
52 | }
53 |
54 | delete() {
55 | return this.server.deleteAccount();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/ui/effects/Appear.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-did-mount-set-state */
2 | import React, { Component } from 'react';
3 | import PropTypes from 'prop-types';
4 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
5 |
6 | export default class Appear extends Component {
7 | static propTypes = {
8 | children: PropTypes.any.isRequired, // eslint-disable-line
9 | transitionName: PropTypes.string,
10 | className: PropTypes.string,
11 | };
12 |
13 | static defaultProps = {
14 | transitionName: 'fadeIn',
15 | className: '',
16 | };
17 |
18 | state = {
19 | mounted: false,
20 | };
21 |
22 | componentDidMount() {
23 | this.setState({ mounted: true });
24 | }
25 |
26 | render() {
27 | const {
28 | children,
29 | transitionName,
30 | className,
31 | } = this.props;
32 |
33 | if (!this.state.mounted) {
34 | return null;
35 | }
36 |
37 | return (
38 |
47 | {children}
48 |
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/theme/src/themes/legacy/index.ts:
--------------------------------------------------------------------------------
1 | /* legacy config, injected into sass */
2 | export const themeBrandPrimary = '#3498db';
3 | export const themeBrandSuccess = '#5cb85c';
4 | export const themeBrandInfo = '#5bc0de';
5 | export const themeBrandWarning = '#FF9F00';
6 | export const themeBrandDanger = '#d9534f';
7 |
8 | export const themeGrayDark = '#373a3c';
9 | export const themeGray = '#55595c';
10 | export const themeGrayLight = '#818a91';
11 | export const themeGrayLighter = '#eceeef';
12 | export const themeGrayLightest = '#f7f7f9';
13 |
14 | export const themeBorderRadius = '6px';
15 | export const themeBorderRadiusSmall = '3px';
16 |
17 | export const themeSidebarWidth = '68px';
18 |
19 | export const themeTextColor = themeGrayDark;
20 |
21 | export const themeTransitionTime = '.5s';
22 |
23 | export const themeInsetShadow = 'inset 0 2px 5px rgba(0, 0, 0, .03)';
24 |
25 | export const darkThemeBlack = '#1A1A1A';
26 |
27 | export const darkThemeGrayDarkest = '#1E1E1E';
28 | export const darkThemeGrayDarker = '#2D2F31';
29 | export const darkThemeGrayDark = '#383A3B';
30 |
31 | export const darkThemeGray = '#47494B';
32 |
33 | export const darkThemeGrayLight = '#515355';
34 | export const darkThemeGrayLighter = '#8a8b8b';
35 | export const darkThemeGrayLightest = '#FFFFFF';
36 |
37 | export const darkThemeGraySmoke = '#CED0D1';
38 | export const darkThemeTextColor = '#FFFFFF';
39 |
--------------------------------------------------------------------------------
/src/components/services/content/ServiceDisabled.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import { defineMessages, intlShape } from 'react-intl';
5 |
6 | import Button from '../../ui/Button';
7 |
8 | const messages = defineMessages({
9 | headline: {
10 | id: 'service.disabledHandler.headline',
11 | defaultMessage: '!!!{name} is disabled',
12 | },
13 | action: {
14 | id: 'service.disabledHandler.action',
15 | defaultMessage: '!!!Enable {name}',
16 | },
17 | });
18 |
19 | export default @observer class ServiceDisabled extends Component {
20 | static propTypes = {
21 | name: PropTypes.string.isRequired,
22 | enable: PropTypes.func.isRequired,
23 | };
24 |
25 | static contextTypes = {
26 | intl: intlShape,
27 | };
28 |
29 | countdownInterval = null;
30 |
31 | countdownIntervalTimeout = 1000;
32 |
33 | render() {
34 | const { name, enable } = this.props;
35 | const { intl } = this.context;
36 |
37 | return (
38 |
39 |
{intl.formatMessage(messages.headline, { name })}
40 |
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/containers/auth/LoginScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import Login from '../../components/auth/Login';
5 | import UserStore from '../../stores/UserStore';
6 |
7 | import { globalError as globalErrorPropType } from '../../prop-types';
8 |
9 | export default @inject('stores', 'actions') @observer class LoginScreen extends Component {
10 | static propTypes = {
11 | error: globalErrorPropType.isRequired,
12 | };
13 |
14 | render() {
15 | const { actions, stores, error } = this.props;
16 | return (
17 |
26 | );
27 | }
28 | }
29 |
30 | LoginScreen.wrappedComponent.propTypes = {
31 | actions: PropTypes.shape({
32 | user: PropTypes.shape({
33 | login: PropTypes.func.isRequired,
34 | }).isRequired,
35 | }).isRequired,
36 | stores: PropTypes.shape({
37 | user: PropTypes.instanceOf(UserStore).isRequired,
38 | }).isRequired,
39 | };
40 |
--------------------------------------------------------------------------------
/src/styles/toggle.scss:
--------------------------------------------------------------------------------
1 | @use "sass:math";
2 | @import './config.scss';
3 |
4 | $toggle-size: 14px;
5 | $toggle-width: 40px;
6 | $toggle-button-size: 22px;
7 |
8 | .theme__dark .franz-form .franz-form__toggle-wrapper .franz-form__toggle {
9 | background: $dark-theme-gray;
10 | border-radius: math.div($toggle-size, 2);
11 |
12 | .franz-form__toggle-button {
13 | background: $dark-theme-gray-lighter;
14 | box-shadow: 0 1px 4px rgba($dark-theme-black, .3);
15 | }
16 | }
17 |
18 | .franz-form .franz-form__toggle-wrapper {
19 | display: flex;
20 | flex-direction: row;
21 |
22 | .franz-form__label { margin-left: 20px; }
23 |
24 | .franz-form__toggle {
25 | background: $theme-gray-lighter;
26 | border-radius: $theme-border-radius;
27 | height: $toggle-size;
28 | position: relative;
29 | width: $toggle-width;
30 |
31 | .franz-form__toggle-button {
32 | background: $theme-gray-light;
33 | border-radius: 100%;
34 | box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
35 | height: $toggle-size - 2;
36 | left: 1px;
37 | top: 1px;
38 | position: absolute;
39 | transition: all .5s;
40 | width: $toggle-size - 2;
41 | }
42 |
43 | &.is-active .franz-form__toggle-button {
44 | background: $theme-brand-primary;
45 | left: $toggle-width - $toggle-size - 3;
46 | }
47 |
48 | input { display: none; }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ### Description
4 |
5 |
6 | ### Motivation and Context
7 |
8 |
9 |
10 | ### How Has This Been Tested?
11 |
12 |
13 |
14 |
15 | ### Screenshots (if appropriate):
16 |
17 | ### Types of changes
18 |
19 | - [ ] Bug fix (non-breaking change which fixes an issue)
20 | - [ ] New feature (non-breaking change which adds functionality)
21 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
22 |
23 | ### Checklist:
24 |
25 |
26 | - [ ] My code follows the code style of this project (run `$ yarn lint`).
27 |
29 |
--------------------------------------------------------------------------------
/src/features/announcements/api.js:
--------------------------------------------------------------------------------
1 | import { app } from '@electron/remote';
2 | import Request from '../../stores/lib/Request';
3 | import { API, API_VERSION } from '../../environment';
4 |
5 | const debug = require('debug')('Franz:feature:announcements:api');
6 |
7 | export const announcementsApi = {
8 | async getCurrentVersion() {
9 | debug('getting current version of electron app');
10 | return Promise.resolve(app.getVersion());
11 | },
12 |
13 | async getChangelog(version) {
14 | debug('fetching release changelog from Github');
15 | const url = `https://api.github.com/repos/meetfranz/franz/releases/tags/v${version}`;
16 | const request = await window.fetch(url, { method: 'GET' });
17 | if (!request.ok) return null;
18 | const data = await request.json();
19 | return data.body;
20 | },
21 |
22 | async getAnnouncement(version) {
23 | debug('fetching release announcement from api');
24 | const url = `${API}/${API_VERSION}/announcements/${version}`;
25 | const response = await window.fetch(url, { method: 'GET' });
26 | if (!response.ok) return null;
27 | return response.json();
28 | },
29 | };
30 |
31 | export const getCurrentVersionRequest = new Request(announcementsApi, 'getCurrentVersion');
32 | export const getChangelogRequest = new Request(announcementsApi, 'getChangelog');
33 | export const getAnnouncementRequest = new Request(announcementsApi, 'getAnnouncement');
34 |
--------------------------------------------------------------------------------
/src/theme/default/legacy.js:
--------------------------------------------------------------------------------
1 | /* legacy config, injected into sass */
2 | export const themeBrandPrimary = '#3498db';
3 | export const themeBrandSuccess = '#5cb85c';
4 | export const themeBrandInfo = '#5bc0de';
5 | export const themeBrandWarning = '#FF9F00';
6 | export const themeBrandDanger = '#d9534f';
7 |
8 | export const themeGrayDark = '#373a3c';
9 | export const themeGray = '#55595c';
10 | export const themeGrayLight = '#818a91';
11 | export const themeGrayLighter = '#eceeef';
12 | export const themeGrayLightest = '#f7f7f9';
13 |
14 | export const themeBorderRadius = '6px';
15 | export const themeBorderRadiusSmall = '3px';
16 |
17 | export const themeSidebarWidth = '68px';
18 |
19 | export const themeTextColor = themeGrayDark;
20 |
21 | export const themeTransitionTime = '.5s';
22 |
23 | export const themeInsetShadow = 'inset 0 2px 5px rgba(0, 0, 0, .03)';
24 |
25 |
26 | export const darkThemeBlack = '#1A1A1A';
27 |
28 | export const darkThemeGrayDarkest = '#1E1E1E';
29 | export const darkThemeGrayDarker = '#2D2F31';
30 | export const darkThemeGrayDark = '#383A3B';
31 |
32 | export const darkThemeGray = '#47494B';
33 |
34 | export const darkThemeGrayLight = '#515355';
35 | export const darkThemeGrayLighter = '#8a8b8b';
36 | export const darkThemeGrayLightest = '#FFFFFF';
37 |
38 | export const darkThemeGraySmoke = '#CED0D1';
39 | export const darkThemeTextColor = '#FFFFFF';
40 |
41 | export const windowsTitleBarHeight = '31px';
42 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/subscriptionWindow.js:
--------------------------------------------------------------------------------
1 | import { BrowserWindow, ipcMain } from 'electron';
2 | import * as remoteMain from '@electron/remote/main';
3 |
4 | const debug = require('debug')('Franz:ipcApi:subscriptionWindow');
5 |
6 | export default async ({ mainWindow }) => {
7 | let subscriptionWindow;
8 | ipcMain.handle('open-inline-subscription-window', async (event, { url }) => {
9 | debug('Opening subscription window with url', url);
10 | try {
11 | const windowBounds = mainWindow.getBounds();
12 |
13 | subscriptionWindow = new BrowserWindow({
14 | parent: mainWindow,
15 | modal: true,
16 | title: '🔒 Franz Supporter License',
17 | width: 800,
18 | height: windowBounds.height - 100,
19 | maxWidth: 800,
20 | minWidth: 600,
21 | webPreferences: {
22 | nodeIntegration: true,
23 | webviewTag: true,
24 | enableRemoteModule: true,
25 | contextIsolation: false,
26 | },
27 | });
28 |
29 | remoteMain.enable(subscriptionWindow.webContents);
30 |
31 | subscriptionWindow.loadURL(`file://${__dirname}/../../index.html#/payment/${encodeURIComponent(url)}`);
32 |
33 | return await new Promise((resolve) => {
34 | subscriptionWindow.on('closed', () => resolve('closed'));
35 | });
36 | // return isDND;
37 | } catch (e) {
38 | console.error(e);
39 | }
40 | });
41 | };
42 |
--------------------------------------------------------------------------------
/uidev/src/withTheme/index.tsx:
--------------------------------------------------------------------------------
1 | import { theme, Theme, ThemeType } from '@meetfranz/theme';
2 | import { Classes } from 'jss';
3 | import React from 'react';
4 | import injectSheet, { ThemeProvider } from 'react-jss';
5 |
6 | const defaultTheme = {
7 | name: 'Default',
8 | variables: theme(ThemeType.default),
9 | };
10 |
11 | const darkTheme = {
12 | name: 'Dark Mode',
13 | variables: theme(ThemeType.dark),
14 | };
15 |
16 | const themes = [defaultTheme, darkTheme];
17 |
18 | const styles = (theme: Theme) => ({
19 | title: {
20 | fontSize: 14,
21 | },
22 | container: {
23 | border: theme.inputBorder,
24 | borderRadius: theme.borderRadiusSmall,
25 | marginBottom: 20,
26 | padding: 20,
27 | background: theme.colorContentBackground,
28 | },
29 | });
30 |
31 | const Container = injectSheet(styles)(({ name, classes, story }: { name: string, classes: Classes, story: React.ReactNode }) => (
32 |
33 | {name}
34 |
35 | {story}
36 |
37 |
38 | ));
39 |
40 | export const WithTheme = ({ children }: {children: React.ReactChild}) => {
41 | return (
42 | <>
43 | {themes.map((theme, key) => (
44 |
45 |
46 |
47 | ))}
48 | >
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/electron/ipc-api/desktopCapturer.ts:
--------------------------------------------------------------------------------
1 | import { desktopCapturer, ipcMain, webContents } from 'electron';
2 | import { RELAY_DESKTOP_CAPTURER_SOURCES_IPC_KEY, REQUEST_DESKTOP_CAPTURER_SOURCES_IPC_KEY, SET_DESKTOP_CAPTURER_SOURCES_IPC_KEY } from '../../features/desktopCapturer/config';
3 |
4 | const debug = require('debug')('Franz:ipcApi:desktopCapturer');
5 |
6 | export default async () => {
7 | ipcMain.handle(REQUEST_DESKTOP_CAPTURER_SOURCES_IPC_KEY, async () => {
8 | try {
9 | const sources = await desktopCapturer.getSources({
10 | types: ['window', 'screen'],
11 | fetchWindowIcons: true,
12 | thumbnailSize: { width: 1920, height: 1080 },
13 | });
14 | debug('Available sources', sources);
15 | return sources.map((source) => {
16 | const thumbnail = source.thumbnail ? source.thumbnail.toDataURL() : null;
17 | const appIcon = source.appIcon ? source.appIcon.toDataURL() : null;
18 |
19 | return {
20 | id: source.id,
21 | name: source.name,
22 | displayId: source.display_id,
23 | thumbnail,
24 | appIcon,
25 | };
26 | });
27 | } catch (e) {
28 | console.error(e);
29 | }
30 | });
31 |
32 | ipcMain.on(RELAY_DESKTOP_CAPTURER_SOURCES_IPC_KEY, (event, { webContentsId, sourceId }) => {
33 | const contents = webContents.fromId(webContentsId);
34 | contents.send(SET_DESKTOP_CAPTURER_SOURCES_IPC_KEY, { sourceId });
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/src/features/workspaces/containers/WorkspacesScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { inject, observer } from 'mobx-react';
3 | import PropTypes from 'prop-types';
4 | import WorkspacesDashboard from '../components/WorkspacesDashboard';
5 | import ErrorBoundary from '../../../components/util/ErrorBoundary';
6 | import { workspaceStore } from '../index';
7 | import {
8 | createWorkspaceRequest,
9 | deleteWorkspaceRequest,
10 | getUserWorkspacesRequest,
11 | updateWorkspaceRequest,
12 | } from '../api';
13 |
14 | @inject('stores', 'actions') @observer
15 | class WorkspacesScreen extends Component {
16 | static propTypes = {
17 | actions: PropTypes.shape({
18 | workspace: PropTypes.shape({
19 | edit: PropTypes.func.isRequired,
20 | }),
21 | }).isRequired,
22 | };
23 |
24 | render() {
25 | const { actions } = this.props;
26 | return (
27 |
28 | actions.workspaces.create(data)}
35 | onWorkspaceClick={w => actions.workspaces.edit({ workspace: w })}
36 | />
37 |
38 | );
39 | }
40 | }
41 |
42 | export default WorkspacesScreen;
43 |
--------------------------------------------------------------------------------
/src/styles/info-bar.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .info-bar {
4 | align-items: center;
5 | background: $theme-brand-primary;
6 | box-shadow: 0 0 8px rgba(black, .2);
7 | display: flex;
8 | height: 50px;
9 | justify-content: center;
10 | padding: 0 20px;
11 | position: relative;
12 | width: 100%;
13 | z-index: 100;
14 |
15 | .info-bar__content {
16 | height: auto;
17 |
18 | .mdi { margin-right: 5px; }
19 | }
20 |
21 | .info-bar__close {
22 | color: #FFF;
23 | position: absolute;
24 | right: 10px;
25 | }
26 |
27 | .info-bar__cta {
28 | border-color: #FFF;
29 | border-radius: $theme-border-radius-small;
30 | border-style: solid;
31 | border-width: 2px;
32 | color: #FFF;
33 | margin-left: 15px;
34 | padding: 3px 8px;
35 |
36 | .loader {
37 | display: inline-block;
38 | height: 12px;
39 | margin-right: 5px;
40 | position: relative;
41 | width: 20px;
42 | z-index: 9999;
43 | }
44 | }
45 |
46 | .info-bar__inline-button {
47 | color: white;
48 | }
49 |
50 | &.info-bar--bottom { order: 10; }
51 |
52 | &.info-bar--primary {
53 | background: $theme-brand-primary;
54 | color: #FFF;
55 |
56 | a { color: #FFF; }
57 | }
58 |
59 | &.info-bar--warning {
60 | background: $theme-brand-warning;
61 | color: #FFF;
62 |
63 | a { color: #FFF; }
64 | }
65 |
66 | &.info-bar--danger {
67 | background: $theme-brand-danger;
68 | color: #FFF;
69 |
70 | a { color: #FFF; }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/styles/service-table.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark .service-table {
4 | .service-table__icon.has-custom-icon { border: 1px solid $dark-theme-gray-dark; }
5 | .service-table__column-info .mdi { color: $dark-theme-gray-lightest; }
6 |
7 | .service-table__row {
8 | border-bottom: 1px solid $dark-theme-gray-darker;
9 |
10 | &:hover { background: $dark-theme-gray-darker; }
11 | &.service-table__row--disabled { color: $dark-theme-gray; }
12 | }
13 | }
14 |
15 | .service-table {
16 | width: 100%;
17 |
18 | .service-table__toggle {
19 | width: 60px;
20 |
21 | .franz-form__field { margin-bottom: 0; }
22 | }
23 |
24 | .service-table__icon {
25 | width: 35px;
26 |
27 | &.has-custom-icon {
28 | border: 1px solid $theme-gray-lighter;
29 | border-radius: $theme-border-radius;
30 | width: 37px;
31 | }
32 | }
33 |
34 | .service-table__column-icon,
35 | .service-table__column-action { width: 40px }
36 |
37 | .service-table__column-info {
38 | width: 40px;
39 |
40 | .mdi {
41 | color: $theme-gray-light;
42 | display: block;
43 | font-size: 18px;
44 | }
45 | }
46 |
47 | .service-table__row {
48 | border-bottom: 1px solid $theme-gray-lightest;
49 |
50 | &:hover { background: $theme-gray-lightest; }
51 |
52 | &.service-table__row--disabled {
53 | color: $theme-gray-light;
54 |
55 | .service-table__column-icon {
56 | filter: grayscale(100%);
57 | opacity: .5;
58 | }
59 | }
60 | }
61 |
62 | td { padding: 10px; }
63 | }
64 |
--------------------------------------------------------------------------------
/src/electron/Settings.js:
--------------------------------------------------------------------------------
1 | import { observable, toJS } from 'mobx';
2 | import { pathExistsSync, outputJsonSync, readJsonSync } from 'fs-extra';
3 | import path from 'path';
4 |
5 | import { SETTINGS_PATH } from '../config';
6 |
7 | const debug = require('debug')('Franz:Settings');
8 |
9 | export default class Settings {
10 | type = '';
11 |
12 | @observable store = {};
13 |
14 | constructor(type, defaultState = {}) {
15 | this.type = type;
16 | this.store = defaultState;
17 | this.defaultState = defaultState;
18 |
19 | if (!pathExistsSync(this.settingsFile)) {
20 | this._writeFile();
21 | } else {
22 | this._hydrate();
23 | }
24 | }
25 |
26 | set(settings) {
27 | this.store = this._merge(settings);
28 |
29 | this._writeFile();
30 | }
31 |
32 | get all() {
33 | return this.store;
34 | }
35 |
36 | get allSerialized() {
37 | return toJS(this.store);
38 | }
39 |
40 | get(key) {
41 | return this.store[key];
42 | }
43 |
44 | _merge(settings) {
45 | return Object.assign(this.defaultState, this.store, settings);
46 | }
47 |
48 | _hydrate() {
49 | this.store = this._merge(readJsonSync(this.settingsFile));
50 | debug('Hydrate store', this.type, toJS(this.store));
51 | }
52 |
53 | _writeFile() {
54 | outputJsonSync(this.settingsFile, this.store, {
55 | spaces: 2,
56 | });
57 | debug('Write settings file', this.type, toJS(this.store));
58 | }
59 |
60 | get settingsFile() {
61 | return path.join(SETTINGS_PATH, `${this.type === 'app' ? 'settings' : this.type}.json`);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/containers/settings/SettingsWindow.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer, inject } from 'mobx-react';
4 |
5 | import ServicesStore from '../../stores/ServicesStore';
6 |
7 | import Layout from '../../components/settings/SettingsLayout';
8 | import Navigation from '../../components/settings/navigation/SettingsNavigation';
9 | import ErrorBoundary from '../../components/util/ErrorBoundary';
10 | import { workspaceStore } from '../../features/workspaces';
11 |
12 | export default @inject('stores', 'actions') @observer class SettingsContainer extends Component {
13 | render() {
14 | const { children, stores } = this.props;
15 | const { closeSettings } = this.props.actions.ui;
16 |
17 |
18 | const navigation = (
19 |
23 | );
24 |
25 | return (
26 |
27 |
31 | {children}
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | SettingsContainer.wrappedComponent.propTypes = {
39 | children: PropTypes.element.isRequired,
40 | stores: PropTypes.shape({
41 | services: PropTypes.instanceOf(ServicesStore).isRequired,
42 | }).isRequired,
43 | actions: PropTypes.shape({
44 | ui: PropTypes.shape({
45 | closeSettings: PropTypes.func.isRequired,
46 | }),
47 | }).isRequired,
48 | };
49 |
--------------------------------------------------------------------------------
/src/helpers/i18n-helpers.js:
--------------------------------------------------------------------------------
1 | export function getLocale({
2 | locale, locales, defaultLocale, fallbackLocale,
3 | }) {
4 | let localeStr = locale;
5 | if (locales[locale] === undefined) {
6 | let localeFuzzy;
7 | Object.keys(locales).forEach((localStr) => {
8 | if (locales && Object.hasOwnProperty.call(locales, localStr)) {
9 | if (locale.substring(0, 2) === localStr.substring(0, 2)) {
10 | localeFuzzy = localStr;
11 | }
12 | }
13 | });
14 |
15 | if (localeFuzzy !== undefined) {
16 | localeStr = localeFuzzy;
17 | }
18 | }
19 |
20 | if (locales[localeStr] === undefined) {
21 | localeStr = defaultLocale;
22 | }
23 |
24 | if (!localeStr) {
25 | localeStr = fallbackLocale;
26 | }
27 |
28 | return localeStr;
29 | }
30 |
31 | export function getSelectOptions({ locales, resetToDefaultText = '', automaticDetectionText = '' }) {
32 | const options = [];
33 |
34 | if (resetToDefaultText) {
35 | options.push(
36 | {
37 | value: '',
38 | label: resetToDefaultText,
39 | },
40 | );
41 | }
42 |
43 | if (automaticDetectionText) {
44 | options.push(
45 | {
46 | value: 'automatic',
47 | label: automaticDetectionText,
48 | },
49 | );
50 | }
51 |
52 | options.push({
53 | value: '───',
54 | label: '───',
55 | disabled: true,
56 | });
57 |
58 | Object.keys(locales).sort(Intl.Collator().compare).forEach((key) => {
59 | options.push({
60 | value: key,
61 | label: locales[key],
62 | });
63 | });
64 |
65 | return options;
66 | }
67 |
--------------------------------------------------------------------------------
/src/styles/type.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 | @import './mixins.scss';
3 |
4 | .theme__dark {
5 | a { color: $dark-theme-gray-smoke; }
6 | .label { color: $dark-theme-gray-lightest; }
7 | .footnote { color: $dark-theme-gray-lightest; }
8 | }
9 |
10 | h1 {
11 | font-size: 30px;
12 | font-weight: 300;
13 | letter-spacing: -1px;
14 | margin-bottom: 25px;
15 | }
16 |
17 | h2 {
18 | font-size: 20px;
19 | font-weight: 500;
20 | letter-spacing: -1px;
21 | margin-bottom: 25px;
22 | margin-top: 55px;
23 |
24 | &:first-of-type { margin-top: 0; }
25 | }
26 |
27 | p {
28 | margin-bottom: 10px;
29 | line-height: 1.7rem;
30 |
31 | &:last-of-type { margin-bottom: 0; }
32 | }
33 |
34 | strong { font-weight: bold; }
35 |
36 | a {
37 | color: $theme-text-color;
38 | text-decoration: none;
39 |
40 | &.button {
41 | background: none;
42 | border: 2px solid $theme-brand-primary;
43 | border-radius: 3px;
44 | color: $theme-brand-primary;
45 | display: inline-block;
46 | padding: 10px 20px;
47 | position: relative;
48 | text-align: center;
49 | transition: background .5s, color .5s;
50 |
51 | &:hover {
52 | background: darken($theme-brand-primary, 5%);
53 | color: #FFF;
54 | }
55 | }
56 |
57 | &.link { color: $theme-brand-primary; }
58 | }
59 |
60 | .error-message, .error-message:last-of-type {
61 | color: $theme-brand-danger;
62 | margin: 10px 0;
63 | }
64 |
65 | .center { text-align: center; }
66 |
67 | .label { @include formLabel(); }
68 |
69 | .footnote {
70 | color: $theme-gray-light;
71 | font-size: 12px;
72 | }
73 |
--------------------------------------------------------------------------------
/uidev/src/stories/headline.stories.tsx:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 | import { observer } from 'mobx-react';
3 | import React from 'react';
4 | import uuid from 'uuid/v4';
5 |
6 | import { H1, H2, H3, H4 } from '@meetfranz/ui';
7 | import { storiesOf } from '../stores/stories';
8 |
9 | // interface IStoreArgs {
10 | // value?: boolean;
11 | // checked?: boolean;
12 | // label?: string;
13 | // id?: string;
14 | // name?: string;
15 | // disabled?: boolean;
16 | // error?: string;
17 | // }
18 |
19 | // const createStore = (args?: IStoreArgs) => {
20 | // return observable(Object.assign({
21 | // id: `element-${uuid()}`,
22 | // name: 'toggle',
23 | // label: 'Label',
24 | // value: true,
25 | // checked: false,
26 | // disabled: false,
27 | // error: '',
28 | // }, args));
29 | // };
30 |
31 | // const WithStoreToggle = observer(({ store }: { store: any }) => (
32 | // <>
33 | // store.checked = !store.checked}
42 | // />
43 | // >
44 | // ));
45 |
46 | storiesOf('Typo')
47 | .add('Headlines', () => (
48 | <>
49 | Welcome to the world of tomorrow
50 | Welcome to the world of tomorrow
51 | Welcome to the world of tomorrow
52 | Welcome to the world of tomorrow
53 | >
54 | ));
55 |
--------------------------------------------------------------------------------
/src/lib/analytics.js:
--------------------------------------------------------------------------------
1 | // import { app } from '@electron/remote';
2 | // import ElectronCookies from '@meetfranz/electron-cookies';
3 | // import querystring from 'querystring';
4 |
5 | // import { STATS_API } from '../config';
6 | // import { isDevMode, GA_ID } from '../environment';
7 |
8 | // ElectronCookies.enable({
9 | // origin: 'https://app.meetfranz.com',
10 | // });
11 |
12 | const debug = require('debug')('Franz:Analytics');
13 |
14 | /* eslint-disable */
15 | // var _paq = window._paq = window._paq || [];
16 |
17 | // _paq.push(["setCookieDomain", "app.meetfranz.com"]);
18 | // _paq.push(['setCustomDimension', 1, app.getVersion()]);
19 | // _paq.push(['setDomains', 'app.meetfranz.com']);
20 | // _paq.push(['setCustomUrl', '/']);
21 | // _paq.push(['trackPageView']);
22 |
23 |
24 | // (function() {
25 | // var u="https://analytics.franzinfra.com/";
26 | // _paq.push(['setTrackerUrl', u+'matomo.php']);
27 | // _paq.push(['setSiteId', '1']);
28 | // var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
29 | // g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
30 | // })();
31 | /* eslint-enable */
32 |
33 | export function gaPage(page) {
34 | debug('Track page', page);
35 | // window._paq.push(['setCustomUrl', page]);
36 | // window._paq.push(['trackPageView']);
37 |
38 | // debug('Track page', page);
39 | }
40 |
41 | export function gaEvent(category, action, label) {
42 | debug('Track Event', category, action, label);
43 | // window._paq.push(['trackEvent', category, action, label]);
44 | // debug('Track event', category, action, label);
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/util/ErrorBoundary/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import injectSheet from 'react-jss';
4 | import { defineMessages, intlShape } from 'react-intl';
5 |
6 | import Button from '../../ui/Button';
7 |
8 | import styles from './styles';
9 |
10 | const messages = defineMessages({
11 | headline: {
12 | id: 'app.errorHandler.headline',
13 | defaultMessage: '!!!Something went wrong.',
14 | },
15 | action: {
16 | id: 'app.errorHandler.action',
17 | defaultMessage: '!!!Reload',
18 | },
19 | });
20 |
21 | export default @injectSheet(styles) class ErrorBoundary extends Component {
22 | state = {
23 | hasError: false,
24 | }
25 |
26 | static propTypes = {
27 | classes: PropTypes.object.isRequired,
28 | children: PropTypes.node.isRequired,
29 | }
30 |
31 | static contextTypes = {
32 | intl: intlShape,
33 | };
34 |
35 | componentDidCatch() {
36 | this.setState({ hasError: true });
37 | }
38 |
39 | render() {
40 | const { classes } = this.props;
41 | const { intl } = this.context;
42 |
43 | if (this.state.hasError) {
44 | return (
45 |
46 |
47 | {intl.formatMessage(messages.headline)}
48 |
49 |
55 | );
56 | }
57 |
58 | return this.props.children;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/ui/FullscreenLoader/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import injectSheet, { withTheme } from 'react-jss';
5 | import classnames from 'classnames';
6 |
7 | import Loader from '../Loader';
8 |
9 | import styles from './styles';
10 |
11 | export default @observer @withTheme @injectSheet(styles) class FullscreenLoader extends Component {
12 | static propTypes = {
13 | className: PropTypes.string,
14 | title: PropTypes.string.isRequired,
15 | classes: PropTypes.object.isRequired,
16 | theme: PropTypes.object.isRequired,
17 | spinnerColor: PropTypes.string,
18 | children: PropTypes.node,
19 | };
20 |
21 | static defaultProps = {
22 | className: null,
23 | spinnerColor: null,
24 | children: null,
25 | };
26 |
27 | render() {
28 | const {
29 | classes,
30 | title,
31 | children,
32 | spinnerColor,
33 | className,
34 | theme,
35 | } = this.props;
36 |
37 | return (
38 |
39 |
45 |
{title}
46 |
47 | {children && (
48 |
49 | {children}
50 |
51 | )}
52 |
53 |
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/styles/content-tabs.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark {
4 | .content-tabs {
5 | .content-tabs__content {
6 | background: $dark-theme-gray-darker;
7 | }
8 |
9 | .content-tabs__tabs {
10 | .content-tabs__item {
11 | background: $dark-theme-gray;
12 | color: #FFF;
13 | border: 0;
14 | }
15 | }
16 | }
17 | }
18 |
19 | .content-tabs {
20 | .content-tabs__tabs {
21 | border-top-left-radius: $theme-border-radius-small;
22 | border-top-right-radius: $theme-border-radius-small;
23 | display: flex;
24 | overflow: hidden;
25 |
26 | .content-tabs__item {
27 | background: linear-gradient($theme-gray-lightest 80%, darken($theme-gray-lightest, 3%));
28 | border-right: 1px solid $theme-gray-lighter;
29 | color: $theme-gray-dark;
30 | flex: 1;
31 | padding: 10px;
32 | transition: background $theme-transition-time;
33 |
34 | &:last-of-type { border-right: 0; }
35 |
36 | &.is-active {
37 | background: $theme-brand-primary;
38 | box-shadow: none;
39 | color: #FFF;
40 | }
41 | }
42 | }
43 |
44 | .content-tabs__content {
45 | background: $theme-gray-lightest;
46 | border-bottom-left-radius: $theme-border-radius-small;
47 | border-bottom-right-radius: $theme-border-radius-small;
48 | padding: 20px 20px;
49 |
50 | .content-tabs__item {
51 | display: none;
52 | top: 0;
53 |
54 | &.is-active { display: block; }
55 | }
56 |
57 | .franz-form__input-wrapper { background: #FFF; }
58 | .franz-form__field:last-of-type { margin-bottom: 0; }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/api/server/LocalApi.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import { session } from '@electron/remote';
3 | import du from 'du';
4 |
5 | import { getServicePartitionsDirectory } from '../../helpers/service-helpers.js';
6 |
7 | const debug = require('debug')('Franz:LocalApi');
8 |
9 | export default class LocalApi {
10 | // Settings
11 | getAppSettings(type) {
12 | return new Promise((resolve) => {
13 | ipcRenderer.once('appSettings', (event, resp) => {
14 | debug('LocalApi::getAppSettings resolves', resp.type, resp.data);
15 | resolve(resp);
16 | });
17 |
18 | ipcRenderer.send('getAppSettings', type);
19 | });
20 | }
21 |
22 | async updateAppSettings(type, data) {
23 | debug('LocalApi::updateAppSettings resolves', type, data);
24 | ipcRenderer.send('updateAppSettings', {
25 | type,
26 | data,
27 | });
28 | }
29 |
30 | // Services
31 | async getAppCacheSize() {
32 | const partitionsDir = getServicePartitionsDirectory();
33 | return new Promise((resolve, reject) => {
34 | du(partitionsDir, (err, size) => {
35 | if (err) reject(err);
36 |
37 | debug('LocalApi::getAppCacheSize resolves', size);
38 | resolve(size);
39 | });
40 | });
41 | }
42 |
43 | async clearCache(serviceId) {
44 | const s = session.fromPartition(`persist:service-${serviceId}`);
45 |
46 | debug('LocalApi::clearCache resolves', serviceId);
47 | return s.clearCache();
48 | }
49 |
50 | async clearAppCache() {
51 | const s = session.defaultSession;
52 |
53 | debug('LocalApi::clearCache clearAppCache');
54 | return s.clearCache();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/stores/RecipePreviewsStore.js:
--------------------------------------------------------------------------------
1 | import { action, computed, observable } from 'mobx';
2 | import { debounce } from 'lodash';
3 | import ms from 'ms';
4 |
5 | import Store from './lib/Store';
6 | import CachedRequest from './lib/CachedRequest';
7 | import Request from './lib/Request';
8 | import { gaEvent } from '../lib/analytics';
9 |
10 | export default class RecipePreviewsStore extends Store {
11 | @observable allRecipePreviewsRequest = new CachedRequest(this.api.recipePreviews, 'all');
12 |
13 | @observable featuredRecipePreviewsRequest = new CachedRequest(this.api.recipePreviews, 'featured');
14 |
15 | @observable searchRecipePreviewsRequest = new Request(this.api.recipePreviews, 'search');
16 |
17 | constructor(...args) {
18 | super(...args);
19 |
20 | // Register action handlers
21 | this.actions.recipePreview.search.listen(this._search.bind(this));
22 | }
23 |
24 | @computed get all() {
25 | return this.allRecipePreviewsRequest.execute().result || [];
26 | }
27 |
28 | @computed get featured() {
29 | return this.featuredRecipePreviewsRequest.execute().result || [];
30 | }
31 |
32 | @computed get searchResults() {
33 | return this.searchRecipePreviewsRequest.result || [];
34 | }
35 |
36 | @computed get dev() {
37 | return this.stores.recipes.all.filter(r => r.local);
38 | }
39 |
40 | // Actions
41 | @action _search({ needle }) {
42 | if (needle !== '') {
43 | this.searchRecipePreviewsRequest.execute(needle);
44 |
45 | this._analyticsSearch(needle);
46 | }
47 | }
48 |
49 | // Helper
50 | _analyticsSearch = debounce((needle) => {
51 | gaEvent('Recipe', 'search', needle);
52 | }, ms('3s'));
53 | }
54 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ### Expected Behavior
8 |
9 |
10 |
11 | ### Current Behavior
12 |
13 |
14 |
15 | ### Screenshots (if appropriate):
16 |
17 | ### Possible Solution
18 |
19 |
20 |
21 | ### Steps to Reproduce (for bugs)
22 |
23 |
24 | 1.
25 | 2.
26 | 3.
27 | 4.
28 |
29 | ### Context
30 |
31 |
32 |
33 | ### Your Environment
34 |
35 | * Franz Version used:
36 | * Operating System and version:
37 |
--------------------------------------------------------------------------------
/src/components/ui/ServiceIcon.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import injectSheet from 'react-jss';
5 | import classnames from 'classnames';
6 |
7 | import ServiceModel from '../../models/Service';
8 |
9 | const styles = theme => ({
10 | root: {
11 | height: 'auto',
12 | },
13 | icon: {
14 | width: theme.serviceIcon.width,
15 | },
16 | isCustomIcon: {
17 | width: theme.serviceIcon.isCustom.width,
18 | border: theme.serviceIcon.isCustom.border,
19 | borderRadius: theme.serviceIcon.isCustom.borderRadius,
20 | },
21 | isDisabled: {
22 | filter: 'grayscale(100%)',
23 | opacity: '.5',
24 | },
25 | });
26 |
27 | @injectSheet(styles) @observer
28 | class ServiceIcon extends Component {
29 | static propTypes = {
30 | classes: PropTypes.object.isRequired,
31 | service: PropTypes.instanceOf(ServiceModel).isRequired,
32 | className: PropTypes.string,
33 | };
34 |
35 | static defaultProps = {
36 | className: '',
37 | };
38 |
39 | render() {
40 | const {
41 | classes,
42 | className,
43 | service,
44 | } = this.props;
45 |
46 | return (
47 |
53 |

62 |
63 | );
64 | }
65 | }
66 |
67 | export default ServiceIcon;
68 |
--------------------------------------------------------------------------------
/src/containers/auth/SignupScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 |
5 | import Signup from '../../components/auth/Signup';
6 | import UserStore from '../../stores/UserStore';
7 | import FeaturesStore from '../../stores/FeaturesStore';
8 |
9 | import { globalError as globalErrorPropType } from '../../prop-types';
10 |
11 | export default @inject('stores', 'actions') @observer class SignupScreen extends Component {
12 | static propTypes = {
13 | error: globalErrorPropType.isRequired,
14 | };
15 |
16 | onSignup(values) {
17 | const { actions, stores } = this.props;
18 |
19 | const { canSkipTrial, defaultTrialPlan, pricingConfig } = stores.features.anonymousFeatures;
20 |
21 | if (!canSkipTrial) {
22 | Object.assign(values, {
23 | plan: defaultTrialPlan,
24 | currency: pricingConfig.currencyID,
25 | });
26 | }
27 |
28 | actions.user.signup(values);
29 | }
30 |
31 | render() {
32 | const { stores, error } = this.props;
33 |
34 | return (
35 | this.onSignup(values)}
37 | isSubmitting={stores.user.signupRequest.isExecuting}
38 | loginRoute={stores.user.loginRoute}
39 | error={error}
40 | />
41 | );
42 | }
43 | }
44 |
45 | SignupScreen.wrappedComponent.propTypes = {
46 | actions: PropTypes.shape({
47 | user: PropTypes.shape({
48 | signup: PropTypes.func.isRequired,
49 | }).isRequired,
50 | }).isRequired,
51 | stores: PropTypes.shape({
52 | user: PropTypes.instanceOf(UserStore).isRequired,
53 | features: PropTypes.instanceOf(FeaturesStore).isRequired,
54 | }).isRequired,
55 | };
56 |
--------------------------------------------------------------------------------
/src/styles/animations.scss:
--------------------------------------------------------------------------------
1 | // FadeIn
2 | .fadeIn-appear { opacity: .01; }
3 |
4 | .fadeIn-appear.fadeIn-appear-active {
5 | opacity: 1;
6 | transition: opacity .5s ease-out;
7 | }
8 |
9 | .fadeIn-enter {
10 | opacity: .01;
11 | transition: opacity .5s ease-out;
12 | }
13 |
14 | .fadeIn-leave { opacity: 1; }
15 |
16 | .fadeIn-leave.fadeIn-leave-active {
17 | opacity: .01;
18 | transition: opacity 300ms ease-in;
19 | }
20 |
21 | // FadeIn Fast
22 | .fadeIn-fast-appear { opacity: .01; }
23 |
24 | .fadeIn-fast-appear.fadeIn-fast-appear-active {
25 | opacity: 1;
26 | transition: opacity .25s ease-out;
27 | }
28 |
29 | .fadeIn-fast-enter {
30 | opacity: .01;
31 | transition: opacity .25s ease-out;
32 | }
33 |
34 | .fadeIn-fast-leave { opacity: 1; }
35 |
36 | .fadeIn-fast-leave.fadeIn-fast-leave-active {
37 | opacity: .01;
38 | transition: opacity .25s ease-in;
39 | }
40 |
41 | // Slide down
42 | .slideDown-appear {
43 | max-height: 0;
44 | overflow-y: hidden;
45 | }
46 |
47 | .slideDown-appear.slideDown-appear-active {
48 | max-height: 500px;
49 | transition: max-height .5s ease-out;
50 | }
51 |
52 | .slideDown-enter {
53 | max-height: 0;
54 | transition: max-height .5s ease-out;
55 | }
56 |
57 | // Slide up
58 | .slideUp-appear {
59 | opacity: 0;
60 | transform: translateY(20px);
61 | }
62 |
63 | .slideUp-appear.slideUp-appear-active {
64 | opacity: 1;
65 | transform: translateY(0px);
66 | transition: all .3s ease-out;
67 | }
68 |
69 | .slideUp-enter {
70 | opacity: 0;
71 | transform: translateY(20px);
72 | transition: all .3s ease-out;
73 | }
74 |
75 | .slideUp-leave { opacity: 1; }
76 |
77 | .slideUp-leave.slideUp-leave-active {
78 | opacity: .01;
79 | transition: opacity 300ms ease-in;
80 | }
81 |
--------------------------------------------------------------------------------
/src/features/todos/index.js:
--------------------------------------------------------------------------------
1 | import { reaction } from 'mobx';
2 | import { TODOS_RECIPE_ID as TODOS_RECIPE } from '../../config';
3 | import TodoStore from './store';
4 |
5 | const debug = require('debug')('Franz:feature:todos');
6 |
7 | export const GA_CATEGORY_TODOS = 'Todos';
8 |
9 | export const DEFAULT_TODOS_WIDTH = 300;
10 | export const TODOS_MIN_WIDTH = 200;
11 | export const DEFAULT_TODOS_VISIBLE = true;
12 | export const DEFAULT_IS_FEATURE_ENABLED_BY_USER = true;
13 | export const TODOS_RECIPE_ID = TODOS_RECIPE;
14 | export const TODOS_PARTITION_ID = 'persist:todos';
15 |
16 | export const TODOS_ROUTES = {
17 | TARGET: '/todos',
18 | };
19 |
20 | export const todosStore = new TodoStore();
21 |
22 | export default function initTodos(stores, actions) {
23 | stores.todos = todosStore;
24 | const { features } = stores;
25 |
26 | reaction(
27 | () => stores.recipes.hasFinishedLoading,
28 | (hasFinishedLoading) => {
29 | if (hasFinishedLoading) {
30 | if (!stores.recipes.isInstalled(TODOS_RECIPE_ID)) {
31 | console.log('Todos recipe is not installed, installing now...');
32 | actions.recipe.install({ recipeId: TODOS_RECIPE_ID });
33 | }
34 | }
35 | },
36 | {
37 | fireImmediately: true,
38 | },
39 | );
40 |
41 | // Toggle todos feature
42 | reaction(
43 | () => features.features.isTodosEnabled,
44 | (isEnabled) => {
45 | if (isEnabled) {
46 | debug('Initializing `todos` feature');
47 | todosStore.start(stores, actions);
48 | } else if (todosStore.isFeatureActive) {
49 | debug('Disabling `todos` feature');
50 | todosStore.stop();
51 | }
52 | },
53 | {
54 | fireImmediately: true,
55 | },
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/styles/recipes.scss:
--------------------------------------------------------------------------------
1 | @import './config.scss';
2 |
3 | .theme__dark .recipe-teaser {
4 | background-color: $dark-theme-gray-dark;
5 | color: $dark-theme-text-color;
6 |
7 | &:hover { background-color: $dark-theme-gray; }
8 | }
9 |
10 | .recipes {
11 | .recipes__list {
12 | align-content: flex-start;
13 | display: flex;
14 | flex-flow: row wrap;
15 | height: auto;
16 | // min-height: 70%;
17 |
18 | &.recipes__list--disabled {
19 | filter: grayscale(100%);
20 | opacity: .3;
21 | pointer-events: none;
22 | }
23 | }
24 |
25 | .recipes__navigation {
26 | height: auto;
27 | margin-bottom: 35px;
28 |
29 | .badge { margin-right: 10px; }
30 |
31 | &.recipes__navigation--disabled {
32 | filter: grayscale(100%);
33 | opacity: .3;
34 | pointer-events: none;
35 | }
36 | }
37 |
38 | &__service-request { float: right; }
39 | }
40 |
41 | .recipe-teaser {
42 | background-color: $theme-gray-lightest;
43 | border-radius: $theme-border-radius;
44 | height: 120px;
45 | margin: 0 20px 20px 0;
46 | overflow: hidden;
47 | position: relative;
48 | transition: background $theme-transition-time;
49 | width: calc(25% - 20px);
50 |
51 | &:hover { background-color: $theme-gray-lighter; }
52 |
53 | .recipe-teaser__icon {
54 | margin-bottom: 10px;
55 | width: 50px;
56 | }
57 |
58 | .recipe-teaser__label { display: block; }
59 |
60 | h2 { z-index: 10; }
61 |
62 | &__dev-badge {
63 | background: $theme-brand-warning;
64 | box-shadow: 0 0 4px rgba(black, .2);
65 | color: #FFF;
66 | font-size: 10px;
67 | position: absolute;
68 | right: -13px;
69 | top: 5px;
70 | transform: rotateZ(45deg);
71 | width: 50px;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/stores/NewsStore.js:
--------------------------------------------------------------------------------
1 | import { computed, observable } from 'mobx';
2 | import { remove } from 'lodash';
3 |
4 | import Store from './lib/Store';
5 | import CachedRequest from './lib/CachedRequest';
6 | import Request from './lib/Request';
7 | import { CHECK_INTERVAL } from '../config';
8 |
9 | export default class NewsStore extends Store {
10 | @observable latestNewsRequest = new CachedRequest(this.api.news, 'latest');
11 |
12 | @observable hideNewsRequest = new Request(this.api.news, 'hide');
13 |
14 | constructor(...args) {
15 | super(...args);
16 |
17 | // Register action handlers
18 | this.actions.news.hide.listen(this._hide.bind(this));
19 | this.actions.user.logout.listen(this._resetNewsRequest.bind(this));
20 | }
21 |
22 | setup() {
23 | // Check for news updates every couple of hours
24 | setInterval(() => {
25 | if (this.latestNewsRequest.wasExecuted && this.stores.user.isLoggedIn) {
26 | this.latestNewsRequest.invalidate({ immediately: true });
27 | }
28 | }, CHECK_INTERVAL);
29 | }
30 |
31 | @computed get latest() {
32 | return this.latestNewsRequest.execute().result || [];
33 | }
34 |
35 | // Actions
36 | _hide({ newsId }) {
37 | this.hideNewsRequest.execute(newsId);
38 |
39 | this.latestNewsRequest.invalidate().patch((result) => {
40 | // TODO: check if we can use mobx.array remove
41 | remove(result, n => n.id === newsId);
42 | });
43 | }
44 |
45 | /**
46 | * Reset the news request when current user logs out so that when another user
47 | * logs in again without an app restart, the request will be fetched again and
48 | * the news will be shown to the user.
49 | *
50 | * @private
51 | */
52 | _resetNewsRequest() {
53 | this.latestNewsRequest.reset();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/ui/src/badge/ProBadge.tsx:
--------------------------------------------------------------------------------
1 | import { mdiStar } from '@mdi/js';
2 | import { Theme } from '@meetfranz/theme';
3 | import classnames from 'classnames';
4 | import React, { Component } from 'react';
5 | import injectStyle from 'react-jss';
6 |
7 | import { Badge, Icon } from '../';
8 | import { IWithStyle } from '../typings/generic';
9 |
10 | interface IProps extends IWithStyle {
11 | badgeClasses?: string;
12 | iconClasses?: string;
13 | inverted?: boolean;
14 | className?: string;
15 | }
16 |
17 | const styles = (theme: Theme) => ({
18 | badge: {
19 | height: 'auto',
20 | padding: [4, 6, 2, 7],
21 | borderRadius: theme.borderRadiusSmall,
22 | },
23 | invertedBadge: {
24 | background: theme.styleTypes.primary.contrast,
25 | color: theme.styleTypes.primary.accent,
26 | },
27 | icon: {
28 | fill: theme.styleTypes.primary.contrast,
29 | },
30 | invertedIcon: {
31 | fill: theme.styleTypes.primary.accent,
32 | },
33 | });
34 |
35 | class ProBadgeComponent extends Component {
36 | render() {
37 | const {
38 | classes,
39 | badgeClasses,
40 | iconClasses,
41 | inverted,
42 | className,
43 | } = this.props;
44 |
45 | return (
46 |
55 |
63 |
64 | );
65 | }
66 | }
67 |
68 | export const ProBadge = injectStyle(styles)(ProBadgeComponent);
69 |
--------------------------------------------------------------------------------
/packages/ui/src/headline/index.tsx:
--------------------------------------------------------------------------------
1 | import { Theme } from '@meetfranz/theme';
2 | import classnames from 'classnames';
3 | import React, { Component } from 'react';
4 | import injectStyle from 'react-jss';
5 |
6 | import { IWithStyle, Omit } from '../typings/generic';
7 |
8 | interface IProps extends IWithStyle {
9 | level?: number;
10 | className?: string;
11 | children: string | React.ReactNode;
12 | id?: string;
13 | }
14 |
15 | const styles = (theme: Theme) => ({
16 | headline: {
17 | fontWeight: 'lighter',
18 | color: theme.colorText,
19 | marginTop: 0,
20 | marginBottom: 10,
21 | textAlign: 'left',
22 | },
23 | h1: {
24 | fontSize: 30,
25 | marginTop: 0,
26 | },
27 | h2: {
28 | fontSize: 20,
29 | },
30 | h3: {
31 | fontSize: 18,
32 | },
33 | h4: {
34 | fontSize: theme.uiFontSize,
35 | },
36 | });
37 |
38 | class HeadlineComponent extends Component {
39 | render() {
40 | const {
41 | classes,
42 | level,
43 | className,
44 | children,
45 | id,
46 | } = this.props;
47 |
48 | return React.createElement(
49 | `h${level}`,
50 | {
51 | id,
52 | className: classnames({
53 | [classes.headline]: true,
54 | [classes[level ? `h${level}` : 'h1']]: true,
55 | [`${className}`]: className,
56 | }),
57 | 'data-type': 'franz-headline',
58 | },
59 | children,
60 | );
61 | }
62 | }
63 |
64 | const Headline = injectStyle(styles)(HeadlineComponent);
65 |
66 | const createH = (level: number) => (props: Omit) => {props.children};
67 |
68 | export const H1 = createH(1);
69 | export const H2 = createH(2);
70 | export const H3 = createH(3);
71 | export const H4 = createH(4);
72 |
--------------------------------------------------------------------------------
/src/models/User.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 |
3 | export default class User {
4 | id = null;
5 |
6 | @observable email = null;
7 |
8 | @observable firstname = null;
9 |
10 | @observable lastname = null;
11 |
12 | @observable organization = null;
13 |
14 | @observable accountType = null;
15 |
16 | @observable emailIsConfirmed = true;
17 |
18 | // better assume it's confirmed to avoid noise
19 | @observable subscription = {};
20 |
21 | @observable isSubscriptionOwner = false;
22 |
23 | @observable hasSubscription = false;
24 |
25 | @observable hadSubscription = false;
26 |
27 | @observable isPremium = false;
28 |
29 | @observable beta = false;
30 |
31 | @observable donor = {};
32 |
33 | @observable isDonor = false;
34 |
35 | @observable locale = false;
36 |
37 | @observable team = {};
38 |
39 |
40 | constructor(data) {
41 | if (!data.id) {
42 | throw Error('User requires Id');
43 | }
44 |
45 | this.id = data.id;
46 | this.email = data.email || this.email;
47 | this.firstname = data.firstname || this.firstname;
48 | this.lastname = data.lastname || this.lastname;
49 | this.organization = data.organization || this.organization;
50 | this.accountType = data.accountType || this.accountType;
51 | this.isPremium = data.isPremium || this.isPremium;
52 | this.beta = data.beta || this.beta;
53 | this.donor = data.donor || this.donor;
54 | this.isDonor = data.isDonor || this.isDonor;
55 | this.locale = data.locale || this.locale;
56 |
57 | this.isSubscriptionOwner = data.isSubscriptionOwner || this.isSubscriptionOwner;
58 | this.hasSubscription = data.hasSubscription || this.hasSubscription;
59 | this.hadSubscription = data.hadSubscription || this.hadSubscription;
60 |
61 | this.team = data.team || this.team;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/styles/welcome.scss:
--------------------------------------------------------------------------------
1 | .auth .welcome {
2 | height: auto;
3 |
4 | &__content {
5 | align-items: center;
6 | color: #FFF;
7 | display: flex;
8 | justify-content: center;
9 | height: auto;
10 | }
11 |
12 | &__logo { width: 100px; }
13 |
14 | &__text {
15 | border-left: 1px solid #FFF;
16 | margin-left: 40px;
17 | padding-left: 40px;
18 |
19 | h1 {
20 | font-size: 60px;
21 | letter-spacing: -.4rem;
22 | margin-bottom: 5px;
23 | }
24 |
25 | h2 {
26 | margin-bottom: 0;
27 | margin-left: 2px;
28 | }
29 | }
30 |
31 | &__services {
32 | height: 100%;
33 | margin-left: -450px;
34 | max-height: 600px;
35 | max-width: 800px;
36 | width: 100%;
37 | }
38 |
39 | &__buttons {
40 | display: block;
41 | margin-top: 100px;
42 | text-align: center;
43 | height: auto;
44 |
45 | .button:first-of-type { margin-right: 25px; }
46 | }
47 |
48 | .button {
49 | border-color: #FFF;
50 | color: #FFF;
51 |
52 | &:hover {
53 | background: #FFF;
54 | color: $theme-brand-primary;
55 | }
56 |
57 | &__inverted {
58 | background: #FFF;
59 | color: $theme-brand-primary;
60 | }
61 |
62 | &__inverted:hover {
63 | background: none;
64 | color: #FFF;
65 | }
66 | }
67 |
68 | &__featured-services {
69 | align-items: center;
70 | background: #FFF;
71 | border-radius: 6px;
72 | display: flex;
73 | flex-wrap: wrap;
74 | margin: 80px auto 0 auto;
75 | padding: 20px 20px 5px;
76 | text-align: center;
77 | width: 480px;
78 | height: auto;
79 | }
80 |
81 | &__featured-service {
82 | margin: 0 10px 15px;
83 | height: 35px;
84 | transition: .5s filter, .5s opacity;
85 | width: 35px;
86 |
87 | img { width: 35px; }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/settings/SettingsLayout.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 |
5 | import ErrorBoundary from '../util/ErrorBoundary';
6 | import { oneOrManyChildElements } from '../../prop-types';
7 | import Appear from '../ui/effects/Appear';
8 |
9 | export default @observer class SettingsLayout extends Component {
10 | static propTypes = {
11 | navigation: PropTypes.element.isRequired,
12 | children: oneOrManyChildElements.isRequired,
13 | closeSettings: PropTypes.func.isRequired,
14 | };
15 |
16 | componentWillMount() {
17 | document.addEventListener('keydown', this.handleKeyDown.bind(this), false);
18 | }
19 |
20 | componentWillUnmount() {
21 | document.removeEventListener('keydown', this.handleKeyDown.bind(this), false);
22 | }
23 |
24 | handleKeyDown(e) {
25 | if (e.keyCode === 27) { // escape key
26 | this.props.closeSettings();
27 | }
28 | }
29 |
30 | render() {
31 | const {
32 | navigation,
33 | children,
34 | closeSettings,
35 | } = this.props;
36 |
37 | return (
38 |
39 |
40 |
41 |
46 |
47 | {navigation}
48 | {children}
49 |
54 |
55 |
56 |
57 |
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/lib/TouchBar.js:
--------------------------------------------------------------------------------
1 | import os from 'os';
2 | import semver from 'semver';
3 | import { TouchBar, getCurrentWindow } from '@electron/remote';
4 | import { autorun } from 'mobx';
5 |
6 | import { isMac } from '../environment';
7 |
8 | export default class FranzTouchBar {
9 | constructor(stores, actions) {
10 | this.stores = stores;
11 | this.actions = actions;
12 |
13 | // Temporary fix for https://github.com/electron/electron/issues/10442
14 | // TODO: remove when we upgrade to electron 1.8.2 or later
15 | try {
16 | if (isMac && semver.gt(os.release(), '16.6.0')) {
17 | this.build = autorun(this._build.bind(this));
18 | }
19 | } catch (err) {
20 | console.error(err);
21 | }
22 | }
23 |
24 | _build() {
25 | const currentWindow = getCurrentWindow();
26 |
27 | if (this.stores.router.location.pathname.startsWith('/payment/')) {
28 | return;
29 | }
30 |
31 | if (this.stores.user.isLoggedIn) {
32 | const { TouchBarButton, TouchBarSpacer } = TouchBar;
33 |
34 | const buttons = [];
35 | this.stores.services.allDisplayed.forEach(((service) => {
36 | buttons.push(new TouchBarButton({
37 | label: `${service.name}${service.unreadDirectMessageCount > 0
38 | ? ' 🔴' : ''} ${service.unreadDirectMessageCount === 0
39 | && service.unreadIndirectMessageCount > 0
40 | ? ' ⚪️' : ''}`,
41 | backgroundColor: service.isActive ? '#3498DB' : null,
42 | click: () => {
43 | this.actions.service.setActive({ serviceId: service.id });
44 | },
45 | }), new TouchBarSpacer({ size: 'small' }));
46 | }));
47 |
48 | const touchBar = new TouchBar({ items: buttons });
49 | currentWindow.setTouchBar(touchBar);
50 | } else {
51 | currentWindow.setTouchBar(null);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/ui/src/badge/index.tsx:
--------------------------------------------------------------------------------
1 | import { Theme } from '@meetfranz/theme';
2 | import classnames from 'classnames';
3 | import React, { Component } from 'react';
4 | import injectStyle from 'react-jss';
5 |
6 | import { IWithStyle } from '../typings/generic';
7 |
8 | interface IProps extends IWithStyle {
9 | type: string;
10 | className?: string;
11 | children: React.ReactNode;
12 | }
13 |
14 | const badgeStyles = (theme: Theme) => {
15 | const styles = {};
16 | Object.keys(theme.styleTypes).map((style) => {
17 | Object.assign(styles, {
18 | [style]: {
19 | background: theme.styleTypes[style].accent,
20 | color: theme.styleTypes[style].contrast,
21 | border: theme.styleTypes[style].border,
22 | },
23 | });
24 | });
25 |
26 | return styles;
27 | };
28 |
29 | const styles = (theme: Theme) => ({
30 | badge: {
31 | display: 'inline-block',
32 | padding: [3, 8, 4],
33 | fontSize: theme.badgeFontSize,
34 | borderRadius: theme.badgeBorderRadius,
35 | margin: [0, 4],
36 |
37 | '&:first-child': {
38 | marginLeft: 0,
39 | },
40 |
41 | '&:last-child': {
42 | marginRight: 0,
43 | },
44 | },
45 | ...badgeStyles(theme),
46 | });
47 |
48 | class BadgeComponent extends Component {
49 | public static defaultProps = {
50 | type: 'primary',
51 | };
52 |
53 | render() {
54 | const {
55 | classes,
56 | children,
57 | type,
58 | className,
59 | } = this.props;
60 |
61 | return (
62 |
70 | {children}
71 |
72 | );
73 | }
74 | }
75 |
76 | export const Badge = injectStyle(styles)(BadgeComponent);
77 |
--------------------------------------------------------------------------------