There is nobody on the guest list..
79 | }
80 |
81 | return 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 |
68 |
69 |
76 | Events
77 |
78 |
79 |
80 |
89 | Public
90 |
91 |
92 |
93 |
94 | Settings
95 |
96 |
97 |
98 | {
103 | e.preventDefault()
104 | window.open(
105 | 'https://app-center.openintents.org/appco/1062/review',
106 | '_blank',
107 | 'noopener'
108 | )
109 | }}
110 | >
111 | Rate App!
112 |
113 |
114 |
115 |
116 | Files
117 |
118 |
119 |
120 |
121 | Help
122 |
123 |
124 |
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 |
78 | sendInvites(currentEvent, profiles, currentEventType)
79 | }
80 | >
81 | Send
82 |
83 |
85 | handleInvitesHide(inviteError, currentEvent, currentEventType)
86 | }
87 | >
88 | Close
89 |
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 |
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 |
76 | {image}
77 | {signOutBtnText}
78 |
79 | )
80 | }
81 |
82 | const signInInlineStyle = Object.assign({}, defaultStyle, style, signInStyle)
83 | return (
84 |
85 | {image}
86 | {signInBtnText}
87 |
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 |
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 |
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 |
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 |
162 | Add
163 |
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 |
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 |
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 |
96 |
97 |
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 |
122 |
123 |
124 |
125 |
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 |
141 | Enjoy YOUR calendar!
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 | showSettingsAddCalendar()}>
142 | Go to settings
143 |
144 | or
145 | Hide this message
146 |
147 |
148 |
149 | )}
150 |
151 | )
152 |
153 | return (
154 |
155 | {eventModal &&
}
156 | {(myPublicCalendar || publicCalendar) && (
157 |
158 |
159 | Public Calendar {myPublicCalendar}
160 | {publicCalendar}
161 |
166 | ×
167 | Close
168 |
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 | showSettingsAddCalendar()}>
183 | Go to settings
184 |
185 | or
186 | Hide this message
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 |
213 | ×
214 | Close
215 |
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 |
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 |
--------------------------------------------------------------------------------