├── resources ├── index.md ├── Screenshot from 2019-02-02 02-10-33.png ├── Screenshot from 2019-02-02 02-10-52.png ├── Screenshot from 2019-02-02 02-11-05.png ├── Screenshot from 2019-02-02 02-11-27.png └── usecases.md ├── .env.development ├── .eslintignore ├── src ├── react-app-env.d.ts ├── components │ ├── App │ │ ├── __usage__ │ │ │ ├── usage.command │ │ │ ├── index.html │ │ │ ├── render.js │ │ │ └── scenario.js │ │ ├── __test__ │ │ │ └── App.test.js │ │ ├── index.jsx │ │ └── App.css │ ├── Export │ │ ├── __usage__ │ │ │ ├── usage.command │ │ │ ├── render.js │ │ │ ├── index.html │ │ │ └── scenario.js │ │ ├── File.js │ │ └── index.jsx │ ├── AppMenu │ │ ├── __usage__ │ │ │ ├── usage.command │ │ │ ├── render.js │ │ │ ├── scenario.js │ │ │ └── index.html │ │ └── index.jsx │ ├── Settings │ │ ├── __usage__ │ │ │ ├── usage.command │ │ │ ├── render.js │ │ │ ├── index.html │ │ │ └── scenario.js │ │ ├── Contacts.js │ │ ├── index.jsx │ │ ├── Notifications.js │ │ └── AddDeleteSetting.js │ ├── EventDetails │ │ └── __usage__ │ │ │ ├── usage.command │ │ │ ├── render.js │ │ │ ├── index.html │ │ │ └── scenario.js │ ├── EventGuestList │ │ ├── __usage__ │ │ │ ├── usage.command │ │ │ ├── index.html │ │ │ ├── render.js │ │ │ └── scenario.js │ │ └── index.jsx │ ├── Terms │ │ ├── index.tsx │ │ └── Licenses.jsx │ ├── Help │ │ ├── index.tsx │ │ ├── QuestionsDevs.jsx │ │ └── QuestionsWeb.jsx │ ├── Header.jsx │ ├── UserProfile │ │ ├── index.jsx │ │ └── SignInButton.jsx │ ├── Calendar │ │ ├── RemindersModal.js │ │ ├── SendInvitesModal.js │ │ └── index.jsx │ ├── Footer.jsx │ ├── FAQ.jsx │ └── PublicCalendar │ │ └── index.jsx ├── containers │ ├── GuestList.js │ ├── Help.js │ ├── index.js │ ├── Files.js │ ├── AppMenu.js │ ├── RemindersModal.js │ ├── UserProfile.js │ ├── SendInvitesModal.js │ ├── PublicCalendar.js │ ├── EventDetails.js │ ├── Calendar.js │ └── Settings.js ├── index.css ├── css │ ├── EventDetails.css │ └── datetime.css ├── store │ ├── reducers.js │ ├── lazy │ │ └── reducer.js │ ├── gaia │ │ ├── reducer.js │ │ └── actions.js │ ├── event │ │ ├── eventAction.js │ │ ├── calendarActionLazy.js │ │ ├── contactActionLazy.js │ │ └── reducer.js │ ├── index.js │ ├── auth │ │ ├── reducer.js │ │ └── actions.js │ └── ActionTypes.js ├── utils.js ├── fontawesome.js ├── routes.js ├── index.js ├── core │ ├── eventDefaults.js │ ├── eventFN.js │ ├── ical.js │ └── chat.js ├── logo.svg ├── reminder │ ├── index.js │ └── reminder.js └── registerServiceWorker.js ├── public ├── 123.gif ├── favicon.ico ├── signin.png ├── blockstack.png ├── favicon-16x16.png ├── favicon-32x32.png ├── images │ ├── avatar.png │ ├── oichat.png │ ├── differences.gif │ └── gcalendar.png ├── mstile-150x150.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── browserconfig.xml ├── _headers ├── manifest.json ├── .well-known │ └── assetlinks.json ├── safari-pinned-tab.svg ├── index.html └── gtutorial.html ├── promo ├── OI+Calendar.gif └── OI+Calendar.mp4 ├── .prettierrc ├── .babelrc ├── spec ├── event_invites.spec.js ├── ICS_EXAMPLE_FILE.ics ├── template.spec.js ├── event_crud.spec.js └── event_ical.spec.js ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── .eslintrc ├── README.md └── package.json /resources/index.md: -------------------------------------------------------------------------------- 1 | #Screenshots 2 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_VERSION=0.0.0-dev 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | build/** 3 | **/dist/** 4 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/123.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/123.gif -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/signin.png -------------------------------------------------------------------------------- /promo/OI+Calendar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/promo/OI+Calendar.gif -------------------------------------------------------------------------------- /promo/OI+Calendar.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/promo/OI+Calendar.mp4 -------------------------------------------------------------------------------- /public/blockstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/blockstack.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/images/avatar.png -------------------------------------------------------------------------------- /public/images/oichat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/images/oichat.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/differences.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/images/differences.gif -------------------------------------------------------------------------------- /public/images/gcalendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/images/gcalendar.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/components/App/__usage__/usage.command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")" 3 | parcel --no-cache --no-hmr index.html 4 | -------------------------------------------------------------------------------- /src/components/Export/__usage__/usage.command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")" 3 | parcel --no-cache --no-hmr index.html 4 | -------------------------------------------------------------------------------- /src/components/AppMenu/__usage__/usage.command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")" 3 | parcel --no-cache --no-hmr index.html 4 | -------------------------------------------------------------------------------- /src/components/Settings/__usage__/usage.command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")" 3 | parcel --no-cache --no-hmr index.html 4 | -------------------------------------------------------------------------------- /src/components/EventDetails/__usage__/usage.command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")" 3 | parcel --no-cache --no-hmr index.html 4 | -------------------------------------------------------------------------------- /src/components/EventGuestList/__usage__/usage.command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")" 3 | parcel --no-cache --no-hmr index.html 4 | -------------------------------------------------------------------------------- /src/containers/GuestList.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | export default connect((state, redux) => { 4 | return {} 5 | }) 6 | -------------------------------------------------------------------------------- /resources/Screenshot from 2019-02-02 02-10-33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/resources/Screenshot from 2019-02-02 02-10-33.png -------------------------------------------------------------------------------- /resources/Screenshot from 2019-02-02 02-10-52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/resources/Screenshot from 2019-02-02 02-10-52.png -------------------------------------------------------------------------------- /resources/Screenshot from 2019-02-02 02-11-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/resources/Screenshot from 2019-02-02 02-11-05.png -------------------------------------------------------------------------------- /resources/Screenshot from 2019-02-02 02-11-27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openintents/calendar-web/HEAD/resources/Screenshot from 2019-02-02 02-11-27.png -------------------------------------------------------------------------------- /src/components/App/__usage__/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true, 6 | "jsxBracketSameLine": false 7 | } 8 | -------------------------------------------------------------------------------- /src/components/EventGuestList/__usage__/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": [ 4 | ["transform-runtime", { 5 | "polyfill": false, 6 | "regenerator": true 7 | }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | input[type='checkbox'], 8 | input[type='radio'] { 9 | margin-top: 2px !important; 10 | } 11 | -------------------------------------------------------------------------------- /spec/event_invites.spec.js: -------------------------------------------------------------------------------- 1 | // var assert = require('assert') 2 | 3 | describe('Invitations', () => { 4 | describe('new event with guests', () => {}) 5 | describe('existing event with guests', () => {}) 6 | }) 7 | -------------------------------------------------------------------------------- /src/components/App/__usage__/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Scenario from './scenario' 4 | ReactDOM.render(, document.body.querySelector('#root')) 5 | -------------------------------------------------------------------------------- /src/components/Export/__usage__/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Scenario from './scenario' 4 | ReactDOM.render(, document.body.querySelector('#root')) 5 | -------------------------------------------------------------------------------- /src/components/AppMenu/__usage__/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Scenario from './scenario' 4 | ReactDOM.render(, document.body.querySelector('#root')) 5 | -------------------------------------------------------------------------------- /src/components/Settings/__usage__/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Scenario from './scenario' 4 | ReactDOM.render(, document.body.querySelector('#root')) 5 | -------------------------------------------------------------------------------- /src/components/EventDetails/__usage__/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Scenario from './scenario' 4 | ReactDOM.render(, document.body.querySelector('#root')) 5 | -------------------------------------------------------------------------------- /src/components/EventGuestList/__usage__/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Scenario from './scenario' 4 | ReactDOM.render(, document.body.querySelector('#root')) 5 | -------------------------------------------------------------------------------- /src/components/Terms/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Licenses from './Licenses' 3 | 4 | interface IProps {} 5 | 6 | const Terms: React.FC = (props: IProps) => ( 7 | <> 8 | 9 | 10 | ) 11 | 12 | export default Terms 13 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #b91d47 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/containers/Help.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Help from '../components/Help' 3 | 4 | const mapStateToProps = state => { 5 | return { 6 | user: state.auth.user, 7 | } 8 | } 9 | 10 | const HelpContainer = connect(mapStateToProps)(Help) 11 | 12 | export default HelpContainer 13 | -------------------------------------------------------------------------------- /src/components/AppMenu/__usage__/scenario.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import { AppMenu } from '..' 4 | 5 | class Scenario extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 |
11 | ) 12 | } 13 | } 14 | 15 | export default Scenario 16 | -------------------------------------------------------------------------------- /src/css/EventDetails.css: -------------------------------------------------------------------------------- 1 | .reminder-group { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | 6 | .reminder-group input, 7 | .reminder-group select { 8 | width: 48%; 9 | } 10 | 11 | .reminder-group select { 12 | height: 34px; 13 | } 14 | 15 | .container-fluid .row + .row { 16 | margin-top: 15px; 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | end_of_line = lf 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | 8 | [*.{html,js}] 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.{json, css}] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [*.md] 17 | indent_style = space 18 | indent_size = 4 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /src/containers/index.js: -------------------------------------------------------------------------------- 1 | import CalendarContainer from './Calendar' 2 | import PublicCalendarContainer from './PublicCalendar' 3 | import SettingsContainer from './Settings' 4 | import FilesContainer from './Files' 5 | import HelpContainer from './Help' 6 | 7 | export { 8 | CalendarContainer, 9 | PublicCalendarContainer, 10 | SettingsContainer, 11 | FilesContainer, 12 | HelpContainer, 13 | } 14 | -------------------------------------------------------------------------------- /src/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { connectRouter } from 'connected-react-router' 3 | 4 | // Reducers 5 | import auth from './auth/reducer' 6 | import events from './event/reducer' 7 | import gaia from './gaia/reducer' 8 | 9 | export default history => 10 | combineReducers({ 11 | router: connectRouter(history), 12 | auth, 13 | events, 14 | gaia, 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/Help/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Profile } from '@stacks/profile' 3 | import FAQs from '../FAQ' 4 | import QuestionsWeb from './QuestionsWeb' 5 | import QuestionsDevs from './QuestionsDevs' 6 | 7 | interface IProps { 8 | user?: Profile 9 | } 10 | 11 | const Help: React.FC = (props: IProps) => ( 12 | <> 13 | 14 | 15 | 16 | 17 | ) 18 | 19 | export default Help 20 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const parseQueryString = query => { 2 | return (query.replace(/^\?/, '').split('&') || []).reduce((acc, d) => { 3 | const [k, v] = d.split('=') 4 | if (k) { 5 | acc[k] = v 6 | } 7 | return acc 8 | }, {}) 9 | } 10 | 11 | export const encodeQueryString = obj => { 12 | const q = Object.keys(obj).map(k => { 13 | return [k, obj[k]].join('=') 14 | }) 15 | 16 | return q && q.length ? `?${q.join('&')}` : '' 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # parcel usage files 13 | .cache 14 | dist 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Editor 28 | .idea 29 | .vscode 30 | -------------------------------------------------------------------------------- /src/fontawesome.js: -------------------------------------------------------------------------------- 1 | // Font Awesome 2 | import { library } from '@fortawesome/fontawesome-svg-core' 3 | import { 4 | faMinus, 5 | faPlus, 6 | faTrashAlt, 7 | faUserCircle, 8 | faSync, 9 | faHandHoldingHeart, 10 | faFileCode, 11 | faQuestion, 12 | } from '@fortawesome/free-solid-svg-icons' 13 | 14 | library.add([ 15 | faMinus, 16 | faPlus, 17 | faTrashAlt, 18 | faUserCircle, 19 | faSync, 20 | faHandHoldingHeart, 21 | faFileCode, 22 | faQuestion, 23 | ]) 24 | -------------------------------------------------------------------------------- /src/components/App/__test__/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { App } from '..' 4 | 5 | let Calendar = props => { 6 | return
Calendar
7 | } 8 | let UserProfile = props => { 9 | return
UserProfile
10 | } 11 | 12 | it('renders without crashing', () => { 13 | const div = document.createElement('div') 14 | ReactDOM.render(, div) 15 | ReactDOM.unmountComponentAtNode(div) 16 | }) 17 | -------------------------------------------------------------------------------- /src/store/lazy/reducer.js: -------------------------------------------------------------------------------- 1 | import { SET_LAZY_VIEW } from '../ActionTypes' 2 | 3 | let initialState = {} 4 | 5 | export default function reduce(state = initialState, action = {}) { 6 | // console.log("LazyReducer", state); 7 | let newState = state 8 | const { type, payload } = action 9 | 10 | switch (type) { 11 | case SET_LAZY_VIEW: 12 | newState = { ...state, ...payload } 13 | break 14 | default: 15 | newState = state 16 | break 17 | } 18 | return newState 19 | } 20 | -------------------------------------------------------------------------------- /src/store/gaia/reducer.js: -------------------------------------------------------------------------------- 1 | import { SET_FILES } from '../ActionTypes' 2 | 3 | export let initialState = { 4 | files: {}, 5 | } 6 | 7 | export default function reduce(state = initialState, action = {}) { 8 | const { type, payload } = action 9 | let newState = state 10 | console.log('[FILEREDUCER]', type, payload) 11 | switch (type) { 12 | case SET_FILES: 13 | newState = { ...state, files: payload.files } 14 | break 15 | default: 16 | break 17 | } 18 | return newState 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Export/File.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const File = props => { 5 | const { url, name, ics } = props 6 | 7 | return ( 8 |
9 | {name} 10 | {ics && ( 11 | 12 |   13 | {name}.ics 14 | 15 | )} 16 |
17 | ) 18 | } 19 | 20 | File.propTypes = { 21 | ics: PropTypes.string, 22 | name: PropTypes.string, 23 | url: PropTypes.string, 24 | } 25 | 26 | export default memo(File) 27 | -------------------------------------------------------------------------------- /src/containers/Files.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { showFiles, loadingFiles } from '../store/gaia/actions' 3 | import Files from '../components/Export' 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | files: state.gaia.files, 8 | } 9 | } 10 | const mapDispatchToProps = (dispatch, redux) => { 11 | return { 12 | refreshFiles: () => { 13 | dispatch(loadingFiles()) 14 | dispatch(showFiles()) 15 | }, 16 | } 17 | } 18 | 19 | const FilesContainer = connect(mapStateToProps, mapDispatchToProps)(Files) 20 | 21 | export default FilesContainer 22 | -------------------------------------------------------------------------------- /public/_headers: -------------------------------------------------------------------------------- 1 | /index.html 2 | Access-Control-Allow-Origin: * 3 | Access-Control-Allow-Headers: "X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding" 4 | Access-Control-Allow-Methods: "POST, GET, OPTIONS, DELETE, PUT" 5 | can't-be-evil: true 6 | /manifest.json 7 | Access-Control-Allow-Origin: * 8 | Access-Control-Allow-Headers: "X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding" 9 | Access-Control-Allow-Methods: "POST, GET, OPTIONS, DELETE, PUT" 10 | can't-be-evil: true 11 | /* 12 | can't-be-evil: true 13 | -------------------------------------------------------------------------------- /src/containers/AppMenu.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | // Components 4 | import AppMenu from '../components/AppMenu' 5 | 6 | const mapStateToProps = state => { 7 | const { events, auth } = state 8 | const { user } = auth 9 | let username = null 10 | let signedIn = false 11 | 12 | if (user) { 13 | username = user.username 14 | signedIn = true 15 | } 16 | 17 | let page = events.showPage 18 | if (!page) { 19 | page = 'all' 20 | } 21 | 22 | return { username, signedIn, page } 23 | } 24 | 25 | const AppMenuContainer = connect(mapStateToProps, null)(AppMenu) 26 | 27 | export default AppMenuContainer 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/react" 8 | ], 9 | "plugins": [ 10 | "prettier", 11 | "react" 12 | ], 13 | "env": { 14 | "browser": true, 15 | "es6": true, 16 | "jest": true 17 | }, 18 | "globals": { 19 | "__DEV__": false, 20 | "__TEST__": false, 21 | "__PROD__": false, 22 | "__COVERAGE__": false 23 | }, 24 | "rules": { 25 | "valid-jsdoc": "warn", 26 | "react/prop-types": "warn", 27 | "linebreak-style": ["error", "unix"] 28 | }, 29 | "settings": { 30 | "react": { 31 | "pragma": "React", 32 | "version": "16.8" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/containers/RemindersModal.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import { 4 | unsetRemindersInfoRequest, 5 | updateAllNotifEnabled, 6 | } from '../store/event/eventActionLazy' 7 | import RemindersModal from '../components/Calendar/RemindersModal' 8 | 9 | export default connect( 10 | state => { 11 | console.log('[ConnectedRemindersModal]', state) 12 | return {} 13 | }, 14 | dispatch => { 15 | return { 16 | handleRemindersHide: () => { 17 | dispatch(unsetRemindersInfoRequest()) 18 | }, 19 | handleNoPermissionCheck: () => { 20 | dispatch(updateAllNotifEnabled(false)) 21 | }, 22 | } 23 | } 24 | )(RemindersModal) 25 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "OI Calendar", 3 | "name": "OI Calendar", 4 | "description": "Private, Encrypted Calendar in the Cloud", 5 | "icons": [ 6 | { 7 | "src": "https://cal.openintents.org/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "https://cal.openintents.org/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "start_url": "https://cal.openintents.org", 18 | "display": "standalone", 19 | "theme_color": "#ffffff", 20 | "background_color": "#ffffff" 21 | } 22 | -------------------------------------------------------------------------------- /src/store/event/eventAction.js: -------------------------------------------------------------------------------- 1 | import { SET_CURRENT_EVENT, UNSET_CURRENT_EVENT } from '../ActionTypes' 2 | 3 | export function setCurrentEvent(eventDetails) { 4 | return { 5 | type: SET_CURRENT_EVENT, 6 | payload: { currentEvent: eventDetails }, 7 | } 8 | } 9 | 10 | export function setNewCurrentEvent(eventDetails, eventType) { 11 | return { 12 | type: SET_CURRENT_EVENT, 13 | payload: { currentEvent: eventDetails, currentEventType: eventType }, 14 | } 15 | } 16 | 17 | export function unsetCurrentEvent() { 18 | return { type: UNSET_CURRENT_EVENT } 19 | } 20 | 21 | export function setCurrentEventUid(uid, eventType) { 22 | return { 23 | type: SET_CURRENT_EVENT, 24 | payload: { currentEventUid: uid, currentEventType: eventType }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import * as Containers from './containers' 2 | import Terms from './components/Terms' 3 | 4 | export const routes = [ 5 | { 6 | component: Containers.CalendarContainer, 7 | exact: true, 8 | path: '/', 9 | }, 10 | { 11 | component: Containers.PublicCalendarContainer, 12 | exact: true, 13 | path: '/public', 14 | }, 15 | { 16 | component: Containers.SettingsContainer, 17 | exact: true, 18 | path: '/settings', 19 | }, 20 | { 21 | component: Containers.FilesContainer, 22 | exact: true, 23 | path: '/files', 24 | }, 25 | { 26 | component: Containers.HelpContainer, 27 | exact: true, 28 | path: '/help', 29 | }, 30 | { 31 | component: Terms, 32 | exact: true, 33 | path: '/terms', 34 | }, 35 | ] 36 | -------------------------------------------------------------------------------- /public/.well-known/assetlinks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "relation": ["delegate_permission/common.handle_all_urls"], 4 | "target": { 5 | "namespace": "android_app", 6 | "package_name": "org.openintents.calendar.sync", 7 | "sha256_cert_fingerprints": [ 8 | "E6:34:C9:F2:9F:CB:18:1F:E6:C3:7C:40:78:5B:16:EC:13:99:4A:ED:86:E1:01:43:9C:92:7B:E8:A8:C9:12:7F" 9 | ] 10 | } 11 | }, 12 | { 13 | "relation": ["delegate_permission/common.handle_all_urls"], 14 | "target": { 15 | "namespace": "android_app", 16 | "package_name": "org.openintents.calendar.sync", 17 | "sha256_cert_fingerprints": [ 18 | "54:55:E3:18:09:6E:33:39:16:FB:E5:E9:53:3B:1F:06:1B:84:88:A8:23:3B:99:F4:7B:C8:A2:9C:C7:6D:BE:B0" 19 | ] 20 | } 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider, connect } from 'react-redux' 4 | 5 | // App 6 | import App from './components/App' 7 | 8 | // Redux Store 9 | import store, { history } from './store' 10 | 11 | // Styles 12 | import './index.css' 13 | 14 | // FontAwesome 15 | import './fontawesome' 16 | 17 | // Service Worker 18 | import registerServiceWorker from './registerServiceWorker' 19 | import { initializeLazyActions } from './store/event/eventActionLazy' 20 | 21 | registerServiceWorker() 22 | const ConnectedApp = connect(state => { 23 | return { auth: state.auth } 24 | })(App) 25 | 26 | ReactDOM.render( 27 | 28 | 32 | , 33 | document.getElementById('root') 34 | ) 35 | -------------------------------------------------------------------------------- /src/components/Export/__usage__/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 15 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/AppMenu/__usage__/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 15 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/Settings/__usage__/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 15 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/EventDetails/__usage__/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 15 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /spec/ICS_EXAMPLE_FILE.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | CALSCALE:GREGORIAN 4 | PRODID:adamgibbons/ics 5 | METHOD:PUBLISH 6 | X-PUBLISHED-TTL:PT1H 7 | BEGIN:VEVENT 8 | UID:d9e5e080-d25e-11e8-806a-e73a41d3e47b 9 | SUMMARY:Bolder Boulder 10 | DTSTAMP:20181017T204900Z 11 | DTSTART:20180530T043000Z 12 | DESCRIPTION:Annual 10-kilometer run in Boulder\, Colorado 13 | URL:http://www.bolderboulder.com/ 14 | GEO:40.0095;105.2669 15 | LOCATION:Folsom Field, University of Colorado (finish line) 16 | STATUS:CONFIRMED 17 | CATEGORIES:10k races,Memorial Day Weekend,Boulder CO 18 | ORGANIZER;CN=Admin:mailto:Race@BolderBOULDER.com 19 | ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Adam Gibbons:mailto:adam@example.com 20 | ATTENDEE;RSVP=FALSE;ROLE=OPT-PARTICIPANT;DIR=https://linkedin.com/in/brittanyseaton;CN=Brittany 21 | Seaton:mailto:brittany@example2.org 22 | DURATION:PT6H30M 23 | END:VEVENT 24 | END:VCALENDAR -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import { Nav, Navbar } from 'react-bootstrap' 3 | 4 | // Containers 5 | import AppMenuContainer from '../containers/AppMenu' 6 | import UserProfileContainer from '../containers/UserProfile' 7 | 8 | const Header = () => { 9 | return ( 10 |
11 | 12 | 13 | 14 | OI Calendar Logo 19 | 20 | OI Calendar 21 | 22 | 23 | 24 | 28 | 29 | 30 |
31 | ) 32 | } 33 | 34 | export default memo(Header) 35 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import { routerMiddleware } from 'connected-react-router' 3 | import thunk from 'redux-thunk' 4 | import { logger } from 'redux-logger' 5 | 6 | import { createBrowserHistory } from 'history' 7 | 8 | // Reducers 9 | import reducers from './reducers' 10 | 11 | export const history = createBrowserHistory() 12 | 13 | // Build the middleware for intercepting and dispatching navigation actions 14 | const historyMiddleware = routerMiddleware(history) 15 | 16 | // Redux DevTools 17 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 18 | 19 | // Middlewares 20 | const middlewares = [thunk, historyMiddleware] 21 | 22 | // Redux Logger 23 | if (process.env.NODE_ENV === 'development') { 24 | middlewares.push(logger) 25 | } 26 | 27 | const store = createStore( 28 | reducers(history), 29 | composeEnhancers(applyMiddleware(...middlewares)) 30 | ) 31 | 32 | store.asyncReducers = {} 33 | 34 | export default store 35 | -------------------------------------------------------------------------------- /src/components/Terms/Licenses.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Card, Container, Row, Col } from 'react-bootstrap' 3 | 4 | const License = props => { 5 | return ( 6 | 7 |
{props.name}
8 |
{props.children}
9 | 10 | ) 11 | } 12 | const Licenses = () => ( 13 | 14 | 15 |

Open Source Licenses

16 | This software may contain portions of the following libraries, subject to 17 | the below licenses. 18 |
19 | 20 | 21 | 22 | 23 | This project started with a fork of work by{' '} 24 | 25 | Yasna R. 26 | 27 | 28 | MIT License 29 | 30 | 31 | 32 |
33 | ) 34 | 35 | export default Licenses 36 | -------------------------------------------------------------------------------- /src/components/App/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Route } from 'react-router-dom' 4 | 5 | // Views 6 | import Footer from '../Footer' 7 | import Header from '../Header' 8 | 9 | // Routes 10 | import { routes } from '../../routes' 11 | 12 | // Styles 13 | import './App.css' 14 | import { ConnectedRouter } from 'connected-react-router' 15 | 16 | export class App extends Component { 17 | // componentDidMount() { 18 | // import('./LazyLoaded').then(({ initializeLazy }) => { 19 | // initializeLazy(store) 20 | // }) 21 | // } 22 | 23 | componentWillMount() { 24 | this.props.initializeLazyActions()(this.props.dispatch) 25 | } 26 | 27 | render() { 28 | return ( 29 | 30 |
31 |
32 | 33 | {routes.map(route => ( 34 | 35 | ))} 36 | 37 |
38 |
39 |
40 | ) 41 | } 42 | } 43 | 44 | App.propTypes = { 45 | history: PropTypes.object.isRequired, 46 | } 47 | 48 | export default App 49 | -------------------------------------------------------------------------------- /src/core/eventDefaults.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import { uuid } from './eventFN' 3 | 4 | export const defaultEvents = [ 5 | { 6 | id: 0, 7 | title: 'Today!', 8 | allDay: true, 9 | start: new Date(moment()), // :Q: is moment really required here? 10 | end: new Date(moment()), 11 | hexColor: '#001F3F', 12 | notes: 'Have a great day!', 13 | }, 14 | ] 15 | 16 | const uuids = [uuid(), uuid(), uuid()] 17 | 18 | export let defaultCalendars = [ 19 | { 20 | uid: uuids[0], 21 | type: 'private', 22 | name: 'default', 23 | data: { src: 'default/AllEvents', events: defaultEvents }, 24 | hexColor: '#001F3F', 25 | }, 26 | { 27 | uid: uuids[1], 28 | type: 'blockstack-user', 29 | name: 'public@friedger.id', 30 | mode: 'read-only', 31 | data: { user: 'friedger.id', src: 'public/AllEvents' }, 32 | hexColor: '#2ECC40', 33 | }, 34 | { 35 | uid: uuids[2], 36 | type: 'ics', 37 | name: 'US Holidays', 38 | mode: 'read-only', 39 | data: { 40 | src: 'https://cal.openintents.org/data/US_Holidays.ics', 41 | }, 42 | hexColor: '#FF851B', 43 | }, 44 | ] 45 | -------------------------------------------------------------------------------- /src/containers/UserProfile.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | // Components 4 | import UserProfile from '../components/UserProfile' 5 | 6 | // Actions 7 | import { signUserIn, signUserOut } from '../store/auth/actions' 8 | 9 | const mapStateToProps = state => { 10 | const user = state.auth.user 11 | const profile = user != null ? state.auth.user.profile : null 12 | return { 13 | isSignedIn: user != null, 14 | isConnecting: user == null && state.auth.userMessage === 'Connecting', 15 | name: profile != null ? profile.name : null, 16 | avatarUrl: 17 | profile != null && 'image' in profile && profile.image.length > 0 18 | ? profile.image[0].contentUrl 19 | : null, 20 | identityAddress: user != null && user.identityAddress, 21 | message: state.auth.userMessage, 22 | } 23 | } 24 | 25 | const mapDispatchToProps = dispatch => { 26 | return { 27 | userSignIn: () => dispatch(signUserIn()), 28 | userSignOut: () => dispatch(signUserOut()), 29 | } 30 | } 31 | 32 | const UserProfileContainer = connect( 33 | mapStateToProps, 34 | mapDispatchToProps 35 | )(UserProfile) 36 | 37 | export default UserProfileContainer 38 | -------------------------------------------------------------------------------- /src/store/auth/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | AUTH_CONNECTED, 3 | AUTH_CONNECTING, 4 | AUTH_DISCONNECTED, 5 | AUTH_SIGN_IN, 6 | AUTH_SIGN_OUT, 7 | } from '../ActionTypes' 8 | 9 | let initialState = { 10 | user: undefined, 11 | userMessage: '', 12 | } 13 | 14 | export default function reduce(state = initialState, action = {}) { 15 | const { type } = action 16 | let newState = state 17 | switch (type) { 18 | case AUTH_CONNECTED: 19 | const userSession = action.payload.userSession 20 | const userOwnedStorage = action.payload.userOwnedStorage 21 | newState = { 22 | ...state, 23 | user: action.payload.user, 24 | userSession, 25 | userOwnedStorage, 26 | } 27 | break 28 | 29 | case AUTH_CONNECTING: 30 | newState = { ...state, userMessage: 'connecting' } 31 | break 32 | 33 | case AUTH_DISCONNECTED: 34 | newState = { ...state, user: undefined, userMessage: 'disconnected' } 35 | break 36 | 37 | case AUTH_SIGN_IN: 38 | newState = { ...state, userMessage: 'redirecting to sign-in' } 39 | break 40 | 41 | case AUTH_SIGN_OUT: 42 | newState = { ...state, user: undefined, userMessage: 'signed out' } 43 | break 44 | 45 | default: 46 | newState = state 47 | break 48 | } 49 | return newState 50 | } 51 | -------------------------------------------------------------------------------- /spec/template.spec.js: -------------------------------------------------------------------------------- 1 | // var assert = require('assert') 2 | 3 | describe('index test', () => { 4 | describe('sayHello function', () => { 5 | it('should say Hello guys!', () => { 6 | console.log('hello') 7 | }) 8 | }) 9 | }) 10 | 11 | /* 12 | assert.fail(actual, expected, message, operator) 13 | Throws an exception that displays the values for actual and expected separated by the provided operator. 14 | 15 | assert(value, message), assert.ok(value, [message]) 16 | Tests if value is truthy, it is equivalent to assert.equal(true, !!value, message); 17 | 18 | assert.equal(actual, expected, [message]) 19 | Tests shallow, coercive equality with the equal comparison operator ( == ). 20 | 21 | assert.notEqual(actual, expected, [message]) 22 | Tests shallow, coercive non-equality with the not equal comparison operator ( != ). 23 | 24 | assert.deepEqual(actual, expected, [message]) 25 | Tests for deep equality. 26 | 27 | assert.notDeepEqual(actual, expected, [message]) 28 | Tests for any deep inequality. 29 | 30 | assert.strictEqual(actual, expected, [message]) 31 | Tests strict equality, as determined by the strict equality operator ( === ) 32 | 33 | assert.notStrictEqual(actual, expected, [message]) 34 | Tests strict non-equality, as determined by the strict not equal operator ( !== ) 35 | 36 | assert.throws(block, [error], [message]) 37 | */ 38 | -------------------------------------------------------------------------------- /src/core/eventFN.js: -------------------------------------------------------------------------------- 1 | export function uuid() { 2 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 3 | var r = (Math.random() * 16) | 0 4 | 5 | var v = c === 'x' ? r : (r & 0x3) | 0x8 6 | return v.toString(16) 7 | }) 8 | } 9 | 10 | export function sharedUrl(eventUid) { 11 | return 'shared/' + eventUid + '/event.json' 12 | } 13 | 14 | const defaultColors = { 15 | navy: { color: '#001F3F' }, 16 | blue: { color: '#0074D9' }, 17 | aqua: { color: '#7FDBFF' }, 18 | teal: { color: '#39CCCC' }, 19 | olive: { color: '#3D9970' }, 20 | green: { color: '#2ECC40' }, 21 | lime: { color: '#01FF70' }, 22 | yellow: { color: '#FFDC00' }, 23 | orange: { color: '#FF851B' }, 24 | red: { color: '#FF4136' }, 25 | fuchsia: { color: '#F012BE' }, 26 | purple: { color: '#B10DC9' }, 27 | maroon: { color: '#85144B' }, 28 | silver: { color: '#DDDDDD' }, 29 | gray: { color: '#AAAAAA' }, 30 | black: { color: '#111111' }, 31 | } 32 | const defaultColorList = Object.values(defaultColors) 33 | 34 | export function guaranteeHexColor(hex) { 35 | return ( 36 | hex || 37 | defaultColorList[Math.floor(Math.random() * defaultColorList.length)].color 38 | ) 39 | } 40 | 41 | export function objectToArray(obj) { 42 | let out = obj 43 | if (out && !Array.isArray(out)) { 44 | out = Object.values(out) 45 | } 46 | return out 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Export/__usage__/scenario.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Export from './Export' 4 | 5 | class Scenario extends Component { 6 | constructor(props) { 7 | super(props) 8 | this.state({ 9 | files: { 10 | gaiahub: 'https://gaia.blockstack.org/something', 11 | calendarListFile: 'https://gaia.blockstack.org/something/Contacts', 12 | contactListFile: 'https://gaia.blockstack.org/something/Contacts', 13 | calendars: { 14 | private: [ 15 | { 16 | name: 'default', 17 | url: 'https://gaia.blockstack.org/something/default/AllEvents', 18 | }, 19 | ], 20 | public: [ 21 | { 22 | name: 'public', 23 | url: 'https://gaia.blockstack.org/something/public/AllEvents', 24 | icsurl: 25 | 'https://gaia.blockstack.org/something/public/AllEvents.ics', 26 | }, 27 | ], 28 | }, 29 | others: [ 30 | { 31 | name: 'anotherfile', 32 | url: 'https://gaia.blockstack.org/something/anotherfile.txt', 33 | }, 34 | ], 35 | }, 36 | }) 37 | } 38 | 39 | render() { 40 | const { files } = this.state 41 | return ( 42 |
43 | 44 |
45 | ) 46 | } 47 | } 48 | 49 | export default Scenario 50 | -------------------------------------------------------------------------------- /spec/event_crud.spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var storeManager = require('../src/store') 3 | var eventActionLazy = require('../src/store/event/eventActionLazy') 4 | 5 | const store = storeManager.createInitialStore({}) 6 | const EVENT_URL = 7 | 'https://chat.openintents.org/#/room/#oi-calendar:openintents.modular.im' 8 | 9 | describe('CRUD Events', () => { 10 | describe('add private event', () => { 11 | it('should store a new private event', () => { 12 | store 13 | .dispatch( 14 | eventActionLazy.addEvent({ 15 | title: 'Testing add private event', 16 | url: EVENT_URL, 17 | }) 18 | ) 19 | .then(() => { 20 | const allEvents = store.getState().events.allEvents 21 | assert.strict.equal( 22 | allEvents, 23 | 2, 24 | 'Should contain default event and new event' 25 | ) 26 | assert.strict.equal( 27 | allEvents[1].url, 28 | EVENT_URL, 29 | 'Should contain the correcly event url' 30 | ) 31 | }) 32 | }) 33 | }) 34 | 35 | describe('add public event', () => { 36 | it('should publish the event', () => {}) 37 | }) 38 | 39 | describe('update event', () => { 40 | it('should store the updated event', () => {}) 41 | }) 42 | 43 | describe('delete event', () => { 44 | it('should remove the event', () => {}) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/usecases.md: -------------------------------------------------------------------------------- 1 | # Use cases 2 | 3 | ## CRUD 4 | 5 | ### Create 6 | 7 | - User clicks (or drags) on a day or timeslot 8 | - Event details are shown 9 | - User confirms details 10 | - Event details are saved 11 | 12 | ### Create with Invites 13 | 14 | - User clicks (or drags) on a day or timeslot 15 | - Event details are shown 16 | - User adds guests 17 | - Details of guests are shown 18 | - User confirms to send invites 19 | - Invites are sent 20 | - Event details are shown 21 | 22 | ### Update Event 23 | 24 | - User clicks on an event 25 | - Event details are shown 26 | - User changes event details 27 | - User confirms to save details 28 | 29 | ### Resend Invites 30 | 31 | - User clicks on an event 32 | - User clicks on "Send Invites" 33 | - Guest details are shown 34 | - User confirms to send details 35 | - Invites are sent 36 | - Event details are hidden 37 | 38 | ### Delete Event 39 | 40 | - User clicks on an event 41 | - User clicks on "Delete event" 42 | - Event is deleted 43 | - Event details are hidden 44 | 45 | ## RSVP 46 | 47 | - User clicks on a privatly shared link 48 | - Event details are shown 49 | - User confirms to add event to calendar 50 | - Event is saved in default calendar 51 | 52 | ### RSVP while not signed in 53 | 54 | - User clicks on a privately shared link 55 | - Event details are shown 56 | - User confirms to add event to calendar 57 | - User is redirected to login 58 | - After login event is saved in default calendar 59 | - Confirmation message is shown 60 | -------------------------------------------------------------------------------- /src/components/Help/QuestionsDevs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Card, Container, Row } from 'react-bootstrap' 3 | import { FAQ } from '../FAQ' 4 | 5 | const QuestionsWeb = () => ( 6 | 7 | 8 |

FAQ for Developers

9 |
10 | 11 | 12 | 13 | 14 | Add a button with a link to 15 | "https://cal.openintents.org/?intent=addevent" and add query 16 | parameters for 17 |
    18 |
  • 19 | start Start date in a format that can be parsed by 20 | Date. 21 |
  • 22 |
  • 23 | end End date in a format that can be parsed by Date. 24 |
  • 25 |
  • 26 | title The title of the event. 27 |
  • 28 |
  • 29 | via A blockstack ID that will be added as guest. 30 |
  • 31 |
32 |
33 | 38 | Add a button with a link to 39 | "https://cal.openintents.org/?intent=addics" and add query 40 | parameters for 41 |
    42 |
  • 43 | url The url to the calendar feed. 44 |
  • 45 |
46 |
47 |
48 |
49 |
50 |
51 | ) 52 | 53 | export default QuestionsWeb 54 | -------------------------------------------------------------------------------- /src/components/UserProfile/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {} from 'react-jdenticon' 4 | 5 | // Components 6 | import BlockstackSignInButton from './SignInButton' 7 | 8 | const UserProfile = props => { 9 | const { 10 | isSignedIn, 11 | isConnecting, 12 | name, 13 | avatarUrl, 14 | message, 15 | identityAddress, 16 | } = props 17 | 18 | if (isSignedIn) { 19 | const image = avatarUrl ? ( 20 | {name} 21 | ) : ( 22 | 23 | ) 24 | 25 | return ( 26 |
27 | 34 |
35 | ) 36 | } 37 | 38 | if (isConnecting) { 39 | return
{message}
40 | } 41 | 42 | return ( 43 |
44 | 50 |
51 | ) 52 | } 53 | 54 | UserProfile.propTypes = { 55 | isSignedIn: PropTypes.bool, 56 | isConnecting: PropTypes.bool, 57 | name: PropTypes.string, 58 | avatarUrl: PropTypes.string, 59 | identityAddress: PropTypes.string, 60 | message: PropTypes.string, 61 | userSignIn: PropTypes.func, 62 | userSignOut: PropTypes.func, 63 | } 64 | 65 | export default UserProfile 66 | -------------------------------------------------------------------------------- /src/components/Calendar/RemindersModal.js: -------------------------------------------------------------------------------- 1 | import { Button, Modal } from 'react-bootstrap' 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | 5 | class RemindersModal extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | console.log('SetRemindersModal') 9 | } 10 | 11 | render() { 12 | const { handleRemindersHide, handleNoPermissionCheck } = this.props 13 | let notifPermissionGranted = true 14 | if (Notification.permission !== 'granted') { 15 | notifPermissionGranted = false 16 | Notification.requestPermission() 17 | } 18 | return ( 19 | 20 | 21 | Reminders 22 | 23 | 24 | {!notifPermissionGranted && ( 25 | <> 26 | Please grant permission to receive notifcations from the browser. 27 | 28 | )} 29 | {notifPermissionGranted && ( 30 | <> 31 | You already granted permissions to receive notifications from the 32 | browser! 33 | 34 | )} 35 | 36 | 37 | 40 | 41 | 42 | 43 | ) 44 | } 45 | } 46 | 47 | RemindersModal.propTypes = { 48 | handleRemindersHide: PropTypes.func, 49 | handleNoPermissionCheck: PropTypes.func, 50 | } 51 | 52 | export default RemindersModal 53 | -------------------------------------------------------------------------------- /spec/event_ical.spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var iCal = require('../src/core/ical') 3 | var fs = require('fs') 4 | 5 | // eslint-disable-next-line node/no-deprecated-api 6 | require.extensions['.ics'] = function(module, filename) { 7 | module.exports = fs.readFileSync(filename, 'utf8') 8 | } 9 | 10 | var ICS_EXAMPLE_FILE = require('./ICS_EXAMPLE_FILE.ics') 11 | 12 | describe('Import/Export iCal (testing duration field)', () => { 13 | describe('importing', () => { 14 | it('should import events with duration', () => { 15 | let events = iCal.iCalParseEvents(ICS_EXAMPLE_FILE) 16 | assert.strictEqual(events.length, 1, 'Should return one event') 17 | assert.strictEqual( 18 | events[0].duration, 19 | '06:30', 20 | 'Should return the duration in the expected format' 21 | ) 22 | }) 23 | 24 | it('should export events with duration', () => { 25 | let event = { 26 | title: 'Test', 27 | description: 'Test', 28 | start: new Date(), 29 | duration: '06:30', 30 | } 31 | 32 | let icsEvent = iCal.eventAsIcs(event) 33 | console.log(icsEvent) 34 | assert.notStrictEqual(icsEvent, null, 'Should return one event') 35 | assert.strictEqual( 36 | icsEvent.duration.hours, 37 | 6, 38 | 'Should return duration in the expected format' 39 | ) 40 | 41 | assert.strictEqual( 42 | icsEvent.duration.minutes, 43 | 30, 44 | 'Should return duration in the expected format' 45 | ) 46 | 47 | assert.strictEqual( 48 | icsEvent.duration.seconds, 49 | 0, 50 | 'Should return duration in the expected format' 51 | ) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/store/auth/actions.js: -------------------------------------------------------------------------------- 1 | import { AUTH_SIGN_IN, AUTH_SIGN_OUT } from '../ActionTypes' 2 | import { UserSession, AppConfig, showConnect } from '@stacks/connect' 3 | import { authenticatedAction } from '../../store/event/eventActionLazy' 4 | import { UserOwnedStorage } from '../../core/event' 5 | export function redirectedToSignIn() { 6 | return { type: AUTH_SIGN_IN } 7 | } 8 | 9 | export function signUserIn(store) { 10 | return async (dispatch, getState) => { 11 | const userSession = new UserSession( 12 | new AppConfig( 13 | ['store_write'], 14 | `${window.location.origin}`, 15 | `${window.location}`, 16 | `${window.location.origin}/manifest.json` 17 | ) 18 | ) 19 | try { 20 | showConnect({ 21 | userSession, 22 | appDetails: { 23 | name: 'OI Calendar', 24 | icon: 'https://cal.openintents.org/android-chrome-192x192.png', 25 | }, 26 | onFinish: ({ userSession }) => { 27 | const userData = userSession.loadUserData() 28 | dispatch( 29 | authenticatedAction( 30 | userData, 31 | userSession, 32 | new UserOwnedStorage(userSession) 33 | ) 34 | ) 35 | }, 36 | }) 37 | dispatch(redirectedToSignIn()) 38 | } catch (e) { 39 | console.error(e) 40 | } 41 | } 42 | } 43 | 44 | export function signUserOut() { 45 | const userSession = new UserSession( 46 | new AppConfig( 47 | ['store_write', 'publish_data'], 48 | `${window.location.origin}`, 49 | `${window.location}`, 50 | `${window.location.origin}/manifest.json` 51 | ) 52 | ) 53 | try { 54 | userSession.signUserOut() 55 | return { type: AUTH_SIGN_OUT } 56 | } catch (e) { 57 | console.error(e) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Settings/__usage__/scenario.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Calendars from '../Calendars' 4 | import Contacts from '../Contacts' 5 | import Settings from '..' 6 | 7 | class Scenario extends Component { 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | calendars: [ 12 | { 13 | type: 'private', 14 | name: 'default', 15 | active: true, 16 | data: { src: 'default/AllEvents' }, 17 | }, 18 | { 19 | type: 'blockstack-user', 20 | name: 'public@friedger.id', 21 | mode: 'read-only', 22 | active: false, 23 | hexColor: '#FF0000', 24 | data: { user: 'friedger.id', src: 'public/AllEvents' }, 25 | }, 26 | ], 27 | contacts: { 28 | 'friedger.id': { roomId: '!oTPxgFhouwHiEGwIpQ:openintents.modular.im' }, 29 | 'pipppapp.id.blockstack': { 30 | roomId: '!vqrdwZGrwdDkQdGgnH:openintents.modular.im', 31 | }, 32 | }, 33 | } 34 | this.lookupContacts = this.lookupContacts.bind(this) 35 | this.addCalendar = this.addCalendar.bind(this) 36 | } 37 | 38 | lookupContacts(query) { 39 | console.log('query', query) 40 | return Promise.resolve(['friedger.id', 'pipppapp.id.blockstack']) 41 | } 42 | 43 | addCalendar(calendar) { 44 | const { calendars } = this.state 45 | calendars.push(calendar) 46 | this.setState({ calendars }) 47 | } 48 | 49 | render() { 50 | const { calendars, contacts } = this.state 51 | return ( 52 |
53 | 54 | 55 | 56 |
57 | ) 58 | } 59 | } 60 | 61 | export default Scenario 62 | -------------------------------------------------------------------------------- /src/components/EventDetails/__usage__/scenario.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import EventDetails from '..' 4 | 5 | const handleHide = () => { 6 | console.log('HIDE') 7 | } 8 | 9 | const deleteEvent = () => { 10 | console.log('DELETE') 11 | } 12 | const addEvent = () => { 13 | console.log('ADD') 14 | } 15 | const updateEvent = () => { 16 | console.log('UPDATE') 17 | } 18 | const loadGuestList = () => { 19 | console.log('LOAD GUEST LIST') 20 | } 21 | 22 | const GuestList = guests => { 23 | return
{JSON.stringify(guests)}
24 | } 25 | 26 | const Scenario = () => { 27 | const eventToAdd = { 28 | eventType: 'add', 29 | eventInfo: { 30 | title: 'Event To Add', 31 | slots: ['2018-12-31T23:00:00.000Z'], 32 | start: new Date('2018-12-31T23:00:00.000Z'), 33 | end: new Date('2018-12-31T23:00:00.000Z'), 34 | action: 'click', 35 | }, 36 | newIndex: 1, 37 | } 38 | const eventToEdit = { 39 | eventType: 'edit', 40 | eventInfo: { 41 | title: 'Event To Edit', 42 | slots: ['2018-12-31T23:00:00.000Z'], 43 | start: new Date('2018-12-31T23:00:00.000Z'), 44 | end: new Date('2018-12-31T23:00:00.000Z'), 45 | action: 'click', 46 | }, 47 | newIndex: 1, 48 | } 49 | return ( 50 |
51 | 60 | 69 |
70 | ) 71 | } 72 | 73 | export default Scenario 74 | -------------------------------------------------------------------------------- /src/containers/SendInvitesModal.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import { setCurrentEvent } from '../store/event/eventAction' 4 | 5 | import { saveAllEvents } from '../store/event/eventActionLazy' 6 | import SendInvitesModal from '../components/Calendar/SendInvitesModal' 7 | import { 8 | setInviteStatus, 9 | sendInvites, 10 | unsetCurrentInvites, 11 | loadGuestList, 12 | } from '../store/event/contactActionLazy' 13 | 14 | export default connect( 15 | state => { 16 | console.log('[ConnectedSendInvitesModal]', state) 17 | const { currentEvent, currentEventType, currentGuests } = state.events 18 | const inviteError = state.events.inviteError 19 | const inviteSuccess = state.events.inviteSuccess 20 | const inviteStatus = state.events.inviteStatus 21 | const sending = inviteStatus === 'started' 22 | 23 | return { 24 | inviteStatus, 25 | inviteError, 26 | inviteSuccess, 27 | currentEvent, 28 | currentEventType, 29 | guests: currentEvent.guests, 30 | title: currentEvent.title, 31 | sending, 32 | profiles: currentGuests, 33 | } 34 | }, 35 | dispatch => { 36 | return { 37 | handleInvitesHide: (inviteError, eventDetails) => { 38 | dispatch(setInviteStatus(undefined)) 39 | dispatch(unsetCurrentInvites()) 40 | eventDetails.noInvites = !inviteError 41 | dispatch(setCurrentEvent(eventDetails)) 42 | }, 43 | loadGuestList: guests => { 44 | dispatch(loadGuestList(guests)) 45 | }, 46 | sendInvites: (eventDetails, guests, editMode) => { 47 | dispatch(setInviteStatus('started')) 48 | dispatch(sendInvites(eventDetails, guests)).then(allEvents => { 49 | if (editMode === 'add' || editMode === 'edit') { 50 | allEvents[eventDetails.uid] = eventDetails 51 | } 52 | dispatch(saveAllEvents(allEvents)) 53 | }) 54 | }, 55 | } 56 | } 57 | )(SendInvitesModal) 58 | -------------------------------------------------------------------------------- /src/components/App/__usage__/scenario.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import App from '..' 4 | // import ReudxApp from "../redux-app"; 5 | // import ConnectedCalendar from "../redux-calendar"; 6 | // import ConnectedEventDetails from "../redux-event-details"; 7 | // import ConnectedUserProfile from "../redux-user-profile"; 8 | // import ConnectedGuestList from "../redux-guest-list"; 9 | 10 | // import EventDetails from "../../components/event-details/EventDetails"; 11 | // import { LoadGuestList, SendInvites } from "../../store/event/eventActionLazy"; 12 | 13 | // import moment from "moment"; 14 | // import * as blockstack from "blockstack"; 15 | // import * as ics from "ics"; 16 | // import * as ICAL from "ical.js"; 17 | 18 | const doNothing = () => {} 19 | 20 | let Calendar = props => { 21 | return
Calendar
22 | } 23 | let UserProfile = props => { 24 | return
UserProfile
25 | } 26 | 27 | let whenAppLoaded = forceUpdate => { 28 | console.log('whenAppLoaded') 29 | import('../../Calendar').then(({ default: CalendarBase }) => { 30 | Calendar = props => { 31 | return ( 32 | 37 | ) 38 | } 39 | console.log('CalendarLoaded') 40 | forceUpdate() 41 | }) 42 | 43 | import('../../UserProfile').then(({ default: UserProfileBase }) => { 44 | UserProfile = props => { 45 | return ( 46 | 52 | ) 53 | } 54 | console.log('CalendarLoaded') 55 | // forceUpdate(); 56 | }) 57 | } 58 | 59 | let dynamicApp 60 | 61 | class DynamicApp extends Component { 62 | render() { 63 | return 64 | } 65 | doSomething() {} 66 | componentDidMount() { 67 | whenAppLoaded(() => { 68 | console.log('DONE') 69 | this.forceUpdate() 70 | }) 71 | } 72 | } 73 | 74 | const Scenario = () => { 75 | dynamicApp = 76 | return dynamicApp 77 | } 78 | 79 | export default Scenario 80 | -------------------------------------------------------------------------------- /src/core/ical.js: -------------------------------------------------------------------------------- 1 | import { parse, Component, Event } from '../../node_modules/ical.js/build/ical' 2 | import { createEvents } from 'ics' 3 | import moment from 'moment' 4 | 5 | function eventFromIcal(d) { 6 | var vevent = new Event(d) 7 | return { 8 | title: vevent.summary, 9 | start: vevent.startDate.toJSDate().toISOString(), 10 | end: vevent.endDate.toJSDate().toISOString(), 11 | duration: vevent.duration ? durationFromObject(vevent.duration) : null, 12 | uid: vevent.uid, 13 | } 14 | } 15 | 16 | export function iCalParseEvents(icsContent, formatEvent) { 17 | try { 18 | var jCal = parse(icsContent) 19 | var comp = new Component(jCal) 20 | console.log('comp', comp) 21 | var vevents = comp.getAllSubcomponents('vevent') 22 | return vevents.map(eventFromIcal) 23 | } catch (e) { 24 | console.log('ics error', e) 25 | } 26 | } 27 | 28 | export function eventAsIcs(event) { 29 | let { title, description, start, end, allDay, uid, duration } = event 30 | start = dateToArray(allDay, new Date(start)) 31 | end = dateToArray(allDay, new Date(end)) 32 | duration = durationToObject(duration) 33 | return { title, description, start, end, uid, duration } 34 | } 35 | 36 | export function icsFromEvents(events) { 37 | try { 38 | var { error, value } = createEvents(Object.values(events).map(eventAsIcs)) 39 | if (error) { 40 | console.log('error creating ics', error) 41 | } else { 42 | return value 43 | } 44 | } catch (e) { 45 | console.log('failed to format events', e) 46 | } 47 | } 48 | 49 | function dateToArray(allDay, date) { 50 | let base = [date.getFullYear(), date.getMonth() + 1, date.getDate()] 51 | if (allDay) { 52 | return base 53 | } else { 54 | return base.concat([date.getHours(), date.getMinutes(), date.getSeconds()]) 55 | } 56 | } 57 | 58 | function durationToObject(time) { 59 | if (time != null) { 60 | let duration = moment.duration(time) 61 | return { 62 | hours: duration.hours(), 63 | minutes: duration.minutes(), 64 | seconds: duration.seconds(), 65 | } 66 | } 67 | 68 | return null 69 | } 70 | 71 | function durationFromObject(obj) { 72 | return pad(obj.hours, 2) + ':' + pad(obj.minutes, 2) 73 | } 74 | 75 | function pad(num, size) { 76 | var s = num + '' 77 | while (s.length < size) s = '0' + s 78 | return s 79 | } 80 | -------------------------------------------------------------------------------- /src/store/gaia/actions.js: -------------------------------------------------------------------------------- 1 | import { SHOW_FILES, SET_FILES } from '../ActionTypes' 2 | import { getBucketUrl } from '@stacks/storage' 3 | 4 | function showFilesScreen(show) { 5 | return { type: SHOW_FILES, payload: { show } } 6 | } 7 | 8 | function newFile(files, f) { 9 | console.log('received ', f) 10 | if (f.endsWith('Contacts')) { 11 | files.contactListFile = files.appBucketUrl + f 12 | } else if (f.endsWith('Calendars')) { 13 | files.calendarListFile = files.appBucketUrl + f 14 | } else if (f.endsWith('public/AllEvents')) { 15 | files.calendars.public[0].url = files.appBucketUrl + f 16 | } else if (f.endsWith('public/AllEvents.ics')) { 17 | files.calendars.public[0].ics = files.appBucketUrl + f 18 | } else if (f.endsWith('default/AllEvents')) { 19 | files.calendars.private = [{ name: 'private', url: files.appBucketUrl + f }] 20 | } else if (f.endsWith('event.json') && f.startsWith('shared/')) { 21 | files.sharedEvents.push({ name: f, url: files.appBucketUrl + f }) 22 | } else if (f.startsWith('msg/')) { 23 | if (!files.msgs) { 24 | files.msgs = [] 25 | } 26 | files.msgs.push({ name: f, url: files.appBucketUrl + f }) 27 | } else { 28 | files.others.push({ name: f, url: files.appBucketUrl + f }) 29 | } 30 | } 31 | 32 | function setFiles(files, count) { 33 | return { type: SET_FILES, payload: { files, count } } 34 | } 35 | 36 | export function loadingFiles() { 37 | return (dispatch, getState) => { 38 | initFiles(getState().auth.user).then(files => dispatch(setFiles(files, 0))) 39 | } 40 | } 41 | 42 | function initFiles(user) { 43 | return getBucketUrl(user.hubUrl, user.appPrivateKey).then(url => { 44 | const files = { 45 | appBucketUrl: url, 46 | calendars: { 47 | public: [{ name: 'public' }], 48 | }, 49 | others: [], 50 | sharedEvents: [], 51 | } 52 | return files 53 | }) 54 | } 55 | 56 | export function showFiles() { 57 | return (dispatch, getState) => { 58 | const { userSession } = getState().auth 59 | dispatch(showFilesScreen(true)) 60 | initFiles(getState().auth.user).then(files => 61 | userSession 62 | .listFiles(f => { 63 | newFile(files, f) 64 | return true 65 | }) 66 | .then(count => { 67 | dispatch(setFiles(files, count)) 68 | }) 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 43 | OI Calendar 44 | 45 | 46 | 47 | 50 |
51 | 55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /src/components/Settings/Contacts.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 3 | 4 | import AddDeleteSetting from './AddDeleteSetting' 5 | 6 | const LINK_URL_BASE = 'https://debutapp.social/' 7 | 8 | const ContactItem = props => { 9 | const { item, user } = props 10 | let { image, username, name } = item 11 | const linkUrl = LINK_URL_BASE + username 12 | let avatarUrl 13 | 14 | if (image && image.length > 0 && image[0].contentUrl) { 15 | avatarUrl = image[0].contentUrl 16 | } 17 | 18 | if (user && user.username && user.username === username) { 19 | name = 'You (' + username + ')' 20 | } 21 | 22 | return ( 23 | <> 24 | {avatarUrl && ( 25 | avatar 26 | )} 27 | {!avatarUrl && } 28 | 29 | {name || username} 30 | 31 | 32 | ) 33 | } 34 | 35 | export default class Contacts extends AddDeleteSetting { 36 | constructor(props) { 37 | super(props) 38 | const addPlaceholder = 'e.g. alice.id or bob.id.blockstack' 39 | 40 | this.state.ItemRenderer = ContactItem 41 | this.state.addTitle = 'Add Contact' 42 | this.state.listTitle = 'Contacts' 43 | this.state.showFollow = true 44 | this.state.addValueToItem = (valueOfAdd, asyncReturn) => { 45 | const { items: contacts } = this.props 46 | const contactQuery = valueOfAdd 47 | console.log('contactQuery', contactQuery) 48 | 49 | // check if contact already in 50 | const usernames = Object.keys(contacts || {}) 51 | if (usernames.includes(contactQuery)) { 52 | asyncReturn({ error: 'already in' }) 53 | } else { 54 | asyncReturn({ item: { username: contactQuery } }) 55 | } 56 | } 57 | 58 | this.state.renderAdd = () => { 59 | return ( 60 | 67 | ) 68 | } 69 | 70 | this.bound2 = ['onAddValueChange'].reduce((acc, d) => { 71 | acc[d] = this[d].bind(this) 72 | delete this[d] 73 | return acc 74 | }, {}) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/EventGuestList/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { ProgressBar, OverlayTrigger, Tooltip } from 'react-bootstrap' 3 | 4 | const GUEST_BASE = 'https://debutapp.social/' 5 | 6 | const renderGuestList = guests => { 7 | let list = [] 8 | 9 | for (let property in guests) { 10 | if (guests.hasOwnProperty(property)) { 11 | var guest = guests[property] 12 | list.push() 13 | } 14 | } 15 | 16 | return list 17 | } 18 | 19 | export const Guest = ({ guest, username }) => { 20 | const guestUrl = GUEST_BASE + username 21 | var avatarUrl 22 | if (guest.image && guest.image.length > 0 && guest.image[0].contentUrl) { 23 | avatarUrl = guest.image[0].contentUrl 24 | } else { 25 | avatarUrl = '/images/avatar.png' 26 | } 27 | var name = guest.name 28 | if (!name) { 29 | name = guest.username 30 | } 31 | const commMethodUrl = '/images/oichat.png' 32 | return ( 33 |
34 | avatar 35 | {name} 36 | 40 | OI Chat (chat.openintents.org) is a matrix service of Blockstack. 41 | 42 | } 43 | > 44 | 45 | {' '} 46 | (via 47 | avatar) 48 | 49 | 50 |
51 | ) 52 | } 53 | 54 | class GuestList extends Component { 55 | render() { 56 | const guests = this.props.guests 57 | // :Q: when is this supposed to be used. The webservice returns all guests "profiles" at once 58 | const numberOfGuests = this.props.guestsCount || 1 59 | const numberOfGuestsLoaded = this.props.guestsLoaded || 0 60 | let guestView 61 | 62 | if (guests && Object.keys(guests).length > 0) { 63 | guestView = renderGuestList(guests) 64 | } else if (numberOfGuests > 0) { 65 | // :WARN: This branch is never called 66 | guestView = ( 67 |
68 | loading guests' details.. 69 |
70 | {JSON.stringify(guests)} 71 | 75 |
76 | ) 77 | } else { 78 | guestView =
There is nobody on the guest list..
79 | } 80 | 81 | return
{guestView}
82 | } 83 | } 84 | 85 | export default GuestList 86 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/reminder/index.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | import Reminder from './reminder' 4 | 5 | const storage = window.localStorage 6 | let remindersArray 7 | 8 | // Add reminder to localStorage 9 | export const addReminder = (event, guests, userSessionChat) => { 10 | let reminders = getReminders() 11 | 12 | const uid = event.uid 13 | let eventData = { 14 | start: event.start, 15 | title: event.title, 16 | guests: event.guests, 17 | } 18 | 19 | const reminder = new Date(event.start) 20 | const reminderTime = parseInt(event.reminderTime, 10) 21 | 22 | if (event.reminderTimeUnit === 'minutes') { 23 | reminder.setMinutes(reminder.getMinutes() - reminderTime) 24 | } else { 25 | reminder.setHours(reminder.getHours() - reminderTime) 26 | } 27 | 28 | eventData.reminder = reminder 29 | 30 | const timeout = moment(reminder).diff(moment(new Date())) 31 | 32 | let r = remindersArray.find(x => x.uid === uid) 33 | 34 | if (r) { 35 | r.setReminderInterval(timeout) 36 | 37 | // Update event metadata 38 | r.title = event.title 39 | r.start = event.start 40 | r.guests = guests 41 | r.userSessionChat = userSessionChat 42 | } else { 43 | remindersArray.push( 44 | new Reminder( 45 | timeout, 46 | event.title, 47 | uid, 48 | event.start, 49 | guests, 50 | userSessionChat 51 | ) 52 | ) 53 | } 54 | 55 | reminders[uid] = eventData 56 | 57 | setReminders(reminders) 58 | } 59 | 60 | // Get reminders from localStorage 61 | export const getReminders = () => { 62 | try { 63 | return JSON.parse(storage.getItem('reminders')) || {} 64 | } catch (e) { 65 | return {} 66 | } 67 | } 68 | 69 | // Set reminders to localStorage 70 | export const setReminders = reminders => { 71 | storage.setItem('reminders', JSON.stringify(reminders)) 72 | } 73 | 74 | // Get `Reminder` Array from localStorage 75 | export const initReminders = userSessionChat => { 76 | const reminders = getReminders() 77 | let delUid = [] 78 | 79 | remindersArray = Object.keys(reminders).map(key => { 80 | const now = moment.utc() 81 | const reminder = moment(reminders[key].reminder) 82 | const timeout = reminder.diff(now) 83 | 84 | // Store `uid` of events whose reminder time has passed 85 | // To be deleted from localStorage 86 | if (timeout < 0) { 87 | delUid.push(key) 88 | } 89 | 90 | return new Reminder( 91 | timeout, 92 | reminders[key].title, 93 | key, 94 | reminders[key].start, 95 | reminders[key].guests, 96 | userSessionChat 97 | ) 98 | }) 99 | 100 | delUid.forEach(uid => delete reminders[uid]) 101 | 102 | setReminders(reminders) 103 | } 104 | -------------------------------------------------------------------------------- /src/components/Help/QuestionsWeb.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Card, Container, Row } from 'react-bootstrap' 3 | import { FAQ } from '../FAQ' 4 | 5 | const QuestionsWeb = () => ( 6 | 7 | 8 |

FAQ Web app

9 |
10 | 11 | 12 | 13 | 14 | In Settings, you can enabled enriched notifications. With this 15 | settings, the app will send you a message currently using matrix.org 16 | via the OI Chat matrix server, if the app is still running at the 17 | time when the notification should be sent. 18 |
19 | The message will contain details about the invited guests of the 20 | relevant event. The details contain information from the SpringRole 21 | application if available. Please note that not all users have a 22 | profile on SpringRole. 23 |
24 | Using OI Chat matrix server requires that you agree to their terms 25 | and conditions before sending the first message. 26 |
27 | 28 | When creating or editing an event, you can add guests by their 29 | blockstack id. For new events with guests, you are asked about 30 | invitations while saving the event, for existing events with guests, 31 | you can always trigger invitations with the "Send invitations" 32 | button. 33 |
34 | Invitations are sent via the OI Chat matrix server. All sent 35 | messages are stored in your own storage (see "Files" menu). Using OI 36 | Chat matrix server requires that you agree to their terms and 37 | conditions before sending the first message. In the future, OI 38 | Calendar will lookup the preferred communication method of each 39 | user. 40 |
41 | Guests who never used OI Chat before will see the invitation when 42 | they log in the first time. Current users of OI Chat, will also 43 | receive notifications according to their settings for invitations in 44 | their matrix client. 45 |
46 |
47 | 48 | 49 | The Files section list all files that OI Calendar has 50 | created on your storage. This is your data! OpenIntents or other 51 | partys do not have access or a copy of this data (only if you have 52 | shared an event with others). 53 |
54 | All files but the public event files are encrypted with your private 55 | key for OI Calendar. The only unencrypted files contains the list of 56 | public events in json and ical format. 57 |
58 |
59 |
60 |
61 |
62 | ) 63 | 64 | export default QuestionsWeb 65 | -------------------------------------------------------------------------------- /src/store/ActionTypes.js: -------------------------------------------------------------------------------- 1 | // Views 2 | export const SET_LAZY_VIEW = 'view.SET_LAZY_VIEW' 3 | 4 | // Auth 5 | export const AUTH_SIGN_IN = 'auth.SIGN_IN' 6 | export const AUTH_SIGN_OUT = 'auth.SIGN_OUT' 7 | export const AUTH_CONNECTING = 'auth.CONNECTING' 8 | export const AUTH_CONNECTED = 'auth.CONNECTED' 9 | export const AUTH_DISCONNECTED = 'auth.DISCONNECTED' 10 | 11 | // Chat 12 | export const INITIALIZE_CHAT = 'chat.INITIALIZE_CHAT' 13 | 14 | // Events 15 | export const EVENTS_ENABLED = 'events.EVENTS_ENABLED' 16 | export const SET_EVENTS = 'events.SET_EVENTS' 17 | export const SET_CURRENT_EVENT = 'events.SET_CURRENT_EVENT' 18 | export const UNSET_CURRENT_EVENT = 'events.UNSET_CURRENT_EVENT' 19 | 20 | // Contacts 21 | export const USER = 'contacts.USER' 22 | export const ADD_CONTACT = 'contacts.ADD_CONTACT' 23 | export const SET_CONTACTS = 'contacts.ALL' 24 | export const LOAD_GUEST_LIST = 'contacts.LOAD_GUEST_LIST' 25 | export const SET_CURRENT_GUESTS = 'contacts.SET_CURRENT_GUESTS' 26 | export const SEND_INVITES_REQUEST = 'contacts.SEND_INVITES_REQUEST' 27 | export const SET_INVITE_SEND_STATUS = 'contacts.SET_INVITE_SEND_STATUS' 28 | export const INVITES_SENT_OK = 'contacts.INVITES_SENT' 29 | export const INVITES_SENT_FAIL = 'contacts.INVITES_SENT_FAIL' 30 | export const UNSET_CURRENT_INVITES = 'contacts.UNSET_CURRENT_INVITES' 31 | 32 | // Calendar 33 | export const SET_CALENDARS = 'calendar.SET_CALENDARS' 34 | export const ADD_CALENDAR = 'calendar.ADD_CALENDAR' 35 | export const SET_PUBLIC_CALENDAR_EVENTS = 'calendar.SET_PUBLIC_CALENDAR_EVENTS' 36 | export const SET_LOADING_CALENDARS = 'calendar.SET_LOADING_CALENDARS' 37 | export const VERIFY_NEW_CALENDAR = 'calendar.VERIFY_NEW_CALENDAR' 38 | 39 | // Settings 40 | export const SHOW_SETTINGS = 'settings.SHOW_SETTINGS' 41 | export const HIDE_SETTINGS = 'settings.HIDE_SETTINGS' 42 | export const SHOW_MY_PUBLIC_CALENDAR = 'settings.SHOW_MY_PUBLIC_CALENDAR' 43 | export const SHOW_ALL_CALENDARS = 'settings.SHOW_ALL_CALENDARS' 44 | export const SHOW_SETTINGS_ADD_CALENDAR = 'settings.SHOW_SETTINGS_ADD_CALENDAR' 45 | export const SHOW_INSTRUCTIONS = 'settings.SHOW_INSTRUCTIONS' 46 | export const SHOW_FILES = 'settings.SHOW_FILES' 47 | export const SET_ALL_NOTIF_ENABLED = 'settings.SET_ALL_NOTIF_ENABLED' 48 | export const SET_RICH_NOTIF_ENABLED = 'settings.SET_RICH_NOTIF_ENABLED' 49 | export const SET_RICH_NOTIF_ERROR = 'settings.SET_RICH_NOTIF_ERROR' 50 | export const SET_RICH_NOTIF_EXCLUDE_GUESTS = 51 | 'settings.SET_RICH_NOTIF_EXCLUDE_GUESTS' 52 | export const SET_REMINDERS_INFO_REQUEST = 'settings.SET_REMINDERS_INFO_REQUEST' 53 | 54 | // Files 55 | export const SET_FILES = 'files.SET_FILES' 56 | 57 | // Chat 58 | export const SET_CHAT_STATUS = 'chat.SET_CHAT_STATUS' 59 | export const CREATE_CONFERENCING_ROOM = 'rooms.CREATE_CONFERENCING_ROOM' 60 | export const REMOVE_CONFERENCING_ROOM = 'rooms.REMOVE_CONFERENCING_ROOM' 61 | 62 | // App 63 | export const SET_ERROR = 'app.SET_ERROR' 64 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Container, Row, Col } from 'react-bootstrap' 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 4 | 5 | const Footer = () => { 6 | return ( 7 | 102 | ) 103 | } 104 | 105 | export default Footer 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OI Calendar 2 | 3 | Private, encrypted calendar in the cloud using Blockstack 4 | ![Logo](/public/android-chrome-192x192.png) 5 | 6 | ## Feature 7 | 8 | - create, read, update, delete events 9 | - publish events 10 | - send invitations 11 | - add events and calendars of other users or ics files 12 | - export/import in ical format 13 | 14 | ## Move from Google Calendar 15 | 16 | Google provides a private link that contains all your events. 17 | Unfortunately, Google does not let you easily use these events, you need either a [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) browser plugin. 18 | 19 | 1. Copy your private Google calendar url 20 | - Login to Google Calendar and goto settings: https://calendar.google.com/calendar/r/settings 21 | - Select your calendar on the left side 22 | ![Select](/resources/Screenshot%20from%202019-02-02%2002-10-33.png) 23 | ![Select3](/resources/Screenshot%20from%202019-02-02%2002-11-05.png) 24 | - Scroll to the bottom 25 | - Copy the private address of your calendar containing your email address and ends with `basic.ics` 26 | ![Select3](resources/Screenshot%20from%202019-02-02%2002-11-27.png) 27 | 1. Add to OI Calendar 28 | - Open OI Calendar https://cal.openintents.org/ 29 | - Enable your CORS browser plugin 30 | - Paste the private address into the `Paste url ...` field and press enter 31 | 1. Enjoy YOUR calendar! 32 | 33 | ## App Developers 34 | 35 | ### Add event via web intent 36 | 37 | Example: https://cal.openintents.org/?intent=addEvent&title=Blockstack%20Event&start=2018-12-31T23:00:00.000Z&end=2018-12-31T24:00:00.000Z 38 | 39 | The following parameters are supported: 40 | 41 | | name | description | 42 | | ------ | -------------------------- | 43 | | intent | "addEvent" | 44 | | title | the name of the event | 45 | | start | date string in zulu format | 46 | | end | date string in zulu format | 47 | | via | the organizer | 48 | 49 | ### Add calendar (read-only) via web intent 50 | 51 | Example: https://cal.openintents.org/?intent=addics&url=https://fosdem.org/2019/schedule/track/decentralized_internet_and_privacy.ics 52 | 53 | The following parameters are supported: 54 | 55 | | name | description | 56 | | ------ | ------------------------------------------------ | 57 | | intent | "addics" | 58 | | url | the location of the calendar file in iCal format | 59 | 60 | ## Development 61 | 62 | This application utilizes react-big-calendar and 63 | react-datetime components to add and remove events to a calendar. 64 | 65 | To clone and run this application locally, execute the following command: 66 | 67 | ``` 68 | git clone https://github.com/openintents/calendar-web.git 69 | cd oi-calendar 70 | npm install 71 | npm start 72 | ``` 73 | -------------------------------------------------------------------------------- /src/containers/PublicCalendar.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | // Components 4 | import PublicCalendar from '../components/PublicCalendar' 5 | 6 | import { setNewCurrentEvent } from '../store/event/eventAction' 7 | import { 8 | showAllCalendars, 9 | setError, 10 | showMyPublicCalendar, 11 | viewPublicCalendar, 12 | } from '../store/event/eventActionLazy' 13 | 14 | import { showSettingsAddCalendar } from '../store/event/calendarActionLazy' 15 | 16 | const mapStateToProps = state => { 17 | const { events, auth } = state 18 | const signedIn = !!auth.user 19 | 20 | const { 21 | currentEvent, 22 | currentEventType, 23 | myPublicCalendar, 24 | myPublicCalendarIcsUrl, 25 | publicCalendarEvents, 26 | publicCalendar, 27 | showInstructions, 28 | currentCalendarIndex, 29 | currentCalendarLength, 30 | currentError, 31 | showRemindersInfo, 32 | inviteStatus, 33 | } = events || {} 34 | 35 | let eventModal 36 | if (currentEvent && !inviteStatus) { 37 | const eventType = currentEventType || 'view' // "add", "edit" 38 | eventModal = { eventType, eventInfo: currentEvent } 39 | } 40 | 41 | const showGeneralInstructions = showInstructions 42 | ? showInstructions.general 43 | : false // preferences not yet loaded 44 | 45 | const showError = currentError && currentError.msg 46 | const error = currentError ? currentError.msg : null 47 | const showRemindersModal = showRemindersInfo 48 | const showSendInvitesModal = !!inviteStatus 49 | return { 50 | events, 51 | signedIn, 52 | eventModal, 53 | currentEvent, 54 | currentEventType, 55 | myPublicCalendar, 56 | myPublicCalendarIcsUrl, 57 | publicCalendarEvents, 58 | publicCalendar, 59 | showGeneralInstructions, 60 | currentCalendarIndex, 61 | currentCalendarLength, 62 | showError, 63 | error, 64 | showSendInvitesModal, 65 | showRemindersModal, 66 | } 67 | } 68 | 69 | const mapDispatchToProps = dispatch => { 70 | return { 71 | showAllCalendars: () => { 72 | dispatch(showAllCalendars()) 73 | }, 74 | showMyPublicCalendar: name => { 75 | dispatch(showMyPublicCalendar(name)) 76 | }, 77 | viewPublicCalendar: name => { 78 | dispatch(viewPublicCalendar(name)) 79 | }, 80 | showSettingsAddCalendar: url => { 81 | dispatch(showSettingsAddCalendar(url)) 82 | }, 83 | pickEventModal: eventModal => { 84 | console.log('[pickEventModal]', eventModal) 85 | const { 86 | eventType: currentEventType, 87 | eventInfo: currentEvent, 88 | } = eventModal 89 | dispatch(setNewCurrentEvent(currentEvent, currentEventType)) 90 | }, 91 | markErrorAsRead: () => { 92 | dispatch(setError()) 93 | }, 94 | } 95 | } 96 | 97 | const PublicCalendarContainer = connect( 98 | mapStateToProps, 99 | mapDispatchToProps 100 | )(PublicCalendar) 101 | 102 | export default PublicCalendarContainer 103 | -------------------------------------------------------------------------------- /src/containers/EventDetails.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import moment from 'moment' 3 | 4 | // Components 5 | import EventDetails from '../components/EventDetails' 6 | 7 | // Actions 8 | import { setCurrentEvent, unsetCurrentEvent } from '../store/event/eventAction' 9 | import { 10 | addEvent, 11 | deleteEvent, 12 | updateEvent, 13 | createConferencingRoom, 14 | removeConferencingRoom, 15 | setRemindersInfoRequest, 16 | } from '../store/event/eventActionLazy' 17 | import { 18 | setInviteStatus, 19 | unsetCurrentInvites, 20 | } from '../store/event/contactActionLazy' 21 | 22 | const eventDefaults = { 23 | start: moment(), 24 | end: moment(), 25 | allDay: false, 26 | public: false, 27 | hexColor: '#265985', 28 | reminderTime: 10, 29 | reminderTimeUnit: 'minutes', 30 | reminderEnabled: true, 31 | } 32 | 33 | const mapStateToProps = state => { 34 | console.log('[ConnectedEventDetails]', state) 35 | const { currentEvent, currentEventType, calendars } = state.events 36 | const inviteError = state.events.inviteError 37 | const inviteSuccess = state.events.inviteSuccess 38 | const inviteStatus = state.events.inviteStatus 39 | const addingConferencing = state.events.addingConferencing 40 | const removingConferencing = state.events.removingConferencing 41 | const allNotifEnabled = state.events.allNotifEnabled 42 | const richNotifEnabled = state.events.richNotifEnabled 43 | const richNofifExclude = state.events.richNofifExclude 44 | 45 | return { 46 | inviteStatus, 47 | inviteError, 48 | inviteSuccess, 49 | currentEvent: Object.assign({}, eventDefaults, currentEvent), 50 | editMode: currentEventType, 51 | addingConferencing, 52 | removingConferencing, 53 | allNotifEnabled, 54 | richNotifEnabled, 55 | richNofifExclude, 56 | calendars, 57 | } 58 | } 59 | 60 | const mapDispatchToProps = (dispatch, redux) => { 61 | return { 62 | unsetCurrentEvent: () => { 63 | dispatch(unsetCurrentEvent()) 64 | }, 65 | popSendInvitesModal: eventDetails => { 66 | dispatch(setCurrentEvent(eventDetails)) 67 | dispatch(setInviteStatus('prepare')) 68 | }, 69 | showRemindersModal: eventDetails => { 70 | dispatch(setCurrentEvent(eventDetails)) 71 | dispatch(setRemindersInfoRequest()) 72 | }, 73 | updateCurrentEvent: eventDetails => { 74 | dispatch(setCurrentEvent(eventDetails)) 75 | }, 76 | unsetInviteError: () => { 77 | dispatch(unsetCurrentInvites()) 78 | }, 79 | deleteEvent: obj => dispatch(deleteEvent(obj)), 80 | addEvent: obj => { 81 | dispatch(addEvent(obj)) 82 | }, 83 | updateEvent: obj => dispatch(updateEvent(obj)), 84 | createConferencingRoom: (eventDetails, guests) => { 85 | dispatch(createConferencingRoom(eventDetails, guests)) 86 | }, 87 | removeConferencingRoom: obj => dispatch(removeConferencingRoom(obj)), 88 | } 89 | } 90 | 91 | const EventDetailsContainer = connect( 92 | mapStateToProps, 93 | mapDispatchToProps 94 | )(EventDetails) 95 | 96 | export default EventDetailsContainer 97 | -------------------------------------------------------------------------------- /src/components/Export/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo, Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Button, Card } from 'react-bootstrap' 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 5 | 6 | // File Component 7 | import File from './File' 8 | 9 | class Files extends Component { 10 | componentDidMount() { 11 | const { files, refreshFiles } = this.props 12 | if (!files.calendars) { 13 | refreshFiles() 14 | } 15 | } 16 | 17 | render() { 18 | const { files, refreshFiles } = this.props 19 | console.log('files', files) 20 | 21 | if (!files.calendars) { 22 | return ( 23 | 24 | Loading 25 | 26 | ) 27 | } 28 | 29 | return ( 30 | 31 | 32 | 38 | 45 | 46 | 47 |
48 | 51 |
52 | 53 | 54 | {files.calendarListFile && ( 55 | 56 | )} 57 | 58 | {files.contactFileList && ( 59 | 60 | )} 61 | 62 | {Object.keys(files.calendars).map(k => { 63 | console.log(files.calendars[k]) 64 | return ( 65 | 71 | ) 72 | })} 73 | 74 | {files.sharedEvents && files.sharedEvents.length > 0 && ( 75 |
76 | 77 | {files.sharedEvents.map((v, k) => { 78 | return 79 | })} 80 |
81 | )} 82 | 83 | {files.msgs && files.msgs.length > 0 && ( 84 |
85 | 86 | {files.msgs.map((v, k) => { 87 | return 88 | })} 89 |
90 | )} 91 | 92 | {files.others && files.others.length > 0 && ( 93 |
94 | 95 | {files.others.map((v, k) => { 96 | return 97 | })} 98 |
99 | )} 100 |
101 |
102 | ) 103 | } 104 | } 105 | 106 | Files.propTypes = { 107 | files: PropTypes.any, 108 | refreshFiles: PropTypes.func, 109 | } 110 | 111 | export default memo(Files) 112 | -------------------------------------------------------------------------------- /src/containers/Calendar.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | // Components 4 | import OICalendar from '../components/Calendar' 5 | 6 | import { setNewCurrentEvent } from '../store/event/eventAction' 7 | import { 8 | showAllCalendars, 9 | hideInstructions, 10 | setError, 11 | showMyPublicCalendar, 12 | viewPublicCalendar, 13 | } from '../store/event/eventActionLazy' 14 | 15 | import { showSettingsAddCalendar } from '../store/event/calendarActionLazy' 16 | import { push } from 'connected-react-router' 17 | 18 | const mapStateToProps = state => { 19 | const { events, auth } = state 20 | const signedIn = !!auth.user 21 | 22 | const { 23 | currentEvent, 24 | currentEventType, 25 | myPublicCalendar, 26 | myPublicCalendarIcsUrl, 27 | publicCalendarEvents, 28 | publicCalendar, 29 | showInstructions, 30 | currentCalendarIndex, 31 | currentCalendarLength, 32 | currentError, 33 | showRemindersInfo, 34 | allNotifEnabled, 35 | inviteStatus, 36 | } = events || {} 37 | 38 | let eventModal 39 | if (currentEvent && !inviteStatus) { 40 | const eventType = currentEventType || 'view' // "add", "edit" 41 | eventModal = { eventType, eventInfo: currentEvent } 42 | } 43 | 44 | const showGeneralInstructions = showInstructions 45 | ? showInstructions.general 46 | : false // preferences not yet loaded 47 | 48 | const showError = currentError && currentError.msg 49 | const error = currentError ? currentError.msg : null 50 | const showRemindersModal = showRemindersInfo && allNotifEnabled 51 | const showSendInvitesModal = !!inviteStatus 52 | return { 53 | events, 54 | signedIn, 55 | eventModal, 56 | currentEvent, 57 | currentEventType, 58 | myPublicCalendar, 59 | myPublicCalendarIcsUrl, 60 | publicCalendarEvents, 61 | publicCalendar, 62 | showGeneralInstructions, 63 | currentCalendarIndex, 64 | currentCalendarLength, 65 | showError, 66 | error, 67 | showSendInvitesModal, 68 | showRemindersModal, 69 | } 70 | } 71 | 72 | const mapDispatchToProps = dispatch => { 73 | return { 74 | showAllCalendars: () => { 75 | dispatch(showAllCalendars()) 76 | }, 77 | hideInstructions: () => { 78 | dispatch(hideInstructions()) 79 | }, 80 | showMyPublicCalendar: name => { 81 | dispatch(showMyPublicCalendar(name)) 82 | }, 83 | viewPublicCalendar: name => { 84 | dispatch(viewPublicCalendar(name)) 85 | }, 86 | showSettingsAddCalendar: url => { 87 | dispatch(push('settings')) 88 | dispatch(showSettingsAddCalendar(url)) 89 | }, 90 | pickEventModal: eventModal => { 91 | console.log('[pickEventModal]', eventModal) 92 | const { 93 | eventType: currentEventType, 94 | eventInfo: currentEvent, 95 | } = eventModal 96 | dispatch(setNewCurrentEvent(currentEvent, currentEventType)) 97 | }, 98 | markErrorAsRead: () => { 99 | dispatch(setError()) 100 | }, 101 | } 102 | } 103 | 104 | const CalendarContainer = connect( 105 | mapStateToProps, 106 | mapDispatchToProps 107 | )(OICalendar) 108 | 109 | export default CalendarContainer 110 | -------------------------------------------------------------------------------- /src/store/event/calendarActionLazy.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_CALENDARS, 3 | SHOW_SETTINGS, 4 | HIDE_SETTINGS, 5 | SHOW_SETTINGS_ADD_CALENDAR, 6 | } from '../ActionTypes' 7 | import { defaultCalendars } from '../../core/eventDefaults' 8 | import { guaranteeHexColor } from '../../core/eventFN' 9 | 10 | // ################ 11 | // When initializing app 12 | // ################ 13 | 14 | function resetCalendars(calendars) { 15 | return (dispatch, getState) => { 16 | const { userOwnedStorage } = getState().auth 17 | userOwnedStorage.publishCalendars(calendars) 18 | dispatch({ type: SET_CALENDARS, payload: calendars }) 19 | } 20 | } 21 | 22 | export function initializeCalendars() { 23 | return (dispatch, getState) => { 24 | const { userOwnedStorage } = getState().auth 25 | return userOwnedStorage.fetchCalendars().then(calendars => { 26 | if (!calendars) { 27 | calendars = defaultCalendars 28 | } else if (calendars.length === 0) { 29 | calendars.push(defaultCalendars[0]) 30 | } else { 31 | calendars = calendars.filter(d => d) 32 | } 33 | // publish now as other devices fetch before storing 34 | dispatch(resetCalendars(calendars)) 35 | return calendars 36 | }) 37 | } 38 | } 39 | 40 | // ################ 41 | // In Settings 42 | // ################ 43 | 44 | export function showSettings() { 45 | return { 46 | type: SHOW_SETTINGS, 47 | } 48 | } 49 | 50 | export function hideSettings() { 51 | console.log('hideSettings') 52 | return { 53 | type: HIDE_SETTINGS, 54 | } 55 | } 56 | 57 | export function showSettingsAddCalendar(url) { 58 | return { type: SHOW_SETTINGS_ADD_CALENDAR, payload: { url } } 59 | } 60 | 61 | export function addCalendar(calendar) { 62 | console.log('addCalendar => ', calendar) 63 | calendar.hexColor = guaranteeHexColor(calendar.hexColor) 64 | return (dispatch, getState) => { 65 | const { userOwnedStorage } = getState().auth 66 | userOwnedStorage.fetchCalendars().then(calendars => { 67 | // TODO check for duplicates 68 | // :TODO: We need to actually import calendars, add uid etc. 69 | dispatch(resetCalendars([...calendars, calendar])) 70 | }) 71 | } 72 | } 73 | 74 | export function deleteCalendars(deleteList) { 75 | return (dispatch, getState) => { 76 | const { userOwnedStorage } = getState().auth 77 | userOwnedStorage.fetchCalendars().then(calendars => { 78 | const uids = deleteList.map(d => d.uid) 79 | const newCalendars = calendars.filter(d => { 80 | return !uids.includes(d.uid) 81 | }) 82 | dispatch(resetCalendars(newCalendars)) 83 | }) 84 | } 85 | } 86 | 87 | export function setCalendarData(calendar, newData) { 88 | return async (dispatch, getState) => { 89 | const { userOwnedStorage } = getState().auth 90 | userOwnedStorage.fetchCalendars().then(calendars => { 91 | const newCalendars = calendars.map(d => { 92 | if (d.uid === calendar.uid) { 93 | d = Object.assign({}, d, newData) 94 | } 95 | return d 96 | }) 97 | console.log(newCalendars) 98 | dispatch(resetCalendars(newCalendars)) 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/components/App/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | margin-right: 10px; 7 | height: 40px; 8 | width: 40px; 9 | } 10 | 11 | .App-header { 12 | padding: 1px; 13 | color: white; 14 | } 15 | 16 | .App-menu { 17 | margin: 8px 10px 0 0; 18 | } 19 | 20 | .body-container { 21 | margin: 70px; 22 | } 23 | 24 | .rbc-toolbar-label { 25 | color: black; 26 | font-weight: bolder; 27 | font-size: 20px; 28 | font-family: sans-serif; 29 | } 30 | 31 | h1, 32 | .instruction { 33 | font-family: fantasy; 34 | } 35 | 36 | .modal { 37 | display: block; 38 | } 39 | 40 | .cursor-pointer { 41 | cursor: pointer; 42 | } 43 | 44 | .rbc-toolbar span:first-child button:first-child { 45 | background-color: #558ed5; 46 | color: white; 47 | font-weight: bold; 48 | } 49 | 50 | .rbc-toolbar span:first-child button:nth-child(2) { 51 | text-indent: -9999px; 52 | line-height: 0; 53 | margin-left: 5px; 54 | } 55 | 56 | .rbc-toolbar span:first-child button:nth-child(2)::after { 57 | content: '◀'; 58 | text-indent: 0; 59 | display: block; 60 | line-height: initial; 61 | font-size: 13px; 62 | } 63 | 64 | .rbc-toolbar span:first-child button:nth-child(3) { 65 | text-indent: -9999px; 66 | line-height: 0; 67 | } 68 | 69 | .rbc-toolbar span:first-child button:nth-child(3)::after { 70 | content: '▶'; 71 | text-indent: 0; 72 | display: block; 73 | line-height: initial; 74 | font-size: 13px; 75 | } 76 | 77 | .rbc-active { 78 | background-color: #558ed5 !important; 79 | color: white !important; 80 | } 81 | 82 | .rbc-header { 83 | background-color: black !important; 84 | color: white !important; 85 | padding: 5px; 86 | border-radius: 5px; 87 | } 88 | 89 | .rbc-time-view .rbc-row { 90 | min-height: 28px; 91 | } 92 | 93 | .rbc-toolbar { 94 | flex-wrap: wrap-reverse; 95 | } 96 | 97 | .rbc-day-bg + .rbc-day-bg, 98 | .rbc-month-row + .rbc-month-row { 99 | border: none !important; 100 | } 101 | 102 | .rbc-off-range-bg { 103 | border-radius: 10px; 104 | margin-left: 1px; 105 | } 106 | 107 | .rbc-month-view { 108 | border-radius: 10px; 109 | } 110 | 111 | .rbc-btn-group { 112 | white-space: pre-wrap !important; 113 | margin-bottom: 10px; 114 | } 115 | 116 | .authUserProfile-Root { 117 | margin: 4px; 118 | text-align: center; 119 | } 120 | 121 | .authUserProfile-Avatar { 122 | border-radius: 50%; 123 | height: 20px; 124 | width: 20px; 125 | } 126 | 127 | .modal-container { 128 | position: relative; 129 | } 130 | .modal-container .modal, 131 | .modal-container .modal-backdrop { 132 | position: absolute; 133 | } 134 | 135 | .nav-pills .nav-link { 136 | padding: 6px 20px 6px 20px !important; 137 | } 138 | 139 | .nav-pills .nav-link.active { 140 | background-color: #337ab7; 141 | color: #ffffff !important; 142 | } 143 | 144 | .nav-pills .nav-link:hover:not(.active) { 145 | background-color: #e0e0e0; 146 | } 147 | 148 | .nav-justified { 149 | white-space: nowrap; 150 | } 151 | 152 | @media screen and (min-width: 1080px) { 153 | .body-container { 154 | width: 1050px; 155 | margin-left: auto; 156 | margin-right: auto; 157 | } 158 | } 159 | 160 | @media screen and (max-width: 500px) { 161 | .body-container { 162 | margin: 2%; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/components/Settings/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Button } from 'react-bootstrap' 3 | 4 | import Calendars from './Calendars' 5 | import Contacts from './Contacts' 6 | import Notifications from './Notifications' 7 | 8 | class SettingsPage extends Component { 9 | render() { 10 | const { 11 | CalendarsContent, 12 | ContactsContent, 13 | NotificationsContent, 14 | handleHide, 15 | } = this.props 16 | return ( 17 |
21 |

Settings

22 | 23 | {CalendarsContent} 24 | {ContactsContent} 25 | {NotificationsContent} 26 | 27 |
28 | ) 29 | } 30 | } 31 | 32 | export default class Settings extends Component { 33 | componentWillMount() { 34 | this.props.showSettings() 35 | } 36 | render() { 37 | const { 38 | // show, 39 | handleHide, 40 | calendars, 41 | addCalendarUrl, 42 | contacts, 43 | addCalendar, 44 | deleteCalendars, 45 | setCalendarData, 46 | lookupContacts, 47 | addContact, 48 | deleteContacts, 49 | followContact, 50 | unfollowContact, 51 | user, 52 | verifyNewCalendar, 53 | verifiedNewCalendarData, 54 | allNotifEnabled, 55 | richNotifEnabled, 56 | richNofifExclude, 57 | richNotifError, 58 | chatStatus, 59 | enableAllNotif, 60 | disableAllNotif, 61 | enableRichNotif, 62 | disableRichNotif, 63 | saveRichNotifExcludeGuests, 64 | } = this.props 65 | const CalendarsContent = ( 66 |
67 | 77 |
78 | ) 79 | 80 | const ContactsContent = ( 81 |
82 | 92 |
93 | ) 94 | 95 | const NotificationsContent = ( 96 |
97 | 109 |
110 | ) 111 | 112 | return ( 113 | handleHide(this.props.history)} 118 | /> 119 | ) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/components/AppMenu/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Nav } from 'react-bootstrap' 3 | import { NavLink } from 'react-router-dom' 4 | import { PropTypes } from 'prop-types' 5 | 6 | export default class AppMenu extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = { 10 | activeKey: props.page, 11 | } 12 | 13 | this.bound = ['onSelect'].reduce((acc, d) => { 14 | acc[d] = this[d].bind(this) 15 | return acc 16 | }, {}) 17 | } 18 | 19 | componentWillReceiveProps(nextProps) { 20 | this.setState({ activeKey: nextProps.page }) 21 | } 22 | 23 | onSelect(eventKey) { 24 | switch (eventKey) { 25 | case 'settings': 26 | this.setState({ activeKey: 'settings' }) 27 | break 28 | case 'public': 29 | this.setState({ activeKey: 'public' }) 30 | break 31 | case 'all': 32 | this.setState({ activeKey: 'all' }) 33 | break 34 | case 'files': 35 | this.setState({ activeKey: 'files' }) 36 | break 37 | case 'rate': 38 | break 39 | case 'help': 40 | this.setState({ activeKey: 'help' }) 41 | break 42 | default: 43 | console.warn('invalid menu item ', eventKey) 44 | break 45 | } 46 | } 47 | 48 | isRoot(match, location) { 49 | if (!match) { 50 | return false 51 | } 52 | return match.isExact 53 | } 54 | 55 | render() { 56 | const { onSelect } = this.bound 57 | const { username, signedIn } = this.props 58 | const hasPublicCalendar = !!username 59 | 60 | return ( 61 | signedIn && ( 62 |
63 | 125 |
126 | ) 127 | ) 128 | } 129 | } 130 | 131 | AppMenu.propTypes = { 132 | username: PropTypes.string, 133 | signedIn: PropTypes.bool.isRequired, 134 | } 135 | -------------------------------------------------------------------------------- /src/components/Calendar/SendInvitesModal.js: -------------------------------------------------------------------------------- 1 | import { Button, Modal, ProgressBar } from 'react-bootstrap' 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | import { renderMatrixError } from '../EventDetails' 5 | import GuestList from '../EventGuestList' 6 | 7 | class SendInvitesModal extends React.Component { 8 | constructor(props) { 9 | super(props) 10 | console.log('SendInvitesModal') 11 | this.state = { profiles: undefined } 12 | } 13 | 14 | componentDidMount() { 15 | var { guests, profiles } = this.props 16 | console.log('didMount', { guests, profiles }) 17 | if (!profiles) { 18 | if (typeof guests !== 'string') { 19 | guests = '' 20 | } 21 | const guestList = guests.toLowerCase().split(/[,\s]+/g) 22 | console.log('dispatch load guest list', guestList) 23 | this.props.loadGuestList(guestList) 24 | } 25 | } 26 | 27 | componentWillReceiveProps(nextProps) { 28 | var { guests, profiles, loadGuestList } = nextProps 29 | console.log('componentWillReceiveProps', { guests, profiles }) 30 | 31 | if (!profiles) { 32 | if (typeof guests !== 'string') { 33 | guests = '' 34 | } 35 | const guestList = guests.toLowerCase().split(/[,\s]+/g) 36 | console.log('dispatch load guest list', guestList) 37 | loadGuestList(guestList) 38 | } 39 | } 40 | 41 | render() { 42 | const { 43 | title, 44 | handleInvitesHide, 45 | sending, 46 | inviteError, 47 | sendInvites, 48 | profiles, 49 | currentEvent, 50 | currentEventType, 51 | } = this.props 52 | console.log('profiles', profiles) 53 | let inviteErrorMsg = [] 54 | if (inviteError) { 55 | inviteErrorMsg = renderMatrixError( 56 | 'Sending invites not possible.', 57 | inviteError 58 | ) 59 | } 60 | return ( 61 | handleInvitesHide(undefined, currentEvent, currentEvent)} 64 | animation={false} 65 | > 66 | 67 | {title} 68 | 69 | 70 | Send invites according to their Blockstack settings: 71 | {profiles && } 72 | {sending && !inviteError && } 73 | {inviteError && inviteErrorMsg} 74 | 75 | 83 | 90 | 91 | 92 | 93 | ) 94 | } 95 | } 96 | 97 | SendInvitesModal.propTypes = { 98 | guests: PropTypes.string, 99 | profiles: PropTypes.object, 100 | handleInvitesHide: PropTypes.func, 101 | inviteError: PropTypes.instanceOf(Error), 102 | loadGuestList: PropTypes.func, 103 | sending: PropTypes.bool, 104 | sendInvites: PropTypes.func, 105 | title: PropTypes.string, 106 | } 107 | 108 | export default SendInvitesModal 109 | -------------------------------------------------------------------------------- /src/components/UserProfile/SignInButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const imageDefaultStyle = { 5 | height: 20, 6 | width: 20, 7 | } 8 | const textDefaultStyle = { 9 | fontSize: 12, 10 | fontFamily: '"Source Code Pro", monospace', 11 | paddingLeft: 5, 12 | verticalAlign: 'top', 13 | top: 3, 14 | position: 'relative', 15 | color: '#fff', 16 | } 17 | 18 | const BlockstackSignInButton = props => { 19 | const { 20 | renderNothing, 21 | signOutBtnText, 22 | signInBtnText, 23 | includeBlockstackLogo, 24 | signInStyle, 25 | signOutStyle, 26 | isSignedIn, 27 | imageStyle, 28 | textStyle, 29 | style, 30 | img, 31 | } = props 32 | 33 | let { defaultStyle } = props 34 | 35 | if (renderNothing) { 36 | return null 37 | } 38 | 39 | // If style isn't set then render default styling. 40 | if (!defaultStyle) { 41 | defaultStyle = { 42 | display: 'inline-block', 43 | backgroundColor: '#270F34', 44 | border: '1px solid #270F34', 45 | paddingTop: 5, 46 | paddingBottom: 5, 47 | paddingLeft: 15, 48 | paddingRight: 15, 49 | borderRadius: 2, 50 | } 51 | } 52 | 53 | const imageInlineStyle = Object.assign({}, imageDefaultStyle, imageStyle) 54 | let altImg = null 55 | if (img) { 56 | altImg = img 57 | } 58 | 59 | const image = includeBlockstackLogo ? ( 60 | Blockstack Logo 61 | ) : ( 62 | altImg 63 | ) 64 | 65 | const textInlineStyle = Object.assign({}, textDefaultStyle, textStyle) 66 | 67 | const signOutInlineStyle = Object.assign( 68 | {}, 69 | defaultStyle, 70 | style, 71 | signOutStyle 72 | ) 73 | if (isSignedIn) { 74 | return ( 75 | 79 | ) 80 | } 81 | 82 | const signInInlineStyle = Object.assign({}, defaultStyle, style, signInStyle) 83 | return ( 84 | 88 | ) 89 | } 90 | 91 | BlockstackSignInButton.propTypes = { 92 | renderNothing: PropTypes.bool, 93 | signOutBtnText: PropTypes.string, 94 | signInBtnText: PropTypes.string, 95 | includeBlockstackLogo: PropTypes.bool, 96 | // eslint-disable-next-line react/forbid-prop-types 97 | style: PropTypes.object, 98 | // eslint-disable-next-line react/forbid-prop-types 99 | defaultStyle: PropTypes.object, 100 | // eslint-disable-next-line react/forbid-prop-types 101 | imageStyle: PropTypes.object, 102 | // eslint-disable-next-line react/forbid-prop-types 103 | textStyle: PropTypes.object, 104 | // eslint-disable-next-line react/forbid-prop-types 105 | signInStyle: PropTypes.object, 106 | // eslint-disable-next-line react/forbid-prop-types 107 | signOutStyle: PropTypes.object, 108 | isSignedIn: PropTypes.bool.isRequired, 109 | signOut: PropTypes.func.isRequired, 110 | signIn: PropTypes.func.isRequired, 111 | img: PropTypes.object, 112 | } 113 | 114 | BlockstackSignInButton.defaultProps = { 115 | renderNothing: false, 116 | signOutBtnText: 'Logout', 117 | signInBtnText: 'Use your own calendar', 118 | includeBlockstackLogo: true, 119 | defaultStyle: null, 120 | style: { padding: '8.5px' }, 121 | signInStyle: {}, 122 | signOutStyle: {}, 123 | imageStyle: {}, 124 | textStyle: {}, 125 | img: null, 126 | } 127 | 128 | export default BlockstackSignInButton 129 | -------------------------------------------------------------------------------- /src/components/Settings/Notifications.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Form, Card, ProgressBar } from 'react-bootstrap' 3 | import { renderMatrixError } from '../EventDetails' 4 | const guestsStringToArray = function(guestsString) { 5 | if (!guestsString || !guestsString.length) { 6 | return [] 7 | } 8 | const guests = guestsString.split(/[,\s]+/g) 9 | return guests.filter(g => g.length > 0).map(g => g.toLowerCase()) 10 | } 11 | 12 | export default class Notifications extends Component { 13 | constructor(props) { 14 | super(props) 15 | 16 | const { allNotifEnabled, richNotifEnabled, richNofifExclude } = this.props 17 | 18 | this.state = { 19 | richNofifExclude: richNofifExclude ? richNofifExclude.join(',') : '', 20 | richNotifEnabled, 21 | allNotifEnabled, 22 | } 23 | } 24 | handleAllNotificationsChange = event => { 25 | const { enableAllNotif, disableAllNotif } = this.props 26 | const checked = event.target.checked 27 | this.setState({ allNotifEnabled: checked }) 28 | console.log({ checked }) 29 | if (checked) { 30 | enableAllNotif() 31 | } else { 32 | disableAllNotif() 33 | } 34 | } 35 | 36 | handleEnrichedNotificationsChange = event => { 37 | const { enableRichNotif, disableRichNotif } = this.props 38 | if (event.target.checked) { 39 | enableRichNotif() 40 | } else { 41 | disableRichNotif() 42 | } 43 | } 44 | 45 | handleExcludedGuestsChange = event => { 46 | const { saveRichNotifExcludeGuests } = this.props 47 | saveRichNotifExcludeGuests(guestsStringToArray(event.target.value)) 48 | } 49 | 50 | renderBody = () => { 51 | const { allNotifEnabled, richNotifEnabled, richNofifExclude } = this.state 52 | const { richNotifError, chatStatus } = this.props 53 | console.log({ allNotifEnabled }) 54 | const checkingChatStatus = chatStatus === 'checking' 55 | const richNotifErrorMsg = richNotifError 56 | ? renderMatrixError('Rich notifications not allowed.', richNotifError) 57 | : [] 58 | return ( 59 |
60 | 61 | 67 | 68 | 69 | 75 | 76 | 77 | Excluded guests 78 | 84 | 85 | {checkingChatStatus && ( 86 | <> 87 | Connecting to OI Chat .. 88 | 89 | 90 | )} 91 | {richNotifError && <>{richNotifErrorMsg}} 92 | 93 | ) 94 | } 95 | 96 | render() { 97 | return ( 98 | 99 | Notifications 100 | {this.renderBody()} 101 | 102 | ) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/store/event/contactActionLazy.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_CONTACTS, 3 | INVITES_SENT_OK, 4 | INVITES_SENT_FAIL, 5 | UNSET_CURRENT_INVITES, 6 | SET_INVITE_SEND_STATUS, 7 | } from '../ActionTypes' 8 | 9 | import { sendInvitesToGuests, loadGuestProfiles } from '../../core/event' 10 | import { setCurrentGuests } from './eventActionLazy' 11 | 12 | function resetContacts(contacts) { 13 | return (dispatch, getState) => { 14 | const { userOwnedStorage } = getState().auth 15 | userOwnedStorage.publishContacts(contacts) 16 | dispatch({ type: SET_CONTACTS, payload: contacts }) 17 | } 18 | } 19 | 20 | // ################ 21 | // When initializing app 22 | // ################ 23 | 24 | export function initializeContactData() { 25 | return async (dispatch, getState) => { 26 | const { userOwnedStorage } = getState().auth 27 | userOwnedStorage.fetchContactData().then(contacts => { 28 | dispatch(resetContacts(contacts)) 29 | }) 30 | } 31 | } 32 | 33 | // ################ 34 | // In Settings 35 | // ################ 36 | 37 | export function lookupContacts() { 38 | return (dispatch, getState) => { 39 | return Promise.reject(new Error('not yet implemented')) 40 | } 41 | } 42 | 43 | export function addContact(username, contact) { 44 | return (dispatch, getState) => { 45 | const { userOwnedStorage } = getState().auth 46 | 47 | userOwnedStorage.fetchContactData().then(contacts => { 48 | contacts[username] = { ...contacts[username], ...contact } 49 | dispatch(resetContacts(contacts)) 50 | }) 51 | } 52 | } 53 | 54 | export function deleteContacts(deleteList) { 55 | return (dispatch, getState) => { 56 | const { userOwnedStorage } = getState().auth 57 | userOwnedStorage.fetchContactData().then(contacts => { 58 | for (var i in deleteList) { 59 | delete contacts[deleteList[i].username] 60 | } 61 | dispatch(resetContacts(contacts)) 62 | }) 63 | } 64 | } 65 | 66 | // ######################### 67 | // INVITES 68 | // ######################### 69 | 70 | function invitesSentSuccess() { 71 | return { 72 | type: INVITES_SENT_OK, 73 | } 74 | } 75 | 76 | function invitesSentFailure(error, eventType, eventInfo) { 77 | return { 78 | type: INVITES_SENT_FAIL, 79 | payload: { error, eventType, eventInfo }, 80 | } 81 | } 82 | export function unsetCurrentInvites() { 83 | return { type: UNSET_CURRENT_INVITES } 84 | } 85 | export function setInviteStatus(status) { 86 | return { type: SET_INVITE_SEND_STATUS, payload: { status } } 87 | } 88 | export function sendInvites(eventInfo, guests) { 89 | return async (dispatch, getState) => { 90 | const state = getState() 91 | sendInvitesToGuests( 92 | state.events.contacts, 93 | state.auth.user, 94 | eventInfo, 95 | guests, 96 | state.events.userSessionChat, 97 | state.auth.userOwnedStorage 98 | ).then( 99 | () => { 100 | dispatch(invitesSentSuccess()) 101 | return Promise.resolve(state.events.allEvents) 102 | }, 103 | error => { 104 | dispatch(invitesSentFailure(error)) 105 | return Promise.reject(error) 106 | } 107 | ) 108 | } 109 | } 110 | 111 | // ######################### 112 | // GUESTS 113 | // ######################### 114 | 115 | export function loadGuestList(guests) { 116 | return async (dispatch, getState) => { 117 | const contacts = getState().events.contacts 118 | console.log('loadGuestList', guests, contacts) 119 | loadGuestProfiles(guests, contacts).then( 120 | ({ profiles, contacts }) => { 121 | dispatch(setCurrentGuests(profiles)) 122 | }, 123 | error => { 124 | console.log('load guest list failed', error) 125 | } 126 | ) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://cal.openintents.org", 3 | "name": "oi-calendar", 4 | "author": "friedger@gmail.com", 5 | "version": "0.11.0", 6 | "dependencies": { 7 | "@fortawesome/fontawesome-svg-core": "^1.2.35", 8 | "@fortawesome/free-solid-svg-icons": "^5.15.3", 9 | "@fortawesome/react-fontawesome": "^0.1.14", 10 | "@stacks/blockchain-api-client": "^0.59.0", 11 | "@stacks/connect": "^5.0.5", 12 | "@stacks/profile": "^1.5.0-alpha.0", 13 | "@stacks/storage": "^1.5.0-alpha.0", 14 | "@stacks/transactions": "^1.4.1", 15 | "@types/react": "^17.0.5", 16 | "connected-react-router": "^6.9.1", 17 | "history": "^5.0.0", 18 | "ical.js": "^1.4.0", 19 | "ics": "^2.27.0", 20 | "matrix-js-sdk": "^10.1.0", 21 | "moment": "^2.29.1", 22 | "npm": "^7.12.1", 23 | "query-string": "^7.0.0", 24 | "react": "^17.0.2", 25 | "react-big-calendar": "^0.33.2", 26 | "react-bootstrap": "^1.6.0", 27 | "react-datetime": "^3.0.4", 28 | "react-dom": "^17.0.2", 29 | "react-jdenticon": "^0.0.9", 30 | "react-pdf": "^5.3.0", 31 | "react-redux": "^7.2.4", 32 | "react-router-dom": "^5.2.0", 33 | "react-scripts": "4.0.3", 34 | "redux": "^4.1.0", 35 | "redux-thunk": "^2.3.0", 36 | "typescript": "^4.2.4" 37 | }, 38 | "scripts": { 39 | "start": "react-scripts start", 40 | "build": "REACT_APP_VERSION=$(node -pe 'require(\"./package.json\").version') react-scripts build", 41 | "test": "react-scripts test --env=jsdom", 42 | "testflow": "mocha spec/*.spec.js --require babel-register --presets @babel/preset-stage-2", 43 | "eject": "react-scripts eject", 44 | "predeploy": "npm run build", 45 | "deploy": "gh-pages -d build", 46 | "lint": "eslint --ext js,jsx ./src ./spec", 47 | "lint:fix": "eslint --fix --ext js,jsx ./src ./spec", 48 | "prettier-js": "prettier --single-quote --write \"src/**/*.js\"", 49 | "prettier-css": "prettier --single-quote --parser css --write \"src/**/*.css\"" 50 | }, 51 | "devDependencies": { 52 | "@babel/cli": "7.2.3", 53 | "@babel/core": "7.3.3", 54 | "@babel/node": "7.2.2", 55 | "@babel/preset-env": "^7.3.1", 56 | "@babel/preset-stage-2": "7.0.0", 57 | "@babel/register": "7.0.0", 58 | "babel-plugin-transform-runtime": "^6.23.0", 59 | "babel-polyfill": "6.26.0", 60 | "babel-preset-es2015": "6.24.1", 61 | "babel-preset-stage-2": "^6.24.1", 62 | "eslint-config-prettier": "^4.0.0", 63 | "eslint-config-standard": "^12.0.0", 64 | "eslint-config-standard-react": "^7.0.2", 65 | "eslint-plugin-node": "^8.0.1", 66 | "eslint-plugin-prettier": "^3.0.1", 67 | "eslint-plugin-promise": "^4.0.1", 68 | "eslint-plugin-react": "^7.12.4", 69 | "eslint-plugin-standard": "^4.0.0", 70 | "gh-pages": "^2.0.1", 71 | "husky": "^1.3.1", 72 | "lint-staged": "^8.1.4", 73 | "mocha": "6.0.1", 74 | "prettier": "^1.16.4", 75 | "prop-types": "^15.7.2", 76 | "redux-logger": "^3.0.6" 77 | }, 78 | "browserslist": [ 79 | "> 0.5%", 80 | "last 2 versions", 81 | "Firefox ESR", 82 | "not dead" 83 | ], 84 | "eslintConfig": { 85 | "extends": "react-app" 86 | }, 87 | "husky": { 88 | "hooks": { 89 | "pre-commit": "lint-staged" 90 | } 91 | }, 92 | "lint-staged": { 93 | "src/**/*.js": [ 94 | "npm run prettier-js", 95 | "npm run lint:fix", 96 | "git add" 97 | ], 98 | "src/**/*.css": [ 99 | "npm run prettier-css", 100 | "git add" 101 | ] 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/containers/Settings.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { 3 | initializeEvents, 4 | showAllCalendars, 5 | verifyNewCalendar, 6 | clearVerifyCalendar, 7 | enableRichNotif, 8 | disableRichNotif, 9 | saveRichNotifExcludeGuests, 10 | updateAllNotifEnabled, 11 | } from '../store/event/eventActionLazy' 12 | 13 | import { 14 | addCalendar, 15 | deleteCalendars, 16 | setCalendarData, 17 | showSettings, 18 | } from '../store/event/calendarActionLazy' 19 | import { 20 | addContact, 21 | deleteContacts, 22 | lookupContacts, 23 | } from '../store/event/contactActionLazy' 24 | import { uuid } from '../core/eventFN' 25 | import Settings from '../components/Settings' 26 | 27 | export default connect( 28 | (state, redux) => { 29 | const show = state.events.showPage === 'settings' 30 | const addCalendarUrl = state.events.showSettingsAddCalendarUrl 31 | var contacts = state.events.contacts 32 | const user = state.auth.user 33 | const { 34 | calendars, 35 | verifiedNewCalendarData, 36 | allNotifEnabled, 37 | richNotifEnabled, 38 | richNofifExclude, 39 | richNotifError, 40 | chatStatus, 41 | } = state.events 42 | console.log(state.events) 43 | return { 44 | show, 45 | contacts, 46 | calendars, 47 | addCalendarUrl, 48 | user, 49 | verifiedNewCalendarData, 50 | allNotifEnabled, 51 | richNotifEnabled, 52 | richNofifExclude, 53 | richNotifError, 54 | chatStatus, 55 | } 56 | }, 57 | (dispatch, redux) => { 58 | return { 59 | showSettings: () => { 60 | dispatch(showSettings()) 61 | }, 62 | handleHide: history => { 63 | console.log('handle hide', history) 64 | dispatch(initializeEvents()) 65 | dispatch(showAllCalendars(history)) 66 | }, 67 | lookupContacts: contactQuery => { 68 | return dispatch(lookupContacts(contactQuery)) 69 | }, 70 | addContact: contactFormData => { 71 | const username = contactFormData.username 72 | const contact = { username } 73 | dispatch(addContact(username, contact)) 74 | }, 75 | deleteContacts: contacts => { 76 | dispatch(deleteContacts(contacts)) 77 | }, 78 | addCalendar: calendar => { 79 | dispatch(addCalendar(calendar)) 80 | dispatch(clearVerifyCalendar()) 81 | }, 82 | deleteCalendars: calendars => { 83 | dispatch(deleteCalendars(calendars)) 84 | }, 85 | setCalendarData: (calendar, data) => { 86 | dispatch(setCalendarData(calendar, data)) 87 | }, 88 | followContact: contact => { 89 | const calendar = { 90 | uid: uuid(), 91 | type: 'blockstack-user', 92 | name: 'public@' + contact.username, 93 | mode: 'read-only', 94 | data: { 95 | user: contact.username, 96 | src: 'public/AllEvents', 97 | }, 98 | } 99 | dispatch(addCalendar(calendar)) 100 | }, 101 | unfollowContact: contact => { 102 | const { calendars } = redux.store.getState().events 103 | const calendarToDelete = calendars.find( 104 | c => c.name === 'public@' + contact.username 105 | ) 106 | dispatch(deleteCalendars([calendarToDelete])) 107 | }, 108 | verifyNewCalendar: calendar => { 109 | dispatch(verifyNewCalendar(calendar)) 110 | }, 111 | enableAllNotif: () => { 112 | dispatch(updateAllNotifEnabled(true)) 113 | }, 114 | disableAllNotif: () => { 115 | dispatch(updateAllNotifEnabled(false)) 116 | }, 117 | enableRichNotif: () => { 118 | dispatch(enableRichNotif()) 119 | }, 120 | disableRichNotif: () => { 121 | dispatch(disableRichNotif()) 122 | }, 123 | saveRichNotifExcludeGuests: guests => { 124 | dispatch(saveRichNotifExcludeGuests(guests)) 125 | }, 126 | } 127 | } 128 | )(Settings) 129 | -------------------------------------------------------------------------------- /src/components/FAQ.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Card, Container, Row, Col } from 'react-bootstrap' 3 | 4 | export const FAQ = props => { 5 | return ( 6 | 7 |
{props.q}
8 |
{props.children}
9 | 10 | ) 11 | } 12 | const FAQs = () => ( 13 | 14 | 15 |

Frequently Asked Questions

16 |
17 | 18 | 19 | 20 | 21 | OI Calendar keeps your data private to you! We, OpenIntents, do not 22 | store or share any user data or analytics. All data generated is 23 | stored in each users' own storage buckets, called Gaia Hubs. You as 24 | a user have control over your own hub with your Blockstack id. 25 |
26 | Differences 27 |
28 | 29 | Most social media companies are providing you with a free account so 30 | that they can sell your information to the highest bidder. 31 | Blockstack is different. With Blockstack, YOU control your identity. 32 | Neither Blockstack nor the makers of OI Calendar can take the id 33 | from you or have access to it. 34 | 35 |
36 | 37 | 38 | Blockstack is a new approach to the internet that let you own your 39 | data and maintain your privacy, security and freedom. Find out more 40 | at{' '} 41 | 42 | Blockstack's documentation 43 | 44 | 45 | 46 | Absolutely, but we always encourage the skeptics and the curious to 47 | check it out yourselves. You may view{' '} 48 | 49 | the source code here 50 | 51 | . OI Calendar comes with a friendly open source license. 52 |
53 | You can even host your own version of the app yourselves. 54 |
55 | 56 | Deploy to Netlify 57 | 58 |
59 |
60 | 61 | 62 | OpenIntents (for short OI) was founded in 2009 in Berlin, Germany. 63 | It started as a community effort with strong focus on open source 64 | and interoperability between apps. You can see more of our work in 65 | Android at openintents.org.{' '} 66 | Friedger Müffke is the 67 | maintainer of OpenIntents' code repositories. 68 | 69 | 70 | You can make monetary contributions to our{' '} 71 | OpenCollective{' '} 72 | or encourage developers by funding an issue on{' '} 73 | gitcoin.co.
74 | Remember that your calendar data and identity will always be yours 75 | and does not depend on OpenIntents' funding. 76 |
77 |
78 | 79 | 80 | On Android, you choose one calendar app from the ones already out 81 | there, like Business Calendar, Etar, ..., then install{' '} 82 | 83 | OI Calendar-Sync 84 | 85 | . OI Calendar-Sync will sync your agenda from your Blockstack 86 | storage to the Android device (on there, in fact, to the Android 87 | calendar provider) and vice versa. You will find a OI Calendar-Sync 88 | account with your Blockstack ID in your Android settings{' '} 89 | Accounts that also shows you when the last sync took place, 90 | usually, every hour. 91 | 92 | 93 |
94 |
95 |
96 | ) 97 | 98 | export default FAQs 99 | -------------------------------------------------------------------------------- /src/reminder/reminder.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | const nameOf = guest => { 4 | if (guest.name) { 5 | return guest.name 6 | } else { 7 | return guest.username 8 | } 9 | } 10 | 11 | const simpleMatrixMessage = (msg, uid) => { 12 | return { 13 | msgtype: 'm.text', 14 | body: `${msg}`, 15 | format: 'org.matrix.custom.html', 16 | formatted_body: `${msg}`, 17 | } 18 | } 19 | 20 | /** 21 | * The Reminder object contains the timerId and metadata of the event 22 | */ 23 | class Reminder { 24 | // Timer ID 25 | timerId = 0 26 | 27 | // Event Metadata 28 | title = '' 29 | uid = '' 30 | start = null 31 | guests = null 32 | 33 | constructor(timeout, title, uid, start, guests, userSessionChat) { 34 | /** 35 | * Gets called when the app is first loaded and the Reminder object is created. 36 | */ 37 | this.setReminderInterval(timeout) 38 | 39 | // Set Metadata 40 | this.title = title 41 | this.uid = uid 42 | this.start = start 43 | this.guests = guests 44 | this.userSessionChat = userSessionChat 45 | } 46 | 47 | setReminderInterval = timeout => { 48 | // If duration is positive, enable reminder 49 | if (timeout > 0) { 50 | // Clear existing timer 51 | if (this.timerId) { 52 | clearTimeout(this.timerId) 53 | } 54 | 55 | this.timerId = setTimeout(this.notifyUser, timeout) 56 | } 57 | } 58 | 59 | // Popup the notification to remind user about the event 60 | notifyUser = () => { 61 | const msg = `${this.title} takes place ${moment 62 | .utc() 63 | .to(moment.utc(this.start))}` 64 | 65 | // web notification 66 | if (Notification.permission !== 'granted') { 67 | Notification.requestPermission() 68 | } else { 69 | const notification = new Notification(msg, { 70 | icon: 'android-chrome-192x192.png', 71 | silent: true, 72 | }) 73 | 74 | notification.onclick = () => { 75 | window.location.href = `/?intent=view&uid=${this.uid}` 76 | } 77 | } 78 | 79 | // matrix notifcation 80 | if (this.userSessionChat) { 81 | const springRolePromises = this.guests.map(g => { 82 | return fetch( 83 | `https://springrole.com/blockstack/${g.identityAddress}` 84 | ).then( 85 | response => { 86 | if (response.ok) { 87 | return g 88 | } else { 89 | return undefined 90 | } 91 | }, 92 | () => { 93 | return undefined 94 | } 95 | ) 96 | }) 97 | console.log('springrole promises', springRolePromises) 98 | Promise.all(springRolePromises) 99 | .then( 100 | springRoleGuests => { 101 | let message 102 | springRoleGuests = springRoleGuests.filter(g => !!g) 103 | if (springRoleGuests.length > 0) { 104 | const links = springRoleGuests.map( 105 | g => 106 | `${nameOf(g)}` 109 | ) 110 | const names = this.guests.map(g => nameOf(g)) 111 | message = { 112 | msgtype: 'm.text', 113 | body: `${msg}. Read more about your guests here: ${names.join( 114 | ', ' 115 | )}`, 116 | format: 'org.matrix.custom.html', 117 | formatted_body: `${msg}. Read more about your guests here: ${links.join( 118 | ', ' 119 | )}`, 120 | } 121 | } else { 122 | message = simpleMatrixMessage(msg, this.uid) 123 | } 124 | return message 125 | }, 126 | error => { 127 | console.log('failed to handle reminder', error) 128 | return simpleMatrixMessage(msg, this.uid) 129 | } 130 | ) 131 | .then( 132 | message => { 133 | console.log('send message to self', message) 134 | this.userSessionChat.sendMessageToSelf(message) 135 | }, 136 | error => { 137 | console.log('err reminder', error) 138 | } 139 | ) 140 | } else { 141 | console.log('no usersession :-(') 142 | } 143 | } 144 | } 145 | 146 | export default Reminder 147 | -------------------------------------------------------------------------------- /src/css/datetime.css: -------------------------------------------------------------------------------- 1 | .rdt { 2 | position: relative; 3 | } 4 | 5 | .rdtPicker { 6 | display: none; 7 | position: absolute; 8 | width: 100%; 9 | max-width: 300px; 10 | padding: 4px; 11 | margin-top: 1px; 12 | z-index: 99999 !important; 13 | background: #fff; 14 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 15 | border: 1px solid #f9f9f9; 16 | } 17 | 18 | .rdtOpen .rdtPicker { 19 | display: block; 20 | } 21 | 22 | .rdtStatic .rdtPicker { 23 | box-shadow: none; 24 | position: static; 25 | } 26 | 27 | .rdtPicker .rdtTimeToggle { 28 | text-align: center; 29 | } 30 | 31 | .rdtPicker table { 32 | width: 100%; 33 | margin: 0; 34 | } 35 | 36 | .rdtPicker td, 37 | .rdtPicker th { 38 | text-align: center; 39 | height: 28px; 40 | } 41 | 42 | .rdtPicker td { 43 | cursor: pointer; 44 | } 45 | 46 | .rdtPicker td.rdtDay:hover, 47 | .rdtPicker td.rdtHour:hover, 48 | .rdtPicker td.rdtMinute:hover, 49 | .rdtPicker td.rdtSecond:hover, 50 | .rdtPicker .rdtTimeToggle:hover { 51 | background: #eeeeee; 52 | cursor: pointer; 53 | } 54 | 55 | .rdtPicker td.rdtOld, 56 | .rdtPicker td.rdtNew { 57 | color: #999999; 58 | } 59 | 60 | .rdtPicker td.rdtToday { 61 | position: relative; 62 | } 63 | 64 | .rdtPicker td.rdtToday:before { 65 | content: ''; 66 | display: inline-block; 67 | border-left: 7px solid transparent; 68 | border-bottom: 7px solid #428bca; 69 | border-top-color: rgba(0, 0, 0, 0.2); 70 | position: absolute; 71 | bottom: 4px; 72 | right: 4px; 73 | } 74 | 75 | .rdtPicker td.rdtActive, 76 | .rdtPicker td.rdtActive:hover { 77 | background-color: #428bca; 78 | color: #fff; 79 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 80 | } 81 | 82 | .rdtPicker td.rdtActive.rdtToday:before { 83 | border-bottom-color: #fff; 84 | } 85 | 86 | .rdtPicker td.rdtDisabled, 87 | .rdtPicker td.rdtDisabled:hover { 88 | background: none; 89 | color: #999999; 90 | cursor: not-allowed; 91 | } 92 | 93 | .rdtPicker td span.rdtOld { 94 | color: #999999; 95 | } 96 | 97 | .rdtPicker td span.rdtDisabled, 98 | .rdtPicker td span.rdtDisabled:hover { 99 | background: none; 100 | color: #999999; 101 | cursor: not-allowed; 102 | } 103 | 104 | .rdtPicker th { 105 | border-bottom: 1px solid #f9f9f9; 106 | } 107 | 108 | .rdtPicker .dow { 109 | width: 14.2857%; 110 | border-bottom: none; 111 | } 112 | 113 | .rdtPicker th.rdtSwitch { 114 | width: 100px; 115 | } 116 | 117 | .rdtPicker th.rdtNext, 118 | .rdtPicker th.rdtPrev { 119 | font-size: 21px; 120 | vertical-align: top; 121 | } 122 | 123 | .rdtPrev span, 124 | .rdtNext span { 125 | display: block; 126 | -webkit-touch-callout: none; /* iOS Safari */ 127 | -webkit-user-select: none; /* Chrome/Safari/Opera */ 128 | -khtml-user-select: none; /* Konqueror */ 129 | -moz-user-select: none; /* Firefox */ 130 | -ms-user-select: none; /* Internet Explorer/Edge */ 131 | user-select: none; 132 | } 133 | 134 | .rdtPicker th.rdtDisabled, 135 | .rdtPicker th.rdtDisabled:hover { 136 | background: none; 137 | color: #999999; 138 | cursor: not-allowed; 139 | } 140 | 141 | .rdtPicker thead tr:first-child th { 142 | cursor: pointer; 143 | } 144 | 145 | .rdtPicker thead tr:first-child th:hover { 146 | background: #eeeeee; 147 | } 148 | 149 | .rdtPicker tfoot { 150 | border-top: 1px solid #f9f9f9; 151 | } 152 | 153 | .rdtPicker button { 154 | border: none; 155 | background: none; 156 | cursor: pointer; 157 | } 158 | 159 | .rdtPicker button:hover { 160 | background-color: #eee; 161 | } 162 | 163 | .rdtPicker thead button { 164 | width: 100%; 165 | height: 100%; 166 | } 167 | 168 | td.rdtMonth, 169 | td.rdtYear { 170 | height: 50px; 171 | width: 25%; 172 | cursor: pointer; 173 | } 174 | 175 | td.rdtMonth:hover, 176 | td.rdtYear:hover { 177 | background: #eee; 178 | } 179 | 180 | .rdtCounters { 181 | display: inline-block; 182 | } 183 | 184 | .rdtCounters > div { 185 | float: left; 186 | } 187 | 188 | .rdtCounter { 189 | height: 100px; 190 | } 191 | 192 | .rdtCounter { 193 | width: 40px; 194 | } 195 | 196 | .rdtCounterSeparator { 197 | line-height: 100px; 198 | } 199 | 200 | .rdtCounter .rdtBtn { 201 | height: 40%; 202 | line-height: 40px; 203 | cursor: pointer; 204 | display: block; 205 | 206 | -webkit-touch-callout: none; /* iOS Safari */ 207 | -webkit-user-select: none; /* Chrome/Safari/Opera */ 208 | -khtml-user-select: none; /* Konqueror */ 209 | -moz-user-select: none; /* Firefox */ 210 | -ms-user-select: none; /* Internet Explorer/Edge */ 211 | user-select: none; 212 | } 213 | 214 | .rdtCounter .rdtBtn:hover { 215 | background: #eee; 216 | } 217 | 218 | .rdtCounter .rdtCount { 219 | height: 20%; 220 | font-size: 1.2em; 221 | } 222 | 223 | .rdtMilli { 224 | vertical-align: middle; 225 | padding-left: 8px; 226 | width: 48px; 227 | } 228 | 229 | .rdtMilli input { 230 | width: 100%; 231 | font-size: 1.2em; 232 | margin-top: 37px; 233 | } 234 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ) 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location) 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl) 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ) 46 | }) 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl) 50 | } 51 | }) 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.') 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.') 74 | } 75 | } 76 | } 77 | } 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error) 81 | }) 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload() 97 | }) 98 | }) 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl) 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ) 108 | }) 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister() 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/components/Settings/AddDeleteSetting.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Button, Card, OverlayTrigger, Tooltip } from 'react-bootstrap' 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 4 | 5 | class AddDeleteSetting extends Component { 6 | constructor(props) { 7 | super(props) 8 | this.state = { 9 | valueOfAdd: props.valueOfAdd || '', 10 | showFollow: false, 11 | } 12 | this.bound = [ 13 | 'renderItem', 14 | 'onAddValueChange', 15 | 'onAddItem', 16 | 'onDeleteItem', 17 | 'onChangeItem', 18 | 'onFollowItem', 19 | 'onUnfollowItem', 20 | ].reduce((acc, d) => { 21 | acc[d] = this[d].bind(this) 22 | return acc 23 | }, {}) 24 | } 25 | 26 | onAddValueChange(event) { 27 | const valueOfAdd = event.target.value 28 | this.setState({ valueOfAdd }) 29 | } 30 | 31 | onAddItem(event) { 32 | const { valueOfAdd, addValueToItem } = this.state 33 | const { addItem } = this.props 34 | addValueToItem(valueOfAdd, ({ item, error }) => { 35 | if (error) { 36 | this.setState({ 37 | valueOfAdd, 38 | errorOfAdd: (error || '').toString(), 39 | }) 40 | } else { 41 | addItem(item) 42 | this.setState({ valueOfAdd: '', errorOfAdd: '' }) 43 | } 44 | }) 45 | } 46 | 47 | onDeleteItem(idx) { 48 | const { items: itemList, deleteItems } = this.props 49 | const item = itemList[idx] 50 | deleteItems([item]) 51 | } 52 | 53 | onChangeItem(item, data) { 54 | const { setItemData } = this.props 55 | setItemData(item, data) 56 | } 57 | 58 | onFollowItem(idx) { 59 | const { items, followItem } = this.props 60 | const item = items[idx] 61 | console.log(item) 62 | followItem(item) 63 | } 64 | 65 | onUnfollowItem(idx) { 66 | const { items, unfollowItem } = this.props 67 | const item = items[idx] 68 | unfollowItem(item) 69 | } 70 | 71 | renderUnFollowButton(follows, i, username) { 72 | const { onUnfollowItem, onFollowItem } = this.bound 73 | 74 | if (follows) { 75 | const tip = 'Remove ' + username + "'s public calendar" 76 | const tooltip = {tip} 77 | return ( 78 | 79 |
80 | onUnfollowItem(i)} 84 | /> 85 |
86 |
87 | ) 88 | } else { 89 | const tip = 'Add ' + username + "'s public calendar" 90 | const tooltip = {tip} 91 | return ( 92 | 93 |
94 | onFollowItem(i)} 98 | /> 99 |
100 |
101 | ) 102 | } 103 | } 104 | 105 | renderItem(d, i, user, calendars) { 106 | const { ItemRenderer, showFollow } = this.state 107 | const { onChangeItem, onDeleteItem } = this.bound 108 | const follows = 109 | showFollow && 110 | !!calendars.find( 111 | c => c.type === 'blockstack-user' && c.data.user === d.username 112 | ) 113 | const showDelete = d.name !== 'default' || d.type !== 'private' 114 | 115 | return ( 116 |
117 |
118 | {ItemRenderer && ( 119 | 125 | )} 126 |
127 | {showFollow && this.renderUnFollowButton(follows, i, d.username)} 128 | {showDelete && ( 129 |
130 | onDeleteItem(i)} 134 | /> 135 |
136 | )} 137 |
138 | ) 139 | } 140 | 141 | render() { 142 | const { items: itemList, user, calendars } = this.props 143 | const { renderItem, onAddItem } = this.bound 144 | const { 145 | valueOfAdd, 146 | addTitle, 147 | listTitle, 148 | renderAdd, 149 | errorOfAdd, 150 | } = this.state 151 | return ( 152 |
153 | 154 | {addTitle} 155 | 156 | {renderAdd()} 157 | 164 | {errorOfAdd} 165 | 166 | 167 | 168 | 169 | {listTitle} 170 | 171 |
172 | {(itemList || []).map((v, k) => 173 | renderItem(v, k, user, calendars) 174 | )} 175 |
176 |
177 |
178 |
179 | ) 180 | } 181 | } 182 | 183 | /* 184 | */ 185 | 186 | export default AddDeleteSetting 187 | -------------------------------------------------------------------------------- /src/components/EventGuestList/__usage__/scenario.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import GuestList from '..' 4 | 5 | const guests = { 6 | 'friedger.id': { 7 | '@type': 'Person', 8 | '@context': 'http://schema.org', 9 | name: 'Friedger Müffke', 10 | description: 'Entredeveloper in Europe', 11 | image: [ 12 | { 13 | '@type': 'ImageObject', 14 | name: 'avatar', 15 | contentUrl: 16 | 'https://gaia.blockstack.org/hub/1Maw8BjWgj6MWrBCfupqQuWANthMhefb2v/0/avatar-0', 17 | }, 18 | ], 19 | account: [ 20 | { 21 | '@type': 'Account', 22 | placeholder: false, 23 | service: 'twitter', 24 | identifier: 'fmdroid', 25 | proofType: 'http', 26 | proofUrl: 'https://twitter.com/fmdroid/status/927285474854670338', 27 | }, 28 | { 29 | '@type': 'Account', 30 | placeholder: false, 31 | service: 'facebook', 32 | identifier: 'friedger.mueffke', 33 | proofType: 'http', 34 | proofUrl: 35 | 'https://www.facebook.com/friedger.mueffke/posts/10155370909214191', 36 | }, 37 | { 38 | '@type': 'Account', 39 | placeholder: false, 40 | service: 'github', 41 | identifier: 'friedger', 42 | proofType: 'http', 43 | proofUrl: 44 | 'https://gist.github.com/friedger/d789f7afd1aa0f23dd3f87eb40c2673e', 45 | }, 46 | { 47 | '@type': 'Account', 48 | placeholder: false, 49 | service: 'bitcoin', 50 | identifier: '1MATdc1Xjen4GUYMhZW5nPxbou24bnWY1v', 51 | proofType: 'http', 52 | proofUrl: '', 53 | }, 54 | { 55 | '@type': 'Account', 56 | placeholder: false, 57 | service: 'pgp', 58 | identifier: '5371148B3FC6B5542CADE04F279B3081B173CFD0', 59 | proofType: 'http', 60 | proofUrl: '', 61 | }, 62 | { 63 | '@type': 'Account', 64 | placeholder: false, 65 | service: 'ethereum', 66 | identifier: '0x73274c046ae899b9e92EaAA1b145F0b5f497dd9a', 67 | proofType: 'http', 68 | proofUrl: '', 69 | }, 70 | ], 71 | apps: { 72 | 'https://app.graphitedocs.com': 73 | 'https://gaia.blockstack.org/hub/17Qhy4ob8EyvScU6yiP6sBdkS2cvWT9FqE/', 74 | 'https://www.stealthy.im': 75 | 'https://gaia.blockstack.org/hub/1KyYJihfZUjYyevfPYJtCEB8UydxqQS67E/', 76 | 'https://www.chat.hihermes.co': 77 | 'https://gaia.blockstack.org/hub/1DbpoUCdEpyTaND5KbZTMU13nhNeDfVScD/', 78 | 'https://app.travelstack.club': 79 | 'https://gaia.blockstack.org/hub/1QK5n11Xn1p5aP74xy14NCcYPndHxnwN5y/', 80 | 'https://app.afari.io': 81 | 'https://gaia.blockstack.org/hub/1E4VQ7A4WVTSXu579xDH8SjJTonfEbR6kL/', 82 | 'https://blockusign.co': 83 | 'https://gaia.blockstack.org/hub/1Pom8K1nh3c3M6R5oHZMK5Y4p2s2386qVQ/', 84 | 'https://blkbreakout.herokuapp.com': 85 | 'https://gaia.blockstack.org/hub/1fE5AQaRGKBJVKBAT2DT28tazgsuwAhUp/', 86 | 'https://kit.now.sh': 87 | 'https://gaia.blockstack.org/hub/1GiyoGB1Rw8vDJEb3jwDRBeivWuSKL5b7u/', 88 | 'http://localhost:3000': 89 | 'https://gaia.blockstack.org/hub/1NRwke9GLbyKG5efbEqFEDgCJGVLoce9TL/', 90 | 'http://localhost:3001': 91 | 'https://gaia.blockstack.org/hub/1sRMsXZADgJD513aX1d7txPWDUw1H2wwd/', 92 | 'https://planet.friedger.de': 93 | 'https://gaia.blockstack.org/hub/1BnkJMbfEz2XDb5kSQtRureeXf91HjJ2x/', 94 | 'http://127.0.0.1:3001': 95 | 'https://gaia.blockstack.org/hub/1NrUHmxV2ABHTugfDCyEcJVdQUNxCt99Wy/', 96 | 'https://animal-kingdom-1.firebaseapp.com': 97 | 'https://gaia.blockstack.org/hub/1EEMu1HAH7Gfe8WiPUVaPyeQuMe6bX12Dj/', 98 | 'https://gaia_admin.brightblock.org': 99 | 'https://gaia.blockstack.org/hub/19gWjknDbEkhQVKLqBnW1y4LpBnpripMnn/', 100 | 'https://dpage.io': 101 | 'https://gaia.blockstack.org/hub/1HdNyomWupHPP4YinrFbEKKezh5x9vRKnf/', 102 | 'https://app.dappywallet.com': 103 | 'https://gaia.blockstack.org/hub/1EEmaiiDZrCKNmcQNGNHkRGN7pstUZTzfV/', 104 | 'https://animalkingdoms.netlify.com': 105 | 'https://gaia.blockstack.org/hub/1G3SNwnaNWFpoNyz4Jzo2avKS49W33rU8y/', 106 | 'https://ourtopia.co': 107 | 'https://gaia.blockstack.org/hub/1DS1SeRyK1EVigp6V8g9jCFMFQGHvWEkVL/', 108 | 'http://localhost:8080': 109 | 'https://gaia.blockstack.org/hub/1GApfxNVsCqvUPoMQxZXv2Yz8ZuHKNXc6r/', 110 | 'http://127.0.0.1:8080': 111 | 'https://gaia.blockstack.org/hub/1EeKHp5xwfBGrdWLHyRm2RhQKiw5kVQSaj/', 112 | 'https://chat.openintents.org': 113 | 'https://gaia.blockstack.org/hub/18zdPLrtyxzE8ArXw6Ne3E84GpZQPWAkwo/', 114 | 'https://humans.name': 115 | 'https://gaia.blockstack.org/hub/1Bhexe6K4viWyUqR9VQEUPPo8xjpf4YjB5/', 116 | 'http://127.0.0.1:3000': 117 | 'https://gaia.blockstack.org/hub/1LBwEcxjmPVfZeNGY7pYysfZ4TNab2cxs/', 118 | 'https://upbeat-wing-158214.netlify.com': 119 | 'https://gaia.blockstack.org/hub/1GEswKnyPW7E2Z9uxjxUndAkFf6HYYX3v5/', 120 | 'https://cal.openintents.org': 121 | 'https://gaia.blockstack.org/hub/14nFNe9opru28Deoy5aB297kgLM3jkNmwF/', 122 | 'https://debutapp.social': 123 | 'https://gaia.blockstack.org/hub/1EokUib85GvmHu3xewYtBWLNiemSKJAiKR/', 124 | 'https://oi-timesheet.com': 125 | 'https://gaia.blockstack.org/hub/1KjTySPCicoTHA1SwRch3n6iZCK1oJyu9m/', 126 | }, 127 | api: { 128 | gaiaHubConfig: { url_prefix: 'https://gaia.blockstack.org/hub/' }, 129 | gaiaHubUrl: 'https://hub.blockstack.org', 130 | }, 131 | }, 132 | } 133 | 134 | const Scenario = () => { 135 | return ( 136 |
137 | 138 |
139 | ) 140 | } 141 | 142 | export default Scenario 143 | -------------------------------------------------------------------------------- /public/gtutorial.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 43 | OI Calendar - How to import from Google Calendar 44 | 45 | 46 | 47 |
48 | 60 |
61 |

