diff --git a/lang/main.json b/lang/main.json
index 83cf4f277..277e93ebf 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -648,6 +648,8 @@
},
"calendarSync": {
"addMeetingURL": "Add a meeting link",
+ "confirmAddLink": "Do you want to add a Jitsi link to this event?",
+ "confirmAddLinkTitle": "Calendar",
"join": "Join",
"joinTooltip": "Join the meeting",
"nextMeeting": "next meeting",
diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js
index 358b8b9c1..900652e95 100644
--- a/react/features/app/components/App.native.js
+++ b/react/features/app/components/App.native.js
@@ -5,6 +5,7 @@ import { Linking } from 'react-native';
import '../../analytics';
import '../../authentication';
+import { DialogContainer } from '../../base/dialog';
import '../../base/jwt';
import { Platform } from '../../base/react';
import {
@@ -180,6 +181,17 @@ export class App extends AbstractApp {
_onLinkingURL({ url }) {
super._openURL(url);
}
+
+ /**
+ * Renders the platform specific dialog container.
+ *
+ * @returns {React$Element}
+ */
+ _renderDialogContainer() {
+ return (
+
+ );
+ }
}
/**
diff --git a/react/features/app/components/App.web.js b/react/features/app/components/App.web.js
index b71ccc6ea..5e5bcae7a 100644
--- a/react/features/app/components/App.web.js
+++ b/react/features/app/components/App.web.js
@@ -3,6 +3,7 @@
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React from 'react';
+import { DialogContainer } from '../../base/dialog';
import '../../base/responsive-ui';
import '../../chat';
import '../../room-lock';
@@ -39,4 +40,17 @@ export class App extends AbstractApp {
);
}
+
+ /**
+ * Renders the platform specific dialog container.
+ *
+ * @returns {React$Element}
+ */
+ _renderDialogContainer() {
+ return (
+
+
+
+ );
+ }
}
diff --git a/react/features/base/app/components/BaseApp.js b/react/features/base/app/components/BaseApp.js
index c7f47e972..000ff56a2 100644
--- a/react/features/base/app/components/BaseApp.js
+++ b/react/features/base/app/components/BaseApp.js
@@ -127,6 +127,7 @@ export default class BaseApp extends Component<*, State> {
{ this._createMainElement(component) }
{ this._createExtraElement() }
+ { this._renderDialogContainer() }
@@ -235,4 +236,11 @@ export default class BaseApp extends Component<*, State> {
this.setState({ route }, resolve);
});
}
+
+ /**
+ * Renders the platform specific dialog container.
+ *
+ * @returns {React$Element}
+ */
+ _renderDialogContainer: () => React$Element<*>
}
diff --git a/react/features/base/dialog/components/DialogContainer.js b/react/features/base/dialog/components/DialogContainer.js
index 3ac8f7d52..7648c3a6a 100644
--- a/react/features/base/dialog/components/DialogContainer.js
+++ b/react/features/base/dialog/components/DialogContainer.js
@@ -22,7 +22,12 @@ export class DialogContainer extends Component {
/**
* 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}
*/
render() {
- const { _component: component } = this.props;
+ const {
+ _component: component,
+ _reducedUI: reducedUI
+ } = this.props;
return (
- component
+ component && !reducedUI
? React.createElement(component, this.props._componentProps)
: null);
}
@@ -49,15 +57,18 @@ export class DialogContainer extends Component {
* @private
* @returns {{
* _component: React.Component,
- * _componentProps: Object
+ * _componentProps: Object,
+ * _reducedUI: boolean
* }}
*/
function _mapStateToProps(state) {
const stateFeaturesBaseDialog = state['features/base/dialog'];
+ const { reducedUI } = state['features/base/responsive-ui'];
return {
_component: stateFeaturesBaseDialog.component,
- _componentProps: stateFeaturesBaseDialog.componentProps
+ _componentProps: stateFeaturesBaseDialog.componentProps,
+ _reducedUI: reducedUI
};
}
diff --git a/react/features/base/dialog/components/DialogContent.native.js b/react/features/base/dialog/components/DialogContent.native.js
new file mode 100644
index 000000000..c9da87f76
--- /dev/null
+++ b/react/features/base/dialog/components/DialogContent.native.js
@@ -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 {
+ /**
+ * Implements {@code Component#render}.
+ *
+ * @inheritdoc
+ */
+ render() {
+ const { children } = this.props;
+
+ const childrenComponent = typeof children === 'string'
+ ? { children }
+ : children;
+
+ return (
+
+ { childrenComponent }
+
+ );
+ }
+}
diff --git a/react/features/base/dialog/components/DialogContent.web.js b/react/features/base/dialog/components/DialogContent.web.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/react/features/base/dialog/components/index.js b/react/features/base/dialog/components/index.js
index ce408fcd9..97927b9e1 100644
--- a/react/features/base/dialog/components/index.js
+++ b/react/features/base/dialog/components/index.js
@@ -1,8 +1,9 @@
// @flow
export { default as BottomSheet } from './BottomSheet';
-export { default as DialogContainer } from './DialogContainer';
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 DialogWithTabs } from './DialogWithTabs';
export { default as AbstractDialogTab } from './AbstractDialogTab';
diff --git a/react/features/base/dialog/components/styles.js b/react/features/base/dialog/components/styles.js
index d31e9fe4a..35b0eda61 100644
--- a/react/features/base/dialog/components/styles.js
+++ b/react/features/base/dialog/components/styles.js
@@ -1,6 +1,6 @@
import { StyleSheet } from 'react-native';
-import { ColorPalette, createStyleSheet } from '../../styles';
+import { BoxModel, ColorPalette, createStyleSheet } from '../../styles';
/**
* The React {@code Component} styles of {@code Dialog}.
@@ -13,6 +13,14 @@ export const dialog = createStyleSheet({
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
* disabled.
diff --git a/react/features/base/react/components/NavigateSectionList.js b/react/features/base/react/components/NavigateSectionList.js
index 7b6eeb7f7..2a6858d3f 100644
--- a/react/features/base/react/components/NavigateSectionList.js
+++ b/react/features/base/react/components/NavigateSectionList.js
@@ -29,6 +29,12 @@ type Props = {
*/
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.
*/
@@ -153,6 +159,23 @@ class NavigateSectionList extends Component {
}
}
+ _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;
/**
@@ -165,7 +188,7 @@ class NavigateSectionList extends Component {
*/
_renderItem(listItem, key: string = '') {
const { item } = listItem;
- const { url } = item;
+ const { id, url } = item;
// XXX The value of title cannot be undefined; otherwise, react-native
// will throw a TypeError: Cannot read property of undefined. While it's
@@ -180,7 +203,9 @@ class NavigateSectionList extends Component {
+ onPress = { url ? this._onPress(url) : undefined }
+ secondaryAction = {
+ url ? undefined : this._onSecondaryAction(id) } />
);
}
diff --git a/react/features/base/react/components/native/NavigateSectionListItem.js b/react/features/base/react/components/native/NavigateSectionListItem.js
index 762e90de1..9018b01df 100644
--- a/react/features/base/react/components/native/NavigateSectionListItem.js
+++ b/react/features/base/react/components/native/NavigateSectionListItem.js
@@ -17,7 +17,12 @@ type Props = {
/**
* 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 {
return lines && lines.length ? lines.map(this._renderItemLine) : null;
}
+ /**
+ * Renders the secondary action label.
+ *
+ * @private
+ * @returns {React$Node}
+ */
+ _renderSecondaryAction() {
+ const { secondaryAction } = this.props;
+
+ return (
+
+ +
+
+ );
+ }
+
/**
* Renders the content of this component.
*
@@ -135,6 +158,7 @@ export default class NavigateSectionListItem extends Component {
{this._renderItemLines(lines)}
+ { this.props.secondaryAction && this._renderSecondaryAction() }
);
}
diff --git a/react/features/base/react/components/native/styles.js b/react/features/base/react/components/native/styles.js
index d422dfb13..53f193843 100644
--- a/react/features/base/react/components/native/styles.js
+++ b/react/features/base/react/components/native/styles.js
@@ -11,6 +11,7 @@ const HEADER_COLOR = ColorPalette.blue;
// Header height is from Android guidelines. Also, this looks good.
const HEADER_HEIGHT = 56;
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
+const SECONDARY_ACTION_BUTTON_SIZE = 30;
export const HEADER_PADDING = BoxModel.padding;
export const STATUSBAR_COLOR = ColorPalette.blueHighlight;
@@ -266,6 +267,21 @@ const SECTION_LIST_STYLES = {
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: {
flexDirection: 'row'
}
diff --git a/react/features/calendar-sync/actions.any.js b/react/features/calendar-sync/actions.any.js
new file mode 100644
index 000000000..ac0276d15
--- /dev/null
+++ b/react/features/calendar-sync/actions.any.js
@@ -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