Merge pull request #3416 from zbettenbuk/calendar-invite
[RN] Add calendar invite
This commit is contained in:
commit
37ff77cd5b
|
@ -648,6 +648,8 @@
|
||||||
},
|
},
|
||||||
"calendarSync": {
|
"calendarSync": {
|
||||||
"addMeetingURL": "Add a meeting link",
|
"addMeetingURL": "Add a meeting link",
|
||||||
|
"confirmAddLink": "Do you want to add a Jitsi link to this event?",
|
||||||
|
"confirmAddLinkTitle": "Calendar",
|
||||||
"join": "Join",
|
"join": "Join",
|
||||||
"joinTooltip": "Join the meeting",
|
"joinTooltip": "Join the meeting",
|
||||||
"nextMeeting": "next meeting",
|
"nextMeeting": "next meeting",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Linking } from 'react-native';
|
||||||
|
|
||||||
import '../../analytics';
|
import '../../analytics';
|
||||||
import '../../authentication';
|
import '../../authentication';
|
||||||
|
import { DialogContainer } from '../../base/dialog';
|
||||||
import '../../base/jwt';
|
import '../../base/jwt';
|
||||||
import { Platform } from '../../base/react';
|
import { Platform } from '../../base/react';
|
||||||
import {
|
import {
|
||||||
|
@ -180,6 +181,17 @@ export class App extends AbstractApp {
|
||||||
_onLinkingURL({ url }) {
|
_onLinkingURL({ url }) {
|
||||||
super._openURL(url);
|
super._openURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the platform specific dialog container.
|
||||||
|
*
|
||||||
|
* @returns {React$Element}
|
||||||
|
*/
|
||||||
|
_renderDialogContainer() {
|
||||||
|
return (
|
||||||
|
<DialogContainer />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { DialogContainer } from '../../base/dialog';
|
||||||
import '../../base/responsive-ui';
|
import '../../base/responsive-ui';
|
||||||
import '../../chat';
|
import '../../chat';
|
||||||
import '../../room-lock';
|
import '../../room-lock';
|
||||||
|
@ -39,4 +40,17 @@ export class App extends AbstractApp {
|
||||||
</AtlasKitThemeProvider>
|
</AtlasKitThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the platform specific dialog container.
|
||||||
|
*
|
||||||
|
* @returns {React$Element}
|
||||||
|
*/
|
||||||
|
_renderDialogContainer() {
|
||||||
|
return (
|
||||||
|
<AtlasKitThemeProvider mode = 'dark'>
|
||||||
|
<DialogContainer />
|
||||||
|
</AtlasKitThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,6 +127,7 @@ export default class BaseApp extends Component<*, State> {
|
||||||
{ this._createMainElement(component) }
|
{ this._createMainElement(component) }
|
||||||
<SoundCollection />
|
<SoundCollection />
|
||||||
{ this._createExtraElement() }
|
{ this._createExtraElement() }
|
||||||
|
{ this._renderDialogContainer() }
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</Provider>
|
</Provider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
|
@ -235,4 +236,11 @@ export default class BaseApp extends Component<*, State> {
|
||||||
this.setState({ route }, resolve);
|
this.setState({ route }, resolve);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the platform specific dialog container.
|
||||||
|
*
|
||||||
|
* @returns {React$Element}
|
||||||
|
*/
|
||||||
|
_renderDialogContainer: () => React$Element<*>
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,12 @@ export class DialogContainer extends Component {
|
||||||
/**
|
/**
|
||||||
* The props to pass to the component that will be rendered.
|
* The props to pass to the component that will be rendered.
|
||||||
*/
|
*/
|
||||||
_componentProps: PropTypes.object
|
_componentProps: PropTypes.object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the UI is in a compact state where we don't show dialogs.
|
||||||
|
*/
|
||||||
|
_reducedUI: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,10 +37,13 @@ export class DialogContainer extends Component {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { _component: component } = this.props;
|
const {
|
||||||
|
_component: component,
|
||||||
|
_reducedUI: reducedUI
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
component
|
component && !reducedUI
|
||||||
? React.createElement(component, this.props._componentProps)
|
? React.createElement(component, this.props._componentProps)
|
||||||
: null);
|
: null);
|
||||||
}
|
}
|
||||||
|
@ -49,15 +57,18 @@ export class DialogContainer extends Component {
|
||||||
* @private
|
* @private
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* _component: React.Component,
|
* _component: React.Component,
|
||||||
* _componentProps: Object
|
* _componentProps: Object,
|
||||||
|
* _reducedUI: boolean
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state) {
|
function _mapStateToProps(state) {
|
||||||
const stateFeaturesBaseDialog = state['features/base/dialog'];
|
const stateFeaturesBaseDialog = state['features/base/dialog'];
|
||||||
|
const { reducedUI } = state['features/base/responsive-ui'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_component: stateFeaturesBaseDialog.component,
|
_component: stateFeaturesBaseDialog.component,
|
||||||
_componentProps: stateFeaturesBaseDialog.componentProps
|
_componentProps: stateFeaturesBaseDialog.componentProps,
|
||||||
|
_reducedUI: reducedUI
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
|
||||||
|
import { dialog as styles } from './styles';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Children of the component.
|
||||||
|
*/
|
||||||
|
children: string | React$Node
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic dialog content container to provide the same styling for all custom
|
||||||
|
* dialogs.
|
||||||
|
*/
|
||||||
|
export default class DialogContent extends Component<Props> {
|
||||||
|
/**
|
||||||
|
* Implements {@code Component#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { children } = this.props;
|
||||||
|
|
||||||
|
const childrenComponent = typeof children === 'string'
|
||||||
|
? <Text>{ children }</Text>
|
||||||
|
: children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style = { styles.dialogContainer }>
|
||||||
|
{ childrenComponent }
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
export { default as BottomSheet } from './BottomSheet';
|
export { default as BottomSheet } from './BottomSheet';
|
||||||
export { default as DialogContainer } from './DialogContainer';
|
|
||||||
export { default as Dialog } from './Dialog';
|
export { default as Dialog } from './Dialog';
|
||||||
|
export { default as DialogContainer } from './DialogContainer';
|
||||||
|
export { default as DialogContent } from './DialogContent';
|
||||||
export { default as StatelessDialog } from './StatelessDialog';
|
export { default as StatelessDialog } from './StatelessDialog';
|
||||||
export { default as DialogWithTabs } from './DialogWithTabs';
|
export { default as DialogWithTabs } from './DialogWithTabs';
|
||||||
export { default as AbstractDialogTab } from './AbstractDialogTab';
|
export { default as AbstractDialogTab } from './AbstractDialogTab';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
import { ColorPalette, createStyleSheet } from '../../styles';
|
import { BoxModel, ColorPalette, createStyleSheet } from '../../styles';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The React {@code Component} styles of {@code Dialog}.
|
* The React {@code Component} styles of {@code Dialog}.
|
||||||
|
@ -13,6 +13,14 @@ export const dialog = createStyleSheet({
|
||||||
color: ColorPalette.blue
|
color: ColorPalette.blue
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified container for a consistent Dialog style.
|
||||||
|
*/
|
||||||
|
dialogContainer: {
|
||||||
|
paddingHorizontal: BoxModel.padding,
|
||||||
|
paddingVertical: 1.5 * BoxModel.padding
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The style of the {@code Text} in a {@code Dialog} button which is
|
* The style of the {@code Text} in a {@code Dialog} button which is
|
||||||
* disabled.
|
* disabled.
|
||||||
|
|
|
@ -29,6 +29,12 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
onRefresh: Function,
|
onRefresh: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be invoked when a secondary action is performed on an item.
|
||||||
|
* The item's ID is passed.
|
||||||
|
*/
|
||||||
|
onSecondaryAction: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to override the rendered default empty list component.
|
* Function to override the rendered default empty list component.
|
||||||
*/
|
*/
|
||||||
|
@ -153,6 +159,23 @@ class NavigateSectionList extends Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onSecondaryAction: Object => Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function that is used in the secondaryAction callback of the
|
||||||
|
* items.
|
||||||
|
*
|
||||||
|
* @param {string} id - The id of the item that secondary action was
|
||||||
|
* performed on.
|
||||||
|
* @private
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
_onSecondaryAction(id) {
|
||||||
|
return () => {
|
||||||
|
this.props.onSecondaryAction(id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
_renderItem: Object => Object;
|
_renderItem: Object => Object;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -165,7 +188,7 @@ class NavigateSectionList extends Component<Props> {
|
||||||
*/
|
*/
|
||||||
_renderItem(listItem, key: string = '') {
|
_renderItem(listItem, key: string = '') {
|
||||||
const { item } = listItem;
|
const { item } = listItem;
|
||||||
const { url } = item;
|
const { id, url } = item;
|
||||||
|
|
||||||
// XXX The value of title cannot be undefined; otherwise, react-native
|
// XXX The value of title cannot be undefined; otherwise, react-native
|
||||||
// will throw a TypeError: Cannot read property of undefined. While it's
|
// will throw a TypeError: Cannot read property of undefined. While it's
|
||||||
|
@ -180,7 +203,9 @@ class NavigateSectionList extends Component<Props> {
|
||||||
<NavigateSectionListItem
|
<NavigateSectionListItem
|
||||||
item = { item }
|
item = { item }
|
||||||
key = { key }
|
key = { key }
|
||||||
onPress = { this._onPress(url) } />
|
onPress = { url ? this._onPress(url) : undefined }
|
||||||
|
secondaryAction = {
|
||||||
|
url ? undefined : this._onSecondaryAction(id) } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,12 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* Function to be invoked when an Item is pressed. The Item's URL is passed.
|
* Function to be invoked when an Item is pressed. The Item's URL is passed.
|
||||||
*/
|
*/
|
||||||
onPress: Function
|
onPress: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be invoked when secondary action was performed on an Item.
|
||||||
|
*/
|
||||||
|
secondaryAction: ?Function
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,6 +105,24 @@ export default class NavigateSectionListItem extends Component<Props> {
|
||||||
return lines && lines.length ? lines.map(this._renderItemLine) : null;
|
return lines && lines.length ? lines.map(this._renderItemLine) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the secondary action label.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {React$Node}
|
||||||
|
*/
|
||||||
|
_renderSecondaryAction() {
|
||||||
|
const { secondaryAction } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
onClick = { secondaryAction }
|
||||||
|
style = { styles.secondaryActionContainer }>
|
||||||
|
<Text style = { styles.secondaryActionLabel }>+</Text>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the content of this component.
|
* Renders the content of this component.
|
||||||
*
|
*
|
||||||
|
@ -135,6 +158,7 @@ export default class NavigateSectionListItem extends Component<Props> {
|
||||||
</Text>
|
</Text>
|
||||||
{this._renderItemLines(lines)}
|
{this._renderItemLines(lines)}
|
||||||
</Container>
|
</Container>
|
||||||
|
{ this.props.secondaryAction && this._renderSecondaryAction() }
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ const HEADER_COLOR = ColorPalette.blue;
|
||||||
// Header height is from Android guidelines. Also, this looks good.
|
// Header height is from Android guidelines. Also, this looks good.
|
||||||
const HEADER_HEIGHT = 56;
|
const HEADER_HEIGHT = 56;
|
||||||
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
|
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
|
||||||
|
const SECONDARY_ACTION_BUTTON_SIZE = 30;
|
||||||
|
|
||||||
export const HEADER_PADDING = BoxModel.padding;
|
export const HEADER_PADDING = BoxModel.padding;
|
||||||
export const STATUSBAR_COLOR = ColorPalette.blueHighlight;
|
export const STATUSBAR_COLOR = ColorPalette.blueHighlight;
|
||||||
|
@ -266,6 +267,21 @@ const SECTION_LIST_STYLES = {
|
||||||
color: OVERLAY_FONT_COLOR
|
color: OVERLAY_FONT_COLOR
|
||||||
},
|
},
|
||||||
|
|
||||||
|
secondaryActionContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: ColorPalette.blue,
|
||||||
|
borderRadius: 3,
|
||||||
|
height: SECONDARY_ACTION_BUTTON_SIZE,
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: BoxModel.margin * 0.5,
|
||||||
|
marginRight: BoxModel.margin,
|
||||||
|
width: SECONDARY_ACTION_BUTTON_SIZE
|
||||||
|
},
|
||||||
|
|
||||||
|
secondaryActionLabel: {
|
||||||
|
color: ColorPalette.white
|
||||||
|
},
|
||||||
|
|
||||||
touchableView: {
|
touchableView: {
|
||||||
flexDirection: 'row'
|
flexDirection: 'row'
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
REFRESH_CALENDAR,
|
||||||
|
SET_CALENDAR_AUTHORIZATION,
|
||||||
|
SET_CALENDAR_EVENTS
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an action to refresh the entry list (fetches new data).
|
||||||
|
*
|
||||||
|
* @param {boolean} forcePermission - Whether to force to re-ask for
|
||||||
|
* the permission or not.
|
||||||
|
* @param {boolean} isInteractive - If true this refresh was caused by
|
||||||
|
* direct user interaction, false otherwise.
|
||||||
|
* @returns {{
|
||||||
|
* type: REFRESH_CALENDAR,
|
||||||
|
* forcePermission: boolean,
|
||||||
|
* isInteractive: boolean
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function refreshCalendar(
|
||||||
|
forcePermission: boolean = false, isInteractive: boolean = true) {
|
||||||
|
return {
|
||||||
|
type: REFRESH_CALENDAR,
|
||||||
|
forcePermission,
|
||||||
|
isInteractive
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an action to signal that a calendar access has been requested. For more
|
||||||
|
* info, see {@link SET_CALENDAR_AUTHORIZATION}.
|
||||||
|
*
|
||||||
|
* @param {string | undefined} authorization - The result of the last calendar
|
||||||
|
* authorization request.
|
||||||
|
* @returns {{
|
||||||
|
* type: SET_CALENDAR_AUTHORIZATION,
|
||||||
|
* authorization: ?string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function setCalendarAuthorization(authorization: ?string) {
|
||||||
|
return {
|
||||||
|
type: SET_CALENDAR_AUTHORIZATION,
|
||||||
|
authorization
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an action to update the current calendar list in redux.
|
||||||
|
*
|
||||||
|
* @param {Array<Object>} events - The new list.
|
||||||
|
* @returns {{
|
||||||
|
* type: SET_CALENDAR_EVENTS,
|
||||||
|
* events: Array<Object>
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function setCalendarEvents(events: Array<Object>) {
|
||||||
|
return {
|
||||||
|
type: SET_CALENDAR_EVENTS,
|
||||||
|
events
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { getDefaultURL } from '../app';
|
||||||
|
import { openDialog } from '../base/dialog';
|
||||||
|
import { generateRoomWithoutSeparator } from '../welcome';
|
||||||
|
|
||||||
|
import { refreshCalendar } from './actions';
|
||||||
|
import { addLinkToCalendarEntry } from './functions.native';
|
||||||
|
|
||||||
|
import {
|
||||||
|
UpdateCalendarEventDialog
|
||||||
|
} from './components';
|
||||||
|
|
||||||
|
export * from './actions.any';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks confirmation from the user to add a Jitsi link to the calendar event.
|
||||||
|
*
|
||||||
|
* @param {string} eventId - The event id.
|
||||||
|
* @returns {{
|
||||||
|
* type: OPEN_DIALOG,
|
||||||
|
* component: React.Component,
|
||||||
|
* componentProps: (Object | undefined)
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function openUpdateCalendarEventDialog(eventId: string) {
|
||||||
|
return openDialog(UpdateCalendarEventDialog, { eventId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates calendar event by generating new invite URL and editing the event
|
||||||
|
* adding some descriptive text and location.
|
||||||
|
*
|
||||||
|
* @param {string} eventId - The event id.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function updateCalendarEvent(eventId: string) {
|
||||||
|
return (dispatch: Dispatch<*>, getState: Function) => {
|
||||||
|
const defaultUrl = getDefaultURL(getState);
|
||||||
|
const roomName = generateRoomWithoutSeparator();
|
||||||
|
|
||||||
|
addLinkToCalendarEntry(getState(), eventId, `${defaultUrl}/${roomName}`)
|
||||||
|
.finally(() => {
|
||||||
|
dispatch(refreshCalendar(false, false));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,14 +2,12 @@
|
||||||
|
|
||||||
import { loadGoogleAPI } from '../google-api';
|
import { loadGoogleAPI } from '../google-api';
|
||||||
|
|
||||||
|
import { refreshCalendar, setCalendarEvents } from './actions';
|
||||||
import { createCalendarConnectedEvent, sendAnalytics } from '../analytics';
|
import { createCalendarConnectedEvent, sendAnalytics } from '../analytics';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CLEAR_CALENDAR_INTEGRATION,
|
CLEAR_CALENDAR_INTEGRATION,
|
||||||
REFRESH_CALENDAR,
|
|
||||||
SET_CALENDAR_AUTH_STATE,
|
SET_CALENDAR_AUTH_STATE,
|
||||||
SET_CALENDAR_AUTHORIZATION,
|
|
||||||
SET_CALENDAR_EVENTS,
|
|
||||||
SET_CALENDAR_INTEGRATION,
|
SET_CALENDAR_INTEGRATION,
|
||||||
SET_CALENDAR_PROFILE_EMAIL,
|
SET_CALENDAR_PROFILE_EMAIL,
|
||||||
SET_LOADING_CALENDAR_EVENTS
|
SET_LOADING_CALENDAR_EVENTS
|
||||||
|
@ -17,6 +15,8 @@ import {
|
||||||
import { _getCalendarIntegration, isCalendarEnabled } from './functions';
|
import { _getCalendarIntegration, isCalendarEnabled } from './functions';
|
||||||
import { generateRoomWithoutSeparator } from '../welcome';
|
import { generateRoomWithoutSeparator } from '../welcome';
|
||||||
|
|
||||||
|
export * from './actions.any';
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,25 +88,18 @@ export function clearCalendarIntegration() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an action to refresh the entry list (fetches new data).
|
* Asks confirmation from the user to add a Jitsi link to the calendar event.
|
||||||
*
|
*
|
||||||
* @param {boolean} forcePermission - Whether to force to re-ask for
|
* NOTE: Currently there is no confirmation prompted on web, so this is just
|
||||||
* the permission or not.
|
* a relaying method to avoid flow problems.
|
||||||
* @param {boolean} isInteractive - If true this refresh was caused by
|
*
|
||||||
* direct user interaction, false otherwise.
|
* @param {string} eventId - The event id.
|
||||||
* @returns {{
|
* @param {string} calendarId - The calendar id.
|
||||||
* type: REFRESH_CALENDAR,
|
* @returns {Function}
|
||||||
* forcePermission: boolean,
|
|
||||||
* isInteractive: boolean
|
|
||||||
* }}
|
|
||||||
*/
|
*/
|
||||||
export function refreshCalendar(
|
export function openUpdateCalendarEventDialog(
|
||||||
forcePermission: boolean = false, isInteractive: boolean = true) {
|
eventId: string, calendarId: string) {
|
||||||
return {
|
return updateCalendarEvent(eventId, calendarId);
|
||||||
type: REFRESH_CALENDAR,
|
|
||||||
forcePermission,
|
|
||||||
isInteractive
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,40 +119,6 @@ export function setCalendarAPIAuthState(newState: ?Object) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an action to signal that a calendar access has been requested. For more
|
|
||||||
* info, see {@link SET_CALENDAR_AUTHORIZATION}.
|
|
||||||
*
|
|
||||||
* @param {string | undefined} authorization - The result of the last calendar
|
|
||||||
* authorization request.
|
|
||||||
* @returns {{
|
|
||||||
* type: SET_CALENDAR_AUTHORIZATION,
|
|
||||||
* authorization: ?string
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
export function setCalendarAuthorization(authorization: ?string) {
|
|
||||||
return {
|
|
||||||
type: SET_CALENDAR_AUTHORIZATION,
|
|
||||||
authorization
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an action to update the current calendar list in redux.
|
|
||||||
*
|
|
||||||
* @param {Array<Object>} events - The new list.
|
|
||||||
* @returns {{
|
|
||||||
* type: SET_CALENDAR_EVENTS,
|
|
||||||
* events: Array<Object>
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
export function setCalendarEvents(events: Array<Object>) {
|
|
||||||
return {
|
|
||||||
type: SET_CALENDAR_EVENTS,
|
|
||||||
events
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an action to update the current calendar profile email state in redux.
|
* Sends an action to update the current calendar profile email state in redux.
|
||||||
*
|
*
|
||||||
|
@ -243,29 +202,6 @@ export function signIn(calendarType: string): Function {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Signals to get current profile data linked to the current calendar
|
|
||||||
* integration that is in use.
|
|
||||||
*
|
|
||||||
* @param {string} calendarType - The calendar integration to which the profile
|
|
||||||
* should be updated.
|
|
||||||
* @returns {Function}
|
|
||||||
*/
|
|
||||||
export function updateProfile(calendarType: string): Function {
|
|
||||||
return (dispatch: Dispatch<*>) => {
|
|
||||||
const integration = _getCalendarIntegration(calendarType);
|
|
||||||
|
|
||||||
if (!integration) {
|
|
||||||
return Promise.reject('No integration found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return dispatch(integration.getCurrentEmail())
|
|
||||||
.then(email => {
|
|
||||||
dispatch(setCalendarProfileEmail(email));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates calendar event by generating new invite URL and editing the event
|
* Updates calendar event by generating new invite URL and editing the event
|
||||||
* adding some descriptive text and location.
|
* adding some descriptive text and location.
|
||||||
|
@ -312,3 +248,26 @@ export function updateCalendarEvent(id: string, calendarId: string): Function {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals to get current profile data linked to the current calendar
|
||||||
|
* integration that is in use.
|
||||||
|
*
|
||||||
|
* @param {string} calendarType - The calendar integration to which the profile
|
||||||
|
* should be updated.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function updateProfile(calendarType: string): Function {
|
||||||
|
return (dispatch: Dispatch<*>) => {
|
||||||
|
const integration = _getCalendarIntegration(calendarType);
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
return Promise.reject('No integration found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispatch(integration.getCurrentEmail())
|
||||||
|
.then(email => {
|
||||||
|
dispatch(setCalendarProfileEmail(email));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import {
|
||||||
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
|
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
|
||||||
import { NavigateSectionList } from '../../base/react';
|
import { NavigateSectionList } from '../../base/react';
|
||||||
|
|
||||||
import { refreshCalendar } from '../actions';
|
import { refreshCalendar, openUpdateCalendarEventDialog } from '../actions';
|
||||||
|
|
||||||
import { isCalendarEnabled } from '../functions';
|
import { isCalendarEnabled } from '../functions';
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ class BaseCalendarList extends Component<Props> {
|
||||||
this._onJoinPress = this._onJoinPress.bind(this);
|
this._onJoinPress = this._onJoinPress.bind(this);
|
||||||
this._onPress = this._onPress.bind(this);
|
this._onPress = this._onPress.bind(this);
|
||||||
this._onRefresh = this._onRefresh.bind(this);
|
this._onRefresh = this._onRefresh.bind(this);
|
||||||
|
this._onSecondaryAction = this._onSecondaryAction.bind(this);
|
||||||
this._toDateString = this._toDateString.bind(this);
|
this._toDateString = this._toDateString.bind(this);
|
||||||
this._toDisplayableItem = this._toDisplayableItem.bind(this);
|
this._toDisplayableItem = this._toDisplayableItem.bind(this);
|
||||||
this._toDisplayableList = this._toDisplayableList.bind(this);
|
this._toDisplayableList = this._toDisplayableList.bind(this);
|
||||||
|
@ -123,6 +124,7 @@ class BaseCalendarList extends Component<Props> {
|
||||||
disabled = { disabled }
|
disabled = { disabled }
|
||||||
onPress = { this._onPress }
|
onPress = { this._onPress }
|
||||||
onRefresh = { this._onRefresh }
|
onRefresh = { this._onRefresh }
|
||||||
|
onSecondaryAction = { this._onSecondaryAction }
|
||||||
renderListEmptyComponent
|
renderListEmptyComponent
|
||||||
= { renderListEmptyComponent }
|
= { renderListEmptyComponent }
|
||||||
sections = { this._toDisplayableList() } />
|
sections = { this._toDisplayableList() } />
|
||||||
|
@ -174,6 +176,20 @@ class BaseCalendarList extends Component<Props> {
|
||||||
this.props.dispatch(refreshCalendar(true));
|
this.props.dispatch(refreshCalendar(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onSecondaryAction: string => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the list's secondary action.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} id - The ID of the item on which the secondary action was
|
||||||
|
* performed.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onSecondaryAction(id) {
|
||||||
|
this.props.dispatch(openUpdateCalendarEventDialog(id, ''));
|
||||||
|
}
|
||||||
|
|
||||||
_toDateString: Object => string;
|
_toDateString: Object => string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -208,6 +224,7 @@ class BaseCalendarList extends Component<Props> {
|
||||||
: (<AddMeetingUrlButton
|
: (<AddMeetingUrlButton
|
||||||
calendarId = { event.calendarId }
|
calendarId = { event.calendarId }
|
||||||
eventId = { event.id } />),
|
eventId = { event.id } />),
|
||||||
|
id: event.id,
|
||||||
key: `${event.id}-${event.startDate}`,
|
key: `${event.id}-${event.startDate}`,
|
||||||
lines: [
|
lines: [
|
||||||
event.url,
|
event.url,
|
||||||
|
|
|
@ -237,9 +237,10 @@ class ConferenceNotification extends Component<Props, State> {
|
||||||
|
|
||||||
for (const event of _eventList) {
|
for (const event of _eventList) {
|
||||||
const eventUrl
|
const eventUrl
|
||||||
= getURLWithoutParamsNormalized(new URL(event.url));
|
= event.url
|
||||||
|
&& getURLWithoutParamsNormalized(new URL(event.url));
|
||||||
|
|
||||||
if (eventUrl !== _currentConferenceURL) {
|
if (eventUrl && eventUrl !== _currentConferenceURL) {
|
||||||
if ((!eventToShow
|
if ((!eventToShow
|
||||||
&& event.startDate > now
|
&& event.startDate > now
|
||||||
&& event.startDate < now + ALERT_MILLISECONDS)
|
&& event.startDate < now + ALERT_MILLISECONDS)
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent } from '../../base/dialog';
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
|
||||||
|
import { updateCalendarEvent } from '../actions';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redux dispatch function.
|
||||||
|
*/
|
||||||
|
dispatch: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the event to be updated.
|
||||||
|
*/
|
||||||
|
eventId: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to translate i18n labels.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for the add Jitsi link confirm dialog.
|
||||||
|
*/
|
||||||
|
class UpdateCalendarEventDialog extends Component<Props> {
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code UpdateCalendarEventDialog} instance.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._onSubmit = this._onSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
okTitleKey = 'dialog.confirm'
|
||||||
|
onSubmit = { this._onSubmit }
|
||||||
|
titleKey = 'calendarSync.confirmAddLinkTitle'
|
||||||
|
width = 'small'>
|
||||||
|
<DialogContent>
|
||||||
|
{ this.props.t('calendarSync.confirmAddLink') }
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSubmit: () => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for the confirm button.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {boolean} - True (to note that the modal should be closed).
|
||||||
|
*/
|
||||||
|
_onSubmit() {
|
||||||
|
this.props.dispatch(updateCalendarEvent(this.props.eventId, ''));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect()(UpdateCalendarEventDialog));
|
|
@ -1,3 +1,6 @@
|
||||||
export { default as ConferenceNotification } from './ConferenceNotification';
|
export { default as ConferenceNotification } from './ConferenceNotification';
|
||||||
export { default as CalendarList } from './CalendarList';
|
export { default as CalendarList } from './CalendarList';
|
||||||
export { default as MicrosoftSignInButton } from './MicrosoftSignInButton';
|
export { default as MicrosoftSignInButton } from './MicrosoftSignInButton';
|
||||||
|
export {
|
||||||
|
default as UpdateCalendarEventDialog
|
||||||
|
} from './UpdateCalendarEventDialog';
|
||||||
|
|
|
@ -89,40 +89,35 @@ export function _updateCalendarEntries(events: Array<Object>) {
|
||||||
function _parseCalendarEntry(event, knownDomains) {
|
function _parseCalendarEntry(event, knownDomains) {
|
||||||
if (event) {
|
if (event) {
|
||||||
const url = _getURLFromEvent(event, knownDomains);
|
const url = _getURLFromEvent(event, knownDomains);
|
||||||
|
const startDate = Date.parse(event.startDate);
|
||||||
|
const endDate = Date.parse(event.endDate);
|
||||||
|
|
||||||
// we only filter events without url on mobile, this is temporary
|
// we want to hide all events that
|
||||||
// till we implement event edit on mobile
|
// - has no start or end date
|
||||||
if (url || navigator.product !== 'ReactNative') {
|
// - for web, if there is no url and we cannot edit the event (has
|
||||||
const startDate = Date.parse(event.startDate);
|
// no calendarId)
|
||||||
const endDate = Date.parse(event.endDate);
|
if (isNaN(startDate)
|
||||||
|
|| isNaN(endDate)
|
||||||
// we want to hide all events that
|
|| (navigator.product !== 'ReactNative'
|
||||||
// - has no start or end date
|
&& !url
|
||||||
// - for web, if there is no url and we cannot edit the event (has
|
&& !event.calendarId)) {
|
||||||
// no calendarId)
|
logger.debug(
|
||||||
if (isNaN(startDate)
|
'Skipping invalid calendar event',
|
||||||
|| isNaN(endDate)
|
event.title,
|
||||||
|| (navigator.product !== 'ReactNative'
|
event.startDate,
|
||||||
&& !url
|
event.endDate,
|
||||||
&& !event.calendarId)) {
|
url,
|
||||||
logger.debug(
|
event.calendarId
|
||||||
'Skipping invalid calendar event',
|
);
|
||||||
event.title,
|
} else {
|
||||||
event.startDate,
|
return {
|
||||||
event.endDate,
|
calendarId: event.calendarId,
|
||||||
url,
|
endDate,
|
||||||
event.calendarId
|
id: event.id,
|
||||||
);
|
startDate,
|
||||||
} else {
|
title: event.title,
|
||||||
return {
|
url
|
||||||
calendarId: event.calendarId,
|
};
|
||||||
endDate,
|
|
||||||
id: event.id,
|
|
||||||
startDate,
|
|
||||||
title: event.title,
|
|
||||||
url
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { NativeModules } from 'react-native';
|
// @flow
|
||||||
|
|
||||||
|
import { NativeModules, Platform } from 'react-native';
|
||||||
import RNCalendarEvents from 'react-native-calendar-events';
|
import RNCalendarEvents from 'react-native-calendar-events';
|
||||||
|
|
||||||
|
import { getShareInfoText } from '../invite';
|
||||||
|
|
||||||
import { setCalendarAuthorization } from './actions';
|
import { setCalendarAuthorization } from './actions';
|
||||||
import { FETCH_END_DAYS, FETCH_START_DAYS } from './constants';
|
import { FETCH_END_DAYS, FETCH_START_DAYS } from './constants';
|
||||||
import { _updateCalendarEntries } from './functions';
|
import { _updateCalendarEntries } from './functions';
|
||||||
|
@ -9,6 +13,39 @@ export * from './functions.any';
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a Jitsi link to a calendar entry.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @param {string} id - The ID of the calendar entry.
|
||||||
|
* @param {string} link - The link to add info with.
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
export function addLinkToCalendarEntry(
|
||||||
|
state: Object, id: string, link: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getShareInfoText(state, link, true).then(shareInfoText => {
|
||||||
|
RNCalendarEvents.findEventById(id).then(event => {
|
||||||
|
const updateText = `${event.description}\n\n${shareInfoText}`;
|
||||||
|
const updateObject = {
|
||||||
|
id: event.id,
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
notes: updateText
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
description: updateText
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
RNCalendarEvents.saveEvent(event.title, updateObject)
|
||||||
|
.then(resolve, reject);
|
||||||
|
}, reject);
|
||||||
|
}, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the calendar feature is enabled by the app. For
|
* Determines whether the calendar feature is enabled by the app. For
|
||||||
* example, Apple through its App Store requires
|
* example, Apple through its App Store requires
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { connect as reactReduxConnect } from 'react-redux';
|
||||||
|
|
||||||
import { appNavigate } from '../../app';
|
import { appNavigate } from '../../app';
|
||||||
import { connect, disconnect } from '../../base/connection';
|
import { connect, disconnect } from '../../base/connection';
|
||||||
import { DialogContainer } from '../../base/dialog';
|
|
||||||
import { getParticipantCount } from '../../base/participants';
|
import { getParticipantCount } from '../../base/participants';
|
||||||
import { Container, LoadingIndicator, TintedView } from '../../base/react';
|
import { Container, LoadingIndicator, TintedView } from '../../base/react';
|
||||||
import {
|
import {
|
||||||
|
@ -315,11 +314,7 @@ class Conference extends Component<Props> {
|
||||||
this._renderConferenceNotification()
|
this._renderConferenceNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
{/*
|
<NotificationsContainer />
|
||||||
* The dialogs are in the topmost stacking layers.
|
|
||||||
*/
|
|
||||||
this.props._reducedUI || <DialogContainer />
|
|
||||||
}
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
|
||||||
|
|
||||||
import { obtainConfig } from '../../base/config';
|
import { obtainConfig } from '../../base/config';
|
||||||
import { connect, disconnect } from '../../base/connection';
|
import { connect, disconnect } from '../../base/connection';
|
||||||
import { DialogContainer } from '../../base/dialog';
|
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
import { Filmstrip } from '../../filmstrip';
|
import { Filmstrip } from '../../filmstrip';
|
||||||
import { CalleeInfoContainer } from '../../invite';
|
import { CalleeInfoContainer } from '../../invite';
|
||||||
|
@ -226,7 +225,6 @@ class Conference extends Component<Props> {
|
||||||
{ filmstripOnly || <Toolbox /> }
|
{ filmstripOnly || <Toolbox /> }
|
||||||
{ filmstripOnly || <SidePanel /> }
|
{ filmstripOnly || <SidePanel /> }
|
||||||
|
|
||||||
<DialogContainer />
|
|
||||||
<NotificationsContainer />
|
<NotificationsContainer />
|
||||||
|
|
||||||
<CalleeInfoContainer />
|
<CalleeInfoContainer />
|
||||||
|
|
|
@ -438,7 +438,7 @@ export function getShareInfoText(
|
||||||
|
|
||||||
if (!dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
|
if (!dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
|
||||||
// URLs for fetching dial in numbers not defined
|
// URLs for fetching dial in numbers not defined
|
||||||
return Promise.reject();
|
return Promise.resolve(infoText);
|
||||||
}
|
}
|
||||||
|
|
||||||
numbersPromise = Promise.all([
|
numbersPromise = Promise.all([
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { DialogContainer } from '../../base/dialog';
|
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
import { Platform, Watermarks } from '../../base/react';
|
import { Platform, Watermarks } from '../../base/react';
|
||||||
import { CalendarList } from '../../calendar-sync';
|
import { CalendarList } from '../../calendar-sync';
|
||||||
|
@ -168,9 +167,6 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
ref = { this._setAdditionalContentRef } />
|
ref = { this._setAdditionalContentRef } />
|
||||||
: null }
|
: null }
|
||||||
</div>
|
</div>
|
||||||
<AtlasKitThemeProvider mode = 'dark'>
|
|
||||||
<DialogContainer />
|
|
||||||
</AtlasKitThemeProvider>
|
|
||||||
</AtlasKitThemeProvider>
|
</AtlasKitThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue