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} events - The new list. + * @returns {{ + * type: SET_CALENDAR_EVENTS, + * events: Array + * }} + */ +export function setCalendarEvents(events: Array) { + return { + type: SET_CALENDAR_EVENTS, + events + }; +} diff --git a/react/features/calendar-sync/actions.native.js b/react/features/calendar-sync/actions.native.js new file mode 100644 index 000000000..dfe555354 --- /dev/null +++ b/react/features/calendar-sync/actions.native.js @@ -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)); + }); + }; +} diff --git a/react/features/calendar-sync/actions.js b/react/features/calendar-sync/actions.web.js similarity index 83% rename from react/features/calendar-sync/actions.js rename to react/features/calendar-sync/actions.web.js index 64269bf45..af97d0d95 100644 --- a/react/features/calendar-sync/actions.js +++ b/react/features/calendar-sync/actions.web.js @@ -2,14 +2,12 @@ import { loadGoogleAPI } from '../google-api'; +import { refreshCalendar, setCalendarEvents } from './actions'; import { createCalendarConnectedEvent, sendAnalytics } from '../analytics'; import { CLEAR_CALENDAR_INTEGRATION, - REFRESH_CALENDAR, SET_CALENDAR_AUTH_STATE, - SET_CALENDAR_AUTHORIZATION, - SET_CALENDAR_EVENTS, SET_CALENDAR_INTEGRATION, SET_CALENDAR_PROFILE_EMAIL, SET_LOADING_CALENDAR_EVENTS @@ -17,6 +15,8 @@ import { import { _getCalendarIntegration, isCalendarEnabled } from './functions'; import { generateRoomWithoutSeparator } from '../welcome'; +export * from './actions.any'; + 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 - * 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 - * }} + * NOTE: Currently there is no confirmation prompted on web, so this is just + * a relaying method to avoid flow problems. + * + * @param {string} eventId - The event id. + * @param {string} calendarId - The calendar id. + * @returns {Function} */ -export function refreshCalendar( - forcePermission: boolean = false, isInteractive: boolean = true) { - return { - type: REFRESH_CALENDAR, - forcePermission, - isInteractive - }; +export function openUpdateCalendarEventDialog( + eventId: string, calendarId: string) { + return updateCalendarEvent(eventId, calendarId); } /** @@ -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} events - The new list. - * @returns {{ - * type: SET_CALENDAR_EVENTS, - * events: Array - * }} - */ -export function setCalendarEvents(events: Array) { - return { - type: SET_CALENDAR_EVENTS, - events - }; -} - /** * 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 * 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)); + }); + }; +} diff --git a/react/features/calendar-sync/components/BaseCalendarList.js b/react/features/calendar-sync/components/BaseCalendarList.js index d55533308..3dc7b490e 100644 --- a/react/features/calendar-sync/components/BaseCalendarList.js +++ b/react/features/calendar-sync/components/BaseCalendarList.js @@ -12,7 +12,7 @@ import { import { getLocalizedDateFormatter, translate } from '../../base/i18n'; import { NavigateSectionList } from '../../base/react'; -import { refreshCalendar } from '../actions'; +import { refreshCalendar, openUpdateCalendarEventDialog } from '../actions'; import { isCalendarEnabled } from '../functions'; @@ -93,6 +93,7 @@ class BaseCalendarList extends Component { this._onJoinPress = this._onJoinPress.bind(this); this._onPress = this._onPress.bind(this); this._onRefresh = this._onRefresh.bind(this); + this._onSecondaryAction = this._onSecondaryAction.bind(this); this._toDateString = this._toDateString.bind(this); this._toDisplayableItem = this._toDisplayableItem.bind(this); this._toDisplayableList = this._toDisplayableList.bind(this); @@ -123,6 +124,7 @@ class BaseCalendarList extends Component { disabled = { disabled } onPress = { this._onPress } onRefresh = { this._onRefresh } + onSecondaryAction = { this._onSecondaryAction } renderListEmptyComponent = { renderListEmptyComponent } sections = { this._toDisplayableList() } /> @@ -174,6 +176,20 @@ class BaseCalendarList extends Component { 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; /** @@ -208,6 +224,7 @@ class BaseCalendarList extends Component { : (), + id: event.id, key: `${event.id}-${event.startDate}`, lines: [ event.url, diff --git a/react/features/calendar-sync/components/ConferenceNotification.native.js b/react/features/calendar-sync/components/ConferenceNotification.native.js index a2ce0f2f6..677d4e5a8 100644 --- a/react/features/calendar-sync/components/ConferenceNotification.native.js +++ b/react/features/calendar-sync/components/ConferenceNotification.native.js @@ -237,9 +237,10 @@ class ConferenceNotification extends Component { for (const event of _eventList) { const eventUrl - = getURLWithoutParamsNormalized(new URL(event.url)); + = event.url + && getURLWithoutParamsNormalized(new URL(event.url)); - if (eventUrl !== _currentConferenceURL) { + if (eventUrl && eventUrl !== _currentConferenceURL) { if ((!eventToShow && event.startDate > now && event.startDate < now + ALERT_MILLISECONDS) diff --git a/react/features/calendar-sync/components/UpdateCalendarEventDialog.native.js b/react/features/calendar-sync/components/UpdateCalendarEventDialog.native.js new file mode 100644 index 000000000..0b4497e19 --- /dev/null +++ b/react/features/calendar-sync/components/UpdateCalendarEventDialog.native.js @@ -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 { + /** + * 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 ( + + + { this.props.t('calendarSync.confirmAddLink') } + + + ); + } + + _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)); diff --git a/react/features/calendar-sync/components/UpdateCalendarEventDialog.web.js b/react/features/calendar-sync/components/UpdateCalendarEventDialog.web.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/calendar-sync/components/index.js b/react/features/calendar-sync/components/index.js index 1c2d8d5c5..d21f473d2 100644 --- a/react/features/calendar-sync/components/index.js +++ b/react/features/calendar-sync/components/index.js @@ -1,3 +1,6 @@ export { default as ConferenceNotification } from './ConferenceNotification'; export { default as CalendarList } from './CalendarList'; export { default as MicrosoftSignInButton } from './MicrosoftSignInButton'; +export { + default as UpdateCalendarEventDialog +} from './UpdateCalendarEventDialog'; diff --git a/react/features/calendar-sync/functions.any.js b/react/features/calendar-sync/functions.any.js index a9a7a9a60..e68c9b93b 100644 --- a/react/features/calendar-sync/functions.any.js +++ b/react/features/calendar-sync/functions.any.js @@ -89,40 +89,35 @@ export function _updateCalendarEntries(events: Array) { function _parseCalendarEntry(event, knownDomains) { if (event) { 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 - // till we implement event edit on mobile - if (url || navigator.product !== 'ReactNative') { - const startDate = Date.parse(event.startDate); - const endDate = Date.parse(event.endDate); - - // we want to hide all events that - // - has no start or end date - // - for web, if there is no url and we cannot edit the event (has - // no calendarId) - if (isNaN(startDate) - || isNaN(endDate) - || (navigator.product !== 'ReactNative' - && !url - && !event.calendarId)) { - logger.debug( - 'Skipping invalid calendar event', - event.title, - event.startDate, - event.endDate, - url, - event.calendarId - ); - } else { - return { - calendarId: event.calendarId, - endDate, - id: event.id, - startDate, - title: event.title, - url - }; - } + // we want to hide all events that + // - has no start or end date + // - for web, if there is no url and we cannot edit the event (has + // no calendarId) + if (isNaN(startDate) + || isNaN(endDate) + || (navigator.product !== 'ReactNative' + && !url + && !event.calendarId)) { + logger.debug( + 'Skipping invalid calendar event', + event.title, + event.startDate, + event.endDate, + url, + event.calendarId + ); + } else { + return { + calendarId: event.calendarId, + endDate, + id: event.id, + startDate, + title: event.title, + url + }; } } diff --git a/react/features/calendar-sync/functions.native.js b/react/features/calendar-sync/functions.native.js index f2c743b55..4cb413e83 100644 --- a/react/features/calendar-sync/functions.native.js +++ b/react/features/calendar-sync/functions.native.js @@ -1,6 +1,10 @@ -import { NativeModules } from 'react-native'; +// @flow + +import { NativeModules, Platform } from 'react-native'; import RNCalendarEvents from 'react-native-calendar-events'; +import { getShareInfoText } from '../invite'; + import { setCalendarAuthorization } from './actions'; import { FETCH_END_DAYS, FETCH_START_DAYS } from './constants'; import { _updateCalendarEntries } from './functions'; @@ -9,6 +13,39 @@ export * from './functions.any'; 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 * example, Apple through its App Store requires diff --git a/react/features/conference/components/Conference.native.js b/react/features/conference/components/Conference.native.js index 312877436..046114088 100644 --- a/react/features/conference/components/Conference.native.js +++ b/react/features/conference/components/Conference.native.js @@ -8,7 +8,6 @@ import { connect as reactReduxConnect } from 'react-redux'; import { appNavigate } from '../../app'; import { connect, disconnect } from '../../base/connection'; -import { DialogContainer } from '../../base/dialog'; import { getParticipantCount } from '../../base/participants'; import { Container, LoadingIndicator, TintedView } from '../../base/react'; import { @@ -315,11 +314,7 @@ class Conference extends Component { this._renderConferenceNotification() } - {/* - * The dialogs are in the topmost stacking layers. - */ - this.props._reducedUI || - } + ); } diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js index 1a4e43925..687ba5a6b 100644 --- a/react/features/conference/components/Conference.web.js +++ b/react/features/conference/components/Conference.web.js @@ -8,7 +8,6 @@ import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout'; import { obtainConfig } from '../../base/config'; import { connect, disconnect } from '../../base/connection'; -import { DialogContainer } from '../../base/dialog'; import { translate } from '../../base/i18n'; import { Filmstrip } from '../../filmstrip'; import { CalleeInfoContainer } from '../../invite'; @@ -226,7 +225,6 @@ class Conference extends Component { { filmstripOnly || } { filmstripOnly || } - diff --git a/react/features/invite/functions.js b/react/features/invite/functions.js index f9c5518ab..730282172 100644 --- a/react/features/invite/functions.js +++ b/react/features/invite/functions.js @@ -438,7 +438,7 @@ export function getShareInfoText( if (!dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) { // URLs for fetching dial in numbers not defined - return Promise.reject(); + return Promise.resolve(infoText); } numbersPromise = Promise.all([ diff --git a/react/features/welcome/components/WelcomePage.web.js b/react/features/welcome/components/WelcomePage.web.js index a0360362a..258ea7d49 100644 --- a/react/features/welcome/components/WelcomePage.web.js +++ b/react/features/welcome/components/WelcomePage.web.js @@ -7,7 +7,6 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme'; import React from 'react'; import { connect } from 'react-redux'; -import { DialogContainer } from '../../base/dialog'; import { translate } from '../../base/i18n'; import { Platform, Watermarks } from '../../base/react'; import { CalendarList } from '../../calendar-sync'; @@ -168,9 +167,6 @@ class WelcomePage extends AbstractWelcomePage { ref = { this._setAdditionalContentRef } /> : null } - - - ); }