Move from Google Calendar

62 |

63 | Google provides a private link that contains all your events. 64 | Unfortunately, Google does not let you easily use these events, you need a 65 | CORS 70 | browser plugin. 71 |

72 |
    73 |
  1. 74 | Copy your private Google calendar url 75 |
      76 |
    • 77 | Login to Google Calendar and goto settings: 78 | https://calendar.google.com/calendar/r/settings 83 |
    • 84 |
    • 85 | Select your calendar on the left side
      86 | Select 96 | 97 | Select3 107 |
    • 108 |
    • On the your calendar's settings, scroll to the bottom
    • 109 |
    • 110 | Copy the private address of your calendar containing your email 111 | address and ends with basic.ics
      112 | Select3 122 |
    • 123 |
    124 |
  2. 125 |
  3. 126 | Add to OI Calendar 127 |
      128 |
    • 129 | Open OI Calendar 130 | https://cal.openintents.org/ 133 |
    • 134 |
    • Enable your CORS browser plugin
    • 135 |
    • 136 | Paste the private address into the Paste url ... field 137 | and press enter 138 |
    • 139 |
    140 |
  4. 141 |
  5. Enjoy YOUR calendar!
  6. 142 |
143 | 144 | 145 | -------------------------------------------------------------------------------- /src/components/PublicCalendar/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { PropTypes } from 'prop-types' 3 | import moment from 'moment' 4 | import { Card, ProgressBar, Button, Alert } from 'react-bootstrap' 5 | import { Calendar, momentLocalizer } from 'react-big-calendar' 6 | import 'react-big-calendar/lib/css/react-big-calendar.css' 7 | import queryString from 'query-string' 8 | // Containers 9 | import EventDetailsContainer from '../../containers/EventDetails' 10 | 11 | let localizer = momentLocalizer(moment) 12 | 13 | class PublicCalendar extends Component { 14 | constructor(props) { 15 | super(props) 16 | this.bound = [].reduce((acc, d) => { 17 | acc[d] = this[d].bind(this) 18 | return acc 19 | }, {}) 20 | } 21 | 22 | componentWillMount() { 23 | if (this.props.public) { 24 | const query = queryString.parse(this.props.location.search) 25 | const calendarName = query.c 26 | if ( 27 | calendarName && 28 | this.props.events.user && 29 | this.props.events.user.username && 30 | this.props.events.user.username.length > 0 31 | ) { 32 | if (calendarName.endsWith(this.props.events.user.username)) { 33 | this.props.showMyPublicCalendar(calendarName) 34 | } else { 35 | this.props.viewPublicCalendar(calendarName) 36 | } 37 | } else { 38 | this.props.history.replace('/') 39 | } 40 | } 41 | } 42 | 43 | eventStyle(event, start, end, isSelected) { 44 | var bgColor = event && event.hexColor ? event.hexColor : '#265985' 45 | var style = { 46 | backgroundColor: bgColor, 47 | borderColor: 'white', 48 | } 49 | return { 50 | style: style, 51 | } 52 | } 53 | 54 | getEventStart(eventInfo) { 55 | return eventInfo ? new Date(eventInfo.start) : new Date() 56 | } 57 | 58 | getEventEnd(eventInfo) { 59 | return eventInfo && (eventInfo.end || eventInfo.calculatedEndTime) 60 | ? eventInfo.end 61 | ? new Date(eventInfo.end) 62 | : new Date(eventInfo.calculatedEndTime) 63 | : new Date() 64 | } 65 | 66 | render() { 67 | console.log('[PublicCalendar.render]', this.props) 68 | const { 69 | signedIn, 70 | myPublicCalendar, 71 | myPublicCalendarIcsUrl, 72 | publicCalendar, 73 | publicCalendarEvents, 74 | currentCalendarLength, 75 | currentCalendarIndex, 76 | eventModal, 77 | showError, 78 | error, 79 | showSettingsAddCalendar, 80 | markErrorAsRead, 81 | } = this.props 82 | const { 83 | handleEditEvent, 84 | handleAddEvent, 85 | handleViewAllCalendars, 86 | } = this.bound 87 | 88 | let events = Object.values(this.props.events.allEvents) 89 | let shareUrl = null 90 | if (myPublicCalendar) { 91 | events = events.filter(e => e.public && e.calendarName === 'default') 92 | shareUrl = 93 | window.location.origin + '/?intent=view&name=' + myPublicCalendar 94 | } else if (publicCalendarEvents) { 95 | events = publicCalendarEvents 96 | shareUrl = 97 | window.location.origin + '/?intent=addics&url=' + publicCalendar 98 | } 99 | 100 | const calendarView = ( 101 |
102 |
103 | {currentCalendarLength && ( 104 | 111 | )} 112 |
113 | handleEditEvent(event)} 123 | onSelectSlot={slotInfo => handleAddEvent(slotInfo)} 124 | style={{ minHeight: '500px' }} 125 | eventPropGetter={this.eventStyle} 126 | startAccessor={this.getEventStart} 127 | endAccessor={this.getEventEnd} 128 | /> 129 | {showError && ( 130 |
138 | 139 | {error} 140 |

141 | 144 | or 145 | 146 |

147 |
148 |
149 | )} 150 |
151 | ) 152 | 153 | return ( 154 |
155 | {eventModal && } 156 | {(myPublicCalendar || publicCalendar) && ( 157 | 158 | 159 | Public Calendar {myPublicCalendar} 160 | {publicCalendar} 161 | 169 | 170 | {myPublicCalendar && events.length > 0 && ( 171 | 172 | Share this url: {shareUrl} 173 | {myPublicCalendarIcsUrl && ( 174 | 175 | {' '} 176 | or as .ics file 177 | 178 | )} 179 | 180 | )} 181 | {myPublicCalendar && events.length === 0 && ( 182 | 183 | No public events yet. Start publishing your events! 184 | 185 | )} 186 | {publicCalendar && events.length > 0 && signedIn && ( 187 | 188 | Add to my calandars 189 | 190 | )} 191 | {calendarView} 192 | 193 | )} 194 |
195 | ) 196 | } 197 | } 198 | 199 | PublicCalendar.propTypes = { 200 | location: PropTypes.object, 201 | public: PropTypes.bool, 202 | showMyPublicCalendar: PropTypes.func, 203 | } 204 | 205 | export default PublicCalendar 206 | -------------------------------------------------------------------------------- /src/store/event/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_EVENTS, 3 | USER, 4 | SET_CONTACTS, 5 | SET_INVITE_SEND_STATUS, 6 | INVITES_SENT_OK, 7 | INVITES_SENT_FAIL, 8 | SET_CURRENT_GUESTS, 9 | SET_CURRENT_EVENT, 10 | UNSET_CURRENT_EVENT, 11 | INITIALIZE_CHAT, 12 | SHOW_SETTINGS, 13 | HIDE_SETTINGS, 14 | SHOW_SETTINGS_ADD_CALENDAR, 15 | SET_CALENDARS, 16 | SHOW_MY_PUBLIC_CALENDAR, 17 | SHOW_ALL_CALENDARS, 18 | SET_PUBLIC_CALENDAR_EVENTS, 19 | SHOW_INSTRUCTIONS, 20 | AUTH_SIGN_OUT, 21 | UNSET_CURRENT_INVITES, 22 | SET_LOADING_CALENDARS, 23 | SET_ERROR, 24 | CREATE_CONFERENCING_ROOM, 25 | REMOVE_CONFERENCING_ROOM, 26 | VERIFY_NEW_CALENDAR, 27 | SHOW_FILES, 28 | SET_RICH_NOTIF_ENABLED, 29 | SET_RICH_NOTIF_ERROR, 30 | SET_RICH_NOTIF_EXCLUDE_GUESTS, 31 | SET_CHAT_STATUS, 32 | SET_REMINDERS_INFO_REQUEST, 33 | SET_ALL_NOTIF_ENABLED, 34 | } from '../ActionTypes' 35 | 36 | let initialState = { 37 | allEvents: [], 38 | calendars: [], 39 | contacts: [], 40 | user: '', 41 | verifiedNewCalendarData: { 42 | status: '', 43 | }, 44 | } 45 | 46 | export default function reduce(state = initialState, action = {}) { 47 | console.log('EventReducer', action) 48 | const { type, payload } = action 49 | let newState = state 50 | switch (type) { 51 | case INITIALIZE_CHAT: 52 | newState = { ...state, userSessionChat: payload } 53 | break 54 | 55 | case USER: 56 | newState = { ...state, user: action.user } 57 | break 58 | 59 | case SET_CONTACTS: 60 | newState = { ...state, contacts: payload } 61 | break 62 | 63 | case SET_EVENTS: 64 | newState = { ...state, allEvents: action.allEvents } 65 | break 66 | 67 | case SET_CURRENT_EVENT: 68 | console.log('SET_CURRENT_EVENT', payload) 69 | newState = { 70 | ...state, 71 | currentEvent: payload.currentEvent, 72 | } 73 | if (payload.hasOwnProperty('currentEventType')) { 74 | newState.currentEventType = payload.currentEventType 75 | } 76 | if (payload.hasOwnProperty('currentEventUid')) { 77 | newState.currentEventUid = payload.currentEventUid 78 | } 79 | break 80 | 81 | case UNSET_CURRENT_EVENT: 82 | newState = { 83 | ...state, 84 | currentEvent: undefined, 85 | currentEventType: undefined, 86 | currentEventUid: undefined, 87 | currentGuests: undefined, 88 | inviteStatus: undefined, 89 | inviteSuccess: undefined, 90 | inviteError: undefined, 91 | } 92 | break 93 | case SET_INVITE_SEND_STATUS: 94 | console.log('SET_INVITE_SEND_STATUS') 95 | newState = { 96 | ...state, 97 | inviteStatus: payload.status, 98 | } 99 | break 100 | case INVITES_SENT_OK: 101 | console.log('INVITES_SENT_OK') 102 | newState = { 103 | ...state, 104 | currentEvent: undefined, 105 | currentEventType: undefined, 106 | currentGuests: undefined, 107 | inviteStatus: undefined, 108 | inviteSuccess: true, 109 | inviteError: undefined, 110 | } 111 | break 112 | 113 | case INVITES_SENT_FAIL: 114 | console.log('INVITES_SENT_FAIL') 115 | newState = { 116 | ...state, 117 | currentEvent: payload.eventInfo, 118 | currentEventType: payload.eventType, 119 | inviteStatus: undefined, 120 | inviteSuccess: false, 121 | inviteError: payload.error, 122 | } 123 | break 124 | case UNSET_CURRENT_INVITES: 125 | newState = { 126 | ...state, 127 | inviteSuccess: undefined, 128 | inviteError: undefined, 129 | } 130 | break 131 | 132 | case SET_CURRENT_GUESTS: 133 | newState = { 134 | ...state, 135 | currentGuests: payload.profiles, 136 | inviteSuccess: undefined, 137 | inviteError: undefined, 138 | } 139 | break 140 | 141 | case SHOW_SETTINGS: 142 | newState = { 143 | ...state, 144 | showPage: 'settings', 145 | myPublicCalendar: undefined, 146 | myPublicCalendarIcsUrl: undefined, 147 | publicCalendar: undefined, 148 | publicCalendarEvents: undefined, 149 | } 150 | break 151 | 152 | case SHOW_SETTINGS_ADD_CALENDAR: 153 | newState = { 154 | ...state, 155 | showPage: 'settings', 156 | myPublicCalendar: undefined, 157 | myPublicCalendarIcsUrl: undefined, 158 | publicCalendar: undefined, 159 | publicCalendarEvents: undefined, 160 | showSettingsAddCalendarUrl: payload.url, 161 | } 162 | break 163 | 164 | case HIDE_SETTINGS: 165 | newState = { ...state, showPage: 'all' } 166 | break 167 | case SET_REMINDERS_INFO_REQUEST: 168 | newState = { ...state, showRemindersInfo: payload.show } 169 | break 170 | case SET_CALENDARS: 171 | newState = { ...state, calendars: payload } 172 | break 173 | case SHOW_MY_PUBLIC_CALENDAR: 174 | newState = { 175 | ...state, 176 | myPublicCalendar: payload.name, 177 | myPublicCalendarIcsUrl: payload.icsUrl, 178 | publicCalendar: undefined, 179 | publicCalendarEvents: undefined, 180 | showPage: 'public', 181 | } 182 | break 183 | case SHOW_ALL_CALENDARS: 184 | newState = { 185 | ...state, 186 | myPublicCalendar: undefined, 187 | myPublicCalendarIcsUrl: undefined, 188 | publicCalendar: undefined, 189 | publicCalendarEvents: undefined, 190 | showPage: 'all', 191 | } 192 | break 193 | case SET_PUBLIC_CALENDAR_EVENTS: 194 | newState = { 195 | ...state, 196 | myPublicCalendar: undefined, 197 | myPublicCalendarIcsUrl: undefined, 198 | publicCalendarEvents: payload.allEvents, 199 | publicCalendar: payload.calendar.name, 200 | } 201 | break 202 | case SET_LOADING_CALENDARS: 203 | const currentIndex = state.currentCalendarIndex || 0 204 | const currentCalendarIndex = 205 | payload.index > currentIndex ? payload.index : currentIndex 206 | const done = payload.index >= payload.length 207 | newState = { 208 | ...state, 209 | currentCalendarIndex: done ? undefined : currentCalendarIndex, 210 | currentCalendarLength: done ? undefined : payload.length, 211 | } 212 | break 213 | case SHOW_INSTRUCTIONS: 214 | newState = { 215 | ...state, 216 | showInstructions: { general: payload.show }, 217 | } 218 | break 219 | case SHOW_FILES: 220 | newState = { 221 | ...state, 222 | showFiles: { all: action.payload.show }, 223 | showPage: 'files', 224 | } 225 | break 226 | case AUTH_SIGN_OUT: 227 | newState = initialState 228 | break 229 | case SET_ERROR: 230 | newState = { 231 | ...state, 232 | currentError: payload, 233 | } 234 | break 235 | case CREATE_CONFERENCING_ROOM: 236 | console.log('CREATE_CONFERENCING_ROOM', payload) 237 | if (payload.status === 'added') { 238 | newState = { 239 | ...state, 240 | addingConferencing: false, 241 | currentEvent: Object.assign({}, state.currentEvent, { 242 | url: payload.url, 243 | }), 244 | } 245 | } else { 246 | newState = { 247 | ...state, 248 | addingConferencing: payload.status === 'adding', 249 | } 250 | } 251 | break 252 | case REMOVE_CONFERENCING_ROOM: 253 | console.log('REMOVE_CONFERENCING_ROOM', payload) 254 | if (payload.status === 'removed') { 255 | newState = { 256 | ...state, 257 | removingConferencing: false, 258 | currentEvent: Object.assign({}, state.currentEvent, { 259 | url: null, 260 | }), 261 | } 262 | } else { 263 | newState = { 264 | ...state, 265 | removingConferencing: payload.status === 'removing', 266 | } 267 | } 268 | break 269 | case VERIFY_NEW_CALENDAR: 270 | newState = { 271 | ...state, 272 | verifiedNewCalendarData: payload, 273 | } 274 | break 275 | case SET_ALL_NOTIF_ENABLED: 276 | newState = { 277 | ...state, 278 | allNotifEnabled: payload.isEnabled, 279 | } 280 | break 281 | case SET_RICH_NOTIF_ERROR: 282 | newState = { 283 | ...state, 284 | richNotifError: payload.error, 285 | } 286 | break 287 | case SET_RICH_NOTIF_ENABLED: 288 | newState = { 289 | ...state, 290 | richNotifEnabled: payload.isEnabled, 291 | richNotifError: payload.error, 292 | } 293 | break 294 | case SET_RICH_NOTIF_EXCLUDE_GUESTS: 295 | newState = { 296 | ...state, 297 | richNofifExclude: payload.guests, 298 | } 299 | break 300 | case SET_CHAT_STATUS: 301 | newState = { 302 | ...state, 303 | chatStatus: payload.status, 304 | } 305 | break 306 | default: 307 | newState = state 308 | break 309 | } 310 | 311 | return newState 312 | } 313 | -------------------------------------------------------------------------------- /src/components/Calendar/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { PropTypes } from 'prop-types' 3 | import moment from 'moment' 4 | import { 5 | Card, 6 | Container, 7 | Row, 8 | Col, 9 | ProgressBar, 10 | Button, 11 | Alert, 12 | } from 'react-bootstrap' 13 | import { Calendar, momentLocalizer } from 'react-big-calendar' 14 | import 'react-big-calendar/lib/css/react-big-calendar.css' 15 | import queryString from 'query-string' 16 | // Containers 17 | import EventDetailsContainer from '../../containers/EventDetails' 18 | import RemindersModalContainer from '../../containers/RemindersModal' 19 | import SendInvitesModalContainer from '../../containers/SendInvitesModal' 20 | import FAQs from '../FAQ' 21 | 22 | import { uuid } from '../../core/eventFN' 23 | 24 | let localizer = momentLocalizer(moment) 25 | 26 | class OICalendar extends Component { 27 | constructor(props) { 28 | super(props) 29 | this.bound = [ 30 | 'handleHideInstructions', 31 | 'handleAddEvent', 32 | 'handleEditEvent', 33 | 'handleAddCalendarByUrl', 34 | ].reduce((acc, d) => { 35 | acc[d] = this[d].bind(this) 36 | return acc 37 | }, {}) 38 | } 39 | 40 | componentWillMount() { 41 | if (this.props.public) { 42 | const query = queryString.parse(this.props.location.search) 43 | const calendarName = query.c 44 | if ( 45 | calendarName && 46 | this.props.events.user && 47 | this.props.events.user.username && 48 | this.props.events.user.username.length > 0 49 | ) { 50 | if (calendarName.endsWith(this.props.events.user.username)) { 51 | this.props.showMyPublicCalendar(calendarName) 52 | } else { 53 | this.props.viewPublicCalendar(calendarName) 54 | } 55 | } else { 56 | this.props.history.replace('/') 57 | } 58 | } 59 | } 60 | 61 | handleHideInstructions() { 62 | this.props.hideInstructions() 63 | } 64 | 65 | handleEditEvent(event) { 66 | const { pickEventModal } = this.props 67 | var eventType 68 | if (event.mode === 'read-only') { 69 | eventType = 'view' 70 | } else { 71 | eventType = 'edit' 72 | } 73 | pickEventModal({ 74 | eventType, 75 | eventInfo: event, 76 | }) 77 | } 78 | 79 | handleAddEvent(slotInfo) { 80 | const { pickEventModal } = this.props 81 | slotInfo.uid = uuid() 82 | pickEventModal({ 83 | eventType: 'add', 84 | eventInfo: slotInfo, 85 | }) 86 | } 87 | 88 | handleAddCalendarByUrl(event) { 89 | if (event.key === 'Enter') { 90 | const { showSettingsAddCalendar } = this.props 91 | showSettingsAddCalendar(event.target.value) 92 | } 93 | } 94 | 95 | eventStyle(event, start, end, isSelected) { 96 | var bgColor = event && event.hexColor ? event.hexColor : '#265985' 97 | var style = { 98 | backgroundColor: bgColor, 99 | borderColor: 'white', 100 | } 101 | return { 102 | style: style, 103 | } 104 | } 105 | 106 | getEventStart(eventInfo) { 107 | return eventInfo ? new Date(eventInfo.start) : new Date() 108 | } 109 | 110 | getEventEnd(eventInfo) { 111 | return eventInfo && (eventInfo.end || eventInfo.calculatedEndTime) 112 | ? eventInfo.end 113 | ? new Date(eventInfo.end) 114 | : new Date(eventInfo.calculatedEndTime) 115 | : new Date() 116 | } 117 | 118 | render() { 119 | console.log('[Calendar.render]', this.props) 120 | const { 121 | signedIn, 122 | showGeneralInstructions, 123 | eventModal, 124 | inviteSuccess, 125 | currentCalendarLength, 126 | currentCalendarIndex, 127 | showError, 128 | error, 129 | showSettingsAddCalendar, 130 | markErrorAsRead, 131 | showRemindersModal, 132 | showSendInvitesModal, 133 | } = this.props 134 | const { 135 | handleHideInstructions, 136 | handleEditEvent, 137 | handleAddEvent, 138 | handleAddCalendarByUrl, 139 | } = this.bound 140 | 141 | let events = Object.values(this.props.events.allEvents) 142 | 143 | const calendarView = ( 144 |
145 |
146 | {currentCalendarLength && ( 147 | 154 | )} 155 |
156 | handleEditEvent(event)} 164 | onSelectSlot={slotInfo => handleAddEvent(slotInfo)} 165 | style={{ minHeight: '500px' }} 166 | eventPropGetter={this.eventStyle} 167 | startAccessor={this.getEventStart} 168 | endAccessor={this.getEventEnd} 169 | /> 170 | {showError && ( 171 |
179 | 180 | {error} 181 |

182 | 185 | or 186 | 187 |

188 |
189 |
190 | )} 191 |
192 | ) 193 | 194 | const oicalendarsync = ( 195 | <> 196 | For Android, use your favorite calendar app with{' '} 197 | 198 | OI Calendar-Sync 199 | 200 | 201 | ) 202 | return ( 203 |
204 | {signedIn && showGeneralInstructions && ( 205 | 206 | 207 | How to use OI Calendar 208 | 216 | 217 | 218 | 219 |
220 | 221 | 222 | Add an event: Click on a day to add 223 | event details. To add a multi-day event, click and hold 224 | while dragging across the days you want to include.
225 | 226 | 227 | Update or delete an event: Click on event 228 | to open it. Edit the details and press{' '} 229 | Update. Or Delete the 230 | event entirely. 231 | 232 |
233 | 234 | 235 | Google Calendar 241 | 242 | 243 | Import events from Google Calendar: Need 244 | help, follow the{' '} 245 | 250 | 2-step tutorial 251 | 252 | . 253 |
254 | 260 | 261 |
262 |
263 | 264 | {oicalendarsync} 265 | 266 |
267 |
268 |
269 |
270 | )} 271 | 272 | {!signedIn && ( 273 | 274 | 275 |

Private, Encrypted Agenda in Your Cloud

276 |
277 | 278 | 279 | 280 | Use OI Calendar to keep track of all your events across all 281 | devices. 282 |
283 | {oicalendarsync} 284 | 285 |
286 |
287 | 288 | 289 | Learn about Blockstack! A good starting 290 | point is{' '} 291 | 296 | Blockstack's documentation 297 | 298 | .
299 | 300 | 301 | 307 | 308 | 309 | Start now: Just login using the Blockstack 310 | button above! 311 | 312 |
313 |
314 |
315 | )} 316 | 317 | {eventModal && !inviteSuccess && } 318 | {showSendInvitesModal && } 319 | {showRemindersModal && } 320 | 321 | {calendarView} 322 | {!signedIn && } 323 |
324 | ) 325 | } 326 | } 327 | 328 | OICalendar.propTypes = { 329 | location: PropTypes.object, 330 | public: PropTypes.bool, 331 | showMyPublicCalendar: PropTypes.func, 332 | } 333 | 334 | export default OICalendar 335 | -------------------------------------------------------------------------------- /src/core/chat.js: -------------------------------------------------------------------------------- 1 | import { resolveZoneFileToProfile } from '@stacks/profile' 2 | import { publicKeyToAddress } from '@stacks/transactions' 3 | import { getPublicKeyFromPrivate } from '@stacks/encryption' 4 | 5 | import { BnsApi, Configuration } from '@stacks/blockchain-api-client' 6 | 7 | import { createClient } from 'matrix-js-sdk' 8 | 9 | const config = new Configuration({ 10 | basePath: 'https://stacks-node-api.mainnet.stacks.co', 11 | }) 12 | const bnsApi = new BnsApi({ config }) 13 | export class UserSessionChat { 14 | constructor(selfRoomId, userSession, userOwnedStorage) { 15 | this.selfRoomId = selfRoomId 16 | this.userSession = userSession 17 | this.userOwnedStorage = userOwnedStorage 18 | this.matrixClient = createClient('https://openintents.modular.im') 19 | } 20 | 21 | getOTP(userData) { 22 | const appUserAddress = publicKeyToAddress( 23 | getPublicKeyFromPrivate(userData.appPrivateKey) 24 | ) 25 | var txid = userData.identityAddress + '' + Math.random() 26 | console.log('txid', txid) 27 | return fetch('https://auth.openintents.org/c/' + txid, { method: 'POST' }) 28 | .then( 29 | response => { 30 | return response.json() 31 | }, 32 | error => console.log('error', error) 33 | ) 34 | .then(c => { 35 | const challenge = c.challenge 36 | console.log('challenge', challenge) 37 | return this.userSession 38 | .putFile('mxid.json', challenge, { encrypt: false }) 39 | .then( 40 | () => { 41 | return { 42 | username: appUserAddress.toLowerCase(), 43 | password: 44 | txid + '|' + window.location.origin + '|' + userData.username, 45 | } 46 | }, 47 | error => console.log('err2', error) 48 | ) 49 | }) 50 | } 51 | 52 | setOnMessageListener(onMsgReceived) { 53 | const matrixClient = this.matrixClient 54 | if (onMsgReceived) { 55 | return this.login().then( 56 | () => { 57 | matrixClient.on('Room.timeline', onMsgReceived) 58 | matrixClient.startClient() 59 | console.log('event listeners are setup') 60 | }, 61 | err => { 62 | console.log('login failed', err) 63 | } 64 | ) 65 | } else { 66 | console.log('user id ', matrixClient.getUserId()) 67 | if (matrixClient.getUserId()) { 68 | matrixClient.stopClient() 69 | } 70 | } 71 | } 72 | 73 | createNewRoom(name, topic, guests, ownerIdentityAddress) { 74 | console.log('createNewRoom', { name, topic, guests, ownerIdentityAddress }) 75 | const matrix = this.matrixClient 76 | return this.login().then(() => { 77 | let invitePromises 78 | if (guests) { 79 | invitePromises = guests.map(g => { 80 | return lookupProfile(g).then( 81 | guestProfile => { 82 | return this.addressToAccount(guestProfile.identityAddress) 83 | }, 84 | error => { 85 | console.log('failed to lookup guest', g, error) 86 | } 87 | ) 88 | }) 89 | } else { 90 | invitePromises = [] 91 | } 92 | return Promise.all(invitePromises).then( 93 | invite => { 94 | if (ownerIdentityAddress) { 95 | invite.push(this.addressToAccount(ownerIdentityAddress)) 96 | } 97 | console.log('creating room', { invite }) 98 | return matrix.createRoom({ 99 | visibility: 'private', 100 | name, 101 | topic, 102 | invite, 103 | }) 104 | }, 105 | error => { 106 | console.log('failed to resolve guests ', error) 107 | } 108 | ) 109 | }) 110 | } 111 | 112 | sendMessage(receiverName, roomId, content) { 113 | return lookupProfile(receiverName).then(receiverProfile => { 114 | console.log('receiver', receiverProfile) 115 | const receiverMatrixAccount = this.addressToAccount( 116 | receiverProfile.identityAddress 117 | ) 118 | content.formatted_body = content.formatted_body.replace( 119 | '', 120 | '' + 123 | receiverProfile.identityAddress + 124 | '' 125 | ) 126 | const matrixClient = this.matrixClient 127 | 128 | return this.login().then(() => { 129 | return matrixClient.joinRoom(roomId, {}).then(data => { 130 | console.log('data join', data) 131 | return matrixClient 132 | .invite(roomId, receiverMatrixAccount) 133 | .finally(() => { 134 | if (receiverProfile.appUserAddress) { 135 | return matrixClient 136 | .invite( 137 | roomId, 138 | this.addressToAccount(receiverProfile.appUserAddress) 139 | ) 140 | .finally(res => { 141 | return matrixClient 142 | .sendEvent(roomId, 'm.room.message', content, '') 143 | .then(res => { 144 | console.log('msg sent', res) 145 | return this.storeChatMessage(res, content) 146 | }) 147 | }) 148 | } else { 149 | return matrixClient 150 | .sendEvent(roomId, 'm.room.message', content, '') 151 | .then(res => { 152 | console.log('msg sent', res) 153 | return this.storeChatMessage(res, content) 154 | }) 155 | } 156 | }) 157 | }) 158 | }) 159 | }) 160 | } 161 | 162 | sendMessageToSelf(content) { 163 | const matrixClient = this.matrixClient 164 | return this.login().then(() => { 165 | console.log('logged in') 166 | return this.getSelfRoom().then( 167 | ({ selfRoomId, newlyCreated }) => { 168 | console.log('self room id', selfRoomId) 169 | if (newlyCreated) { 170 | console.log('saving selfRoomId', selfRoomId) 171 | this.userOwnedStorage 172 | .savePreferences({ selfRoomId }) 173 | .finally(() => { 174 | console.log('tried to save') 175 | }) 176 | } 177 | return matrixClient.joinRoom(selfRoomId, {}).then( 178 | data => { 179 | console.log('data join', data) 180 | return matrixClient 181 | .sendEvent(selfRoomId, 'm.room.message', content, '') 182 | .then( 183 | res => { 184 | console.log('msg sent', res) 185 | return this.storeChatMessage(res, content) 186 | }, 187 | error => { 188 | console.log('failed to send', error) 189 | return Promise.reject(error) 190 | } 191 | ) 192 | }, 193 | error => { 194 | console.log('failed to join', error) 195 | return Promise.reject(error) 196 | } 197 | ) 198 | }, 199 | error => { 200 | console.log('failed to get self room', error) 201 | return Promise.reject(error) 202 | } 203 | ) 204 | }) 205 | } 206 | 207 | /** 208 | * Private Methods 209 | **/ 210 | 211 | login() { 212 | if (this.matrixClient.getUserId()) { 213 | return Promise.resolve() 214 | } else { 215 | const userData = this.userSession.loadUserData() 216 | return this.getOTP(userData).then(result => { 217 | var deviceDisplayName = userData.username + ' via OI Calendar' 218 | console.log( 219 | 'login', 220 | deviceDisplayName, 221 | result.username, 222 | result.password 223 | ) 224 | return this.matrixClient.login('m.login.password', { 225 | identifier: { 226 | type: 'm.id.user', 227 | user: result.username, 228 | }, 229 | user: result.username, 230 | password: result.password, 231 | initial_device_display_name: deviceDisplayName, 232 | }) 233 | }) 234 | } 235 | } 236 | 237 | getSelfRoom() { 238 | if (this.selfRoomId) { 239 | return Promise.resolve({ selfRoomId: this.selfRoomId }) 240 | } else { 241 | const userData = this.userSession.loadUserData() 242 | return this.createNewRoom( 243 | 'OI Calendar Reminders', 244 | 'Receive information about events', 245 | null, 246 | userData.identityAddress 247 | ).then(room => { 248 | console.log('Self room', room) 249 | return { selfRoomId: room.room_id, newlyCreated: true } 250 | }) 251 | } 252 | } 253 | 254 | addressToAccount(address) { 255 | // TODO lookup home server for user 256 | return '@' + address.toLowerCase() + ':openintents.modular.im' 257 | } 258 | 259 | storeChatMessage(chatEvent, content) { 260 | return this.userSession.putFile( 261 | 'msg/' + encodeURIComponent(chatEvent.event_id), 262 | JSON.stringify({ chatEvent, content }) 263 | ) 264 | } 265 | } 266 | 267 | export function lookupProfile(username) { 268 | if (!username) { 269 | return Promise.reject(new Error('Invalid username')) 270 | } 271 | console.log('username', username) 272 | let lookupPromise = bnsApi.getNameInfo({ name: username }) 273 | return lookupPromise.then( 274 | responseJSON => { 275 | if ( 276 | responseJSON.hasOwnProperty('zonefile') && 277 | responseJSON.hasOwnProperty('address') 278 | ) { 279 | let profile = {} 280 | profile.identityAddress = responseJSON.address 281 | profile.username = username 282 | return resolveZoneFileToProfile( 283 | responseJSON.zonefile, 284 | responseJSON.address 285 | ).then(pr => { 286 | console.log('pr', pr) 287 | if (pr.apps[window.location.origin]) { 288 | const gaiaUrl = pr.apps[window.location.origin] 289 | const urlParts = gaiaUrl.split('/') 290 | profile.appUserAddress = urlParts[urlParts.length - 2] 291 | } 292 | return profile 293 | }) 294 | } else { 295 | Promise.reject( 296 | new Error( 297 | 'Invalid zonefile lookup response: did not contain `address`' + 298 | ' or `zonefile` field' 299 | ) 300 | ) 301 | } 302 | }, 303 | error => { 304 | return Promise.reject( 305 | new Error('Failed to read profile for ' + username + ' ' + error) 306 | ) 307 | } 308 | ) 309 | } 310 | 311 | export function createSessionChat(selfRoomId, userSession, userOwnedStorage) { 312 | return new UserSessionChat(selfRoomId, userSession, userOwnedStorage) 313 | } 314 | --------------------------------------------------------------------------------