[RN] Add invite function to calendar

This commit is contained in:
Bettenbuk Zoltan 2018-09-04 09:29:48 +02:00
parent 126e2d6e14
commit 2d87757aaa
19 changed files with 436 additions and 120 deletions

View File

@ -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",

View File

@ -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>
);
}
}

View File

@ -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';

View File

@ -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.

View File

@ -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<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;
/**
@ -165,7 +188,7 @@ class NavigateSectionList extends Component<Props> {
*/
_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<Props> {
<NavigateSectionListItem
item = { item }
key = { key }
onPress = { this._onPress(url) } />
onPress = { url ? this._onPress(url) : undefined }
secondaryAction = {
url ? undefined : this._onSecondaryAction(id) } />
);
}

View File

@ -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<Props> {
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.
*
@ -135,6 +158,7 @@ export default class NavigateSectionListItem extends Component<Props> {
</Text>
{this._renderItemLines(lines)}
</Container>
{ this.props.secondaryAction && this._renderSecondaryAction() }
</Container>
);
}

View File

@ -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'
}

View File

@ -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
};
}

View File

@ -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));
});
};
}

View File

@ -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<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.
*
@ -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));
});
};
}

View File

@ -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<Props> {
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<Props> {
disabled = { disabled }
onPress = { this._onPress }
onRefresh = { this._onRefresh }
onSecondaryAction = { this._onSecondaryAction }
renderListEmptyComponent
= { renderListEmptyComponent }
sections = { this._toDisplayableList() } />
@ -174,6 +176,20 @@ class BaseCalendarList extends Component<Props> {
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<Props> {
: (<AddMeetingUrlButton
calendarId = { event.calendarId }
eventId = { event.id } />),
id: event.id,
key: `${event.id}-${event.startDate}`,
lines: [
event.url,

View File

@ -237,9 +237,10 @@ class ConferenceNotification extends Component<Props, State> {
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)

View File

@ -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));

View File

@ -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';

View File

@ -89,40 +89,35 @@ export function _updateCalendarEntries(events: Array<Object>) {
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
};
}
}

View File

@ -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

View File

@ -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([