This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { Component } from 'react';
|
||||
|
||||
/**
|
||||
* A React Component for adding a meeting URL to an existing calendar meeting.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class AddMeetingUrlButton extends Component<void> {
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
// Not yet implemented.
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default AddMeetingUrlButton;
|
||||
@@ -0,0 +1,101 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createCalendarClickedEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IStore } from '../../app/types';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconPlus } from '../../base/icons/svg';
|
||||
import Tooltip from '../../base/tooltip/components/Tooltip';
|
||||
import { updateCalendarEvent } from '../actions.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AddMeetingUrlButton}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The calendar ID associated with the calendar event.
|
||||
*/
|
||||
calendarId: string;
|
||||
|
||||
/**
|
||||
* Invoked to add a meeting URL to a calendar event.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* The ID of the calendar event that will have a meeting URL added on click.
|
||||
*/
|
||||
eventId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A React Component for adding a meeting URL to an existing calendar event.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class AddMeetingUrlButton extends Component<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code AddMeetingUrlButton} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onKeyPress = this._onKeyPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<Tooltip content = { this.props.t('calendarSync.addMeetingURL') }>
|
||||
<div
|
||||
className = 'button add-button'
|
||||
onClick = { this._onClick }
|
||||
onKeyPress = { this._onKeyPress }
|
||||
role = 'button'>
|
||||
<Icon src = { IconPlus } />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to adding a meeting URL to a calendar event.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { calendarId, dispatch, eventId } = this.props;
|
||||
|
||||
sendAnalytics(createCalendarClickedEvent('add.url'));
|
||||
|
||||
dispatch(updateCalendarEvent(eventId, calendarId));
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPress(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this._onClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(AddMeetingUrlButton));
|
||||
143
react/features/calendar-sync/components/CalendarList.native.tsx
Normal file
143
react/features/calendar-sync/components/CalendarList.native.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import AbstractPage from '../../base/react/components/AbstractPage';
|
||||
import { openSettings } from '../../mobile/permissions/functions';
|
||||
import { refreshCalendar } from '../actions.native';
|
||||
|
||||
import CalendarListContent from './CalendarListContent.native';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link CalendarList}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The current state of the calendar access permission.
|
||||
*/
|
||||
_authorization?: string;
|
||||
|
||||
/**
|
||||
* Indicates if the list is disabled or not.
|
||||
*/
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to display a list of events from the (mobile) user's calendar.
|
||||
*/
|
||||
class CalendarList extends AbstractPage<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code CalendarList} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._getRenderListEmptyComponent
|
||||
= this._getRenderListEmptyComponent.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public API method for {@code Component}s rendered in
|
||||
* {@link AbstractPagedList}. When invoked, refreshes the calendar entries
|
||||
* in the app.
|
||||
*
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @param {boolean} isInteractive - If true this refresh was caused by
|
||||
* direct user interaction, false otherwise.
|
||||
* @public
|
||||
* @returns {void}
|
||||
*/
|
||||
static refresh(dispatch: IStore['dispatch'], isInteractive: boolean) {
|
||||
dispatch(refreshCalendar(false, isInteractive));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { disabled } = this.props;
|
||||
|
||||
return (
|
||||
CalendarListContent
|
||||
? <View
|
||||
style = {
|
||||
(disabled
|
||||
? styles.calendarSyncDisabled
|
||||
: styles.calendarSync) as ViewStyle }>
|
||||
<CalendarListContent
|
||||
disabled = { disabled }
|
||||
listEmptyComponent
|
||||
= { this._getRenderListEmptyComponent() } />
|
||||
</View>
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list empty component if a custom one has to be rendered instead
|
||||
* of the default one in the {@link NavigateSectionList}.
|
||||
*
|
||||
* @private
|
||||
* @returns {?React$Component}
|
||||
*/
|
||||
_getRenderListEmptyComponent() {
|
||||
const { _authorization, t } = this.props;
|
||||
|
||||
// If we don't provide a list specific renderListEmptyComponent, then
|
||||
// the default empty component of the NavigateSectionList will be
|
||||
// rendered, which (atm) is a simple "Pull to refresh" message.
|
||||
if (_authorization !== 'denied') {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style = { styles.noPermissionMessageView as ViewStyle }>
|
||||
<Text style = { styles.noPermissionMessageText as ViewStyle }>
|
||||
{ t('calendarSync.permissionMessage') }
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress = { openSettings }
|
||||
style = { styles.noPermissionMessageButton as ViewStyle } >
|
||||
<Text style = { styles.noPermissionMessageButtonText as ViewStyle }>
|
||||
{ t('calendarSync.permissionButton') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {{
|
||||
* _authorization: ?string,
|
||||
* _eventList: Array<Object>
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { authorization } = state['features/calendar-sync'];
|
||||
|
||||
return {
|
||||
_authorization: authorization
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(CalendarList));
|
||||
269
react/features/calendar-sync/components/CalendarList.web.tsx
Normal file
269
react/features/calendar-sync/components/CalendarList.web.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createCalendarClickedEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconCalendar } from '../../base/icons/svg';
|
||||
import AbstractPage from '../../base/react/components/AbstractPage';
|
||||
import Spinner from '../../base/ui/components/web/Spinner';
|
||||
import { openSettingsDialog } from '../../settings/actions.web';
|
||||
import { SETTINGS_TABS } from '../../settings/constants';
|
||||
import { refreshCalendar } from '../actions.web';
|
||||
import { ERRORS } from '../constants';
|
||||
|
||||
import CalendarListContent from './CalendarListContent.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link CalendarList}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The error object containing details about any error that has occurred
|
||||
* while interacting with calendar integration.
|
||||
*/
|
||||
_calendarError?: { error: string; };
|
||||
|
||||
/**
|
||||
* Whether or not a calendar may be connected for fetching calendar events.
|
||||
*/
|
||||
_hasIntegrationSelected: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not events have been fetched from a calendar.
|
||||
*/
|
||||
_hasLoadedEvents: boolean;
|
||||
|
||||
/**
|
||||
* Indicates if the list is disabled or not.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to display a list of events from the user's calendar.
|
||||
*/
|
||||
class CalendarList extends AbstractPage<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code CalendarList} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._getRenderListEmptyComponent
|
||||
= this._getRenderListEmptyComponent.bind(this);
|
||||
this._onOpenSettings = this._onOpenSettings.bind(this);
|
||||
this._onKeyPressOpenSettings = this._onKeyPressOpenSettings.bind(this);
|
||||
this._onRefreshEvents = this._onRefreshEvents.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { disabled } = this.props;
|
||||
|
||||
return (
|
||||
CalendarListContent
|
||||
? <CalendarListContent
|
||||
disabled = { Boolean(disabled) }
|
||||
listEmptyComponent
|
||||
= { this._getRenderListEmptyComponent() } />
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a component for showing the error message related to calendar
|
||||
* sync.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Component}
|
||||
*/
|
||||
_getErrorMessage() {
|
||||
const { _calendarError = { error: undefined }, t } = this.props;
|
||||
|
||||
let errorMessageKey = 'calendarSync.error.generic';
|
||||
let showRefreshButton = true;
|
||||
let showSettingsButton = true;
|
||||
|
||||
if (_calendarError.error === ERRORS.GOOGLE_APP_MISCONFIGURED) {
|
||||
errorMessageKey = 'calendarSync.error.appConfiguration';
|
||||
showRefreshButton = false;
|
||||
showSettingsButton = false;
|
||||
} else if (_calendarError.error === ERRORS.AUTH_FAILED) {
|
||||
errorMessageKey = 'calendarSync.error.notSignedIn';
|
||||
showRefreshButton = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'meetings-list-empty'>
|
||||
<p className = 'description'>
|
||||
{ t(errorMessageKey) }
|
||||
</p>
|
||||
<div className = 'calendar-action-buttons'>
|
||||
{ showSettingsButton
|
||||
&& <div
|
||||
className = 'button'
|
||||
onClick = { this._onOpenSettings }>
|
||||
{ t('calendarSync.permissionButton') }
|
||||
</div>
|
||||
}
|
||||
{ showRefreshButton
|
||||
&& <div
|
||||
className = 'button'
|
||||
onClick = { this._onRefreshEvents }>
|
||||
{ t('calendarSync.refresh') }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list empty component if a custom one has to be rendered instead
|
||||
* of the default one in the {@link NavigateSectionList}.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Component}
|
||||
*/
|
||||
_getRenderListEmptyComponent() {
|
||||
const {
|
||||
_calendarError,
|
||||
_hasIntegrationSelected,
|
||||
_hasLoadedEvents,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
if (_calendarError) {
|
||||
return this._getErrorMessage();
|
||||
} else if (_hasIntegrationSelected && _hasLoadedEvents) {
|
||||
return (
|
||||
<div className = 'meetings-list-empty'>
|
||||
<p className = 'description'>
|
||||
{ t('calendarSync.noEvents') }
|
||||
</p>
|
||||
<div
|
||||
className = 'button'
|
||||
onClick = { this._onRefreshEvents }>
|
||||
{ t('calendarSync.refresh') }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (_hasIntegrationSelected && !_hasLoadedEvents) {
|
||||
return (
|
||||
<div className = 'meetings-list-empty'>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'meetings-list-empty'>
|
||||
<div className = 'meetings-list-empty-image'>
|
||||
<img
|
||||
alt = { t('welcomepage.logo.calendar') }
|
||||
src = './images/calendar.svg' />
|
||||
</div>
|
||||
<div className = 'description'>
|
||||
{ t('welcomepage.connectCalendarText', {
|
||||
app: interfaceConfig.APP_NAME,
|
||||
provider: interfaceConfig.PROVIDER_NAME
|
||||
}) }
|
||||
</div>
|
||||
<div
|
||||
className = 'meetings-list-empty-button'
|
||||
onClick = { this._onOpenSettings }
|
||||
onKeyPress = { this._onKeyPressOpenSettings }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
<Icon
|
||||
className = 'meetings-list-empty-icon'
|
||||
src = { IconCalendar } />
|
||||
<span>{ t('welcomepage.connectCalendarButton') }</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@code SettingsDialog}.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenSettings() {
|
||||
sendAnalytics(createCalendarClickedEvent('connect'));
|
||||
|
||||
this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR));
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPressOpenSettings(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this._onOpenSettings();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets an updated list of calendar events.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRefreshEvents() {
|
||||
this.props.dispatch(refreshCalendar(true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code CalendarList} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _calendarError: Object,
|
||||
* _hasIntegrationSelected: boolean,
|
||||
* _hasLoadedEvents: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const {
|
||||
error,
|
||||
events,
|
||||
integrationType,
|
||||
isLoadingEvents
|
||||
} = state['features/calendar-sync'];
|
||||
|
||||
return {
|
||||
_calendarError: error,
|
||||
_hasIntegrationSelected: Boolean(integrationType),
|
||||
_hasLoadedEvents: Boolean(events) || !isLoadingEvents
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(CalendarList));
|
||||
@@ -0,0 +1,249 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createCalendarClickedEvent, createCalendarSelectedEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { appNavigate } from '../../app/actions.native';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getLocalizedDateFormatter } from '../../base/i18n/dateUtil';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import NavigateSectionList from '../../base/react/components/native/NavigateSectionList';
|
||||
import { openUpdateCalendarEventDialog, refreshCalendar } from '../actions.native';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link CalendarListContent}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The calendar event list.
|
||||
*/
|
||||
_eventList: Array<any>;
|
||||
|
||||
/**
|
||||
* Indicates if the list is disabled or not.
|
||||
*/
|
||||
disabled: boolean;
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
listEmptyComponent: React.ReactElement<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to display a list of events from a connected calendar.
|
||||
*/
|
||||
class CalendarListContent extends Component<IProps> {
|
||||
/**
|
||||
* Default values for the component's props.
|
||||
*/
|
||||
static defaultProps = {
|
||||
_eventList: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code CalendarListContent} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
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);
|
||||
this._toTimeString = this._toTimeString.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
||||
* immediately after this component is mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
sendAnalytics(createCalendarSelectedEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { disabled, listEmptyComponent } = this.props;
|
||||
|
||||
return (
|
||||
<NavigateSectionList
|
||||
disabled = { disabled }
|
||||
onPress = { this._onPress }
|
||||
onRefresh = { this._onRefresh }
|
||||
onSecondaryAction = { this._onSecondaryAction }
|
||||
renderListEmptyComponent
|
||||
= { listEmptyComponent }
|
||||
sections = { this._toDisplayableList() } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the list's navigate action.
|
||||
*
|
||||
* @private
|
||||
* @param {string} url - The url string to navigate to.
|
||||
* @param {string} analyticsEventName - Тhe name of the analytics event
|
||||
* associated with this action.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPress(url: string, analyticsEventName = 'meeting.tile') {
|
||||
sendAnalytics(createCalendarClickedEvent(analyticsEventName));
|
||||
|
||||
this.props.dispatch(appNavigate(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to execute when the list is doing a pull-to-refresh.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRefresh() {
|
||||
this.props.dispatch(refreshCalendar(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: string) {
|
||||
this.props.dispatch(openUpdateCalendarEventDialog(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a date string for a given event.
|
||||
*
|
||||
* @param {Object} event - The event.
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_toDateString(event: any) {
|
||||
const startDateTime
|
||||
= getLocalizedDateFormatter(event.startDate).format('MMM Do, YYYY');
|
||||
|
||||
return `${startDateTime}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a displayable object from an event.
|
||||
*
|
||||
* @param {Object} event - The calendar event.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_toDisplayableItem(event: any) {
|
||||
return {
|
||||
id: event.id,
|
||||
key: `${event.id}-${event.startDate}`,
|
||||
lines: [
|
||||
event.url,
|
||||
this._toTimeString(event)
|
||||
],
|
||||
title: event.title,
|
||||
url: event.url
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the event list to a displayable list with sections.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
_toDisplayableList() {
|
||||
const { _eventList, t } = this.props;
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const { createSection } = NavigateSectionList;
|
||||
const TODAY_SECTION = 'today';
|
||||
const sectionMap = new Map();
|
||||
|
||||
for (const event of _eventList) {
|
||||
const displayableEvent = this._toDisplayableItem(event);
|
||||
const startDate = new Date(event.startDate).getDate();
|
||||
|
||||
if (startDate === now.getDate()) {
|
||||
let todaySection = sectionMap.get(TODAY_SECTION);
|
||||
|
||||
if (!todaySection) {
|
||||
todaySection
|
||||
= createSection(t('calendarSync.today'), TODAY_SECTION);
|
||||
sectionMap.set(TODAY_SECTION, todaySection);
|
||||
}
|
||||
|
||||
todaySection.data.push(displayableEvent);
|
||||
} else if (sectionMap.has(startDate)) {
|
||||
const section = sectionMap.get(startDate);
|
||||
|
||||
if (section) {
|
||||
section.data.push(displayableEvent);
|
||||
}
|
||||
} else {
|
||||
const newSection
|
||||
= createSection(this._toDateString(event), startDate);
|
||||
|
||||
sectionMap.set(startDate, newSection);
|
||||
newSection.data.push(displayableEvent);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(sectionMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a time (interval) string for a given event.
|
||||
*
|
||||
* @param {Object} event - The event.
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_toTimeString(event: any) {
|
||||
const startDateTime
|
||||
= getLocalizedDateFormatter(event.startDate).format('lll');
|
||||
const endTime
|
||||
= getLocalizedDateFormatter(event.endDate).format('LT');
|
||||
|
||||
return `${startDateTime} - ${endTime}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_eventList: state['features/calendar-sync'].events
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(CalendarListContent));
|
||||
@@ -0,0 +1,163 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createCalendarClickedEvent, createCalendarSelectedEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { appNavigate } from '../../app/actions.web';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import MeetingsList from '../../base/react/components/web/MeetingsList';
|
||||
|
||||
import AddMeetingUrlButton from './AddMeetingUrlButton.web';
|
||||
import JoinButton from './JoinButton.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link CalendarListContent}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The calendar event list.
|
||||
*/
|
||||
_eventList: Array<Object>;
|
||||
|
||||
/**
|
||||
* Indicates if the list is disabled or not.
|
||||
*/
|
||||
disabled: boolean;
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
listEmptyComponent: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to display a list of events from a connected calendar.
|
||||
*/
|
||||
class CalendarListContent extends Component<IProps> {
|
||||
/**
|
||||
* Default values for the component's props.
|
||||
*/
|
||||
static defaultProps = {
|
||||
_eventList: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code CalendarListContent} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onJoinPress = this._onJoinPress.bind(this);
|
||||
this._onPress = this._onPress.bind(this);
|
||||
this._toDisplayableItem = this._toDisplayableItem.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
||||
* immediately after this component is mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
sendAnalytics(createCalendarSelectedEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { disabled, listEmptyComponent } = this.props;
|
||||
const { _eventList = [] } = this.props;
|
||||
const meetings = _eventList.map(this._toDisplayableItem);
|
||||
|
||||
return (
|
||||
<MeetingsList
|
||||
disabled = { disabled }
|
||||
listEmptyComponent = { listEmptyComponent }
|
||||
meetings = { meetings }
|
||||
onPress = { this._onPress } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the list's navigate action.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} event - The click event.
|
||||
* @param {string} url - The url string to navigate to.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onJoinPress(event: React.KeyboardEvent, url: string) {
|
||||
event.stopPropagation();
|
||||
|
||||
this._onPress(url, 'meeting.join');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the list's navigate action.
|
||||
*
|
||||
* @private
|
||||
* @param {string} url - The url string to navigate to.
|
||||
* @param {string} analyticsEventName - Тhe name of the analytics event
|
||||
* associated with this action.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPress(url: string, analyticsEventName = 'meeting.tile') {
|
||||
sendAnalytics(createCalendarClickedEvent(analyticsEventName));
|
||||
|
||||
this.props.dispatch(appNavigate(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a displayable object from an event.
|
||||
*
|
||||
* @param {Object} event - The calendar event.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_toDisplayableItem(event: any) {
|
||||
return {
|
||||
elementAfter: event.url
|
||||
? <JoinButton
|
||||
onPress = { this._onJoinPress }
|
||||
url = { event.url } />
|
||||
: (<AddMeetingUrlButton
|
||||
calendarId = { event.calendarId }
|
||||
eventId = { event.id } />),
|
||||
date: event.startDate,
|
||||
time: [ event.startDate, event.endDate ],
|
||||
description: event.url,
|
||||
title: event.title,
|
||||
url: event.url
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {{
|
||||
* _eventList: Array<Object>
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_eventList: state['features/calendar-sync'].events
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(CalendarListContent);
|
||||
21
react/features/calendar-sync/components/JoinButton.native.ts
Normal file
21
react/features/calendar-sync/components/JoinButton.native.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Component } from 'react';
|
||||
|
||||
/**
|
||||
* A React Component for joining an existing calendar meeting.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class JoinButton extends Component<void> {
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
// Not yet implemented.
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default JoinButton;
|
||||
96
react/features/calendar-sync/components/JoinButton.web.tsx
Normal file
96
react/features/calendar-sync/components/JoinButton.web.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconPlus } from '../../base/icons/svg';
|
||||
import Tooltip from '../../base/tooltip/components/Tooltip';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link JoinButton}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The function called when the button is pressed.
|
||||
*/
|
||||
onPress: Function;
|
||||
|
||||
/**
|
||||
* The meeting URL associated with the {@link JoinButton} instance.
|
||||
*/
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A React Component for joining an existing calendar meeting.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class JoinButton extends Component<IProps> {
|
||||
|
||||
/**
|
||||
* Initializes a new {@code JoinButton} instance.
|
||||
*
|
||||
* @param {*} props - The read-only properties with which the new instance
|
||||
* is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onKeyPress = this._onKeyPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content = { t('calendarSync.joinTooltip') }>
|
||||
<div
|
||||
className = 'button join-button'
|
||||
onClick = { this._onClick }
|
||||
onKeyPress = { this._onKeyPress }
|
||||
role = 'button'>
|
||||
<Icon
|
||||
size = '14'
|
||||
src = { IconPlus } />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when the component is clicked.
|
||||
*
|
||||
* @param {Object} event - The DOM click event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick(event?: React.MouseEvent) {
|
||||
this.props.onPress(event, this.props.url);
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPress(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this._onClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(JoinButton);
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link MicrosoftSignInButton}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The callback to invoke when {@code MicrosoftSignInButton} is clicked.
|
||||
*/
|
||||
onClick: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* The text to display within {@code MicrosoftSignInButton}.
|
||||
*/
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A React Component showing a button to sign in with Microsoft.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class MicrosoftSignInButton extends Component<IProps> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<div
|
||||
className = 'microsoft-sign-in'
|
||||
onClick = { this.props.onClick }>
|
||||
<img
|
||||
alt = { this.props.t('welcomepage.logo.microsoftLogo') }
|
||||
className = 'microsoft-logo'
|
||||
src = 'images/microsoftLogo.svg' />
|
||||
<div className = 'microsoft-cta'>
|
||||
{ this.props.text }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(MicrosoftSignInButton);
|
||||
@@ -0,0 +1,65 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import ConfirmDialog from '../../base/dialog/components/native/ConfirmDialog';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { updateCalendarEvent } from '../actions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* The ID of the event to be updated.
|
||||
*/
|
||||
eventId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for the add Jitsi link confirm dialog.
|
||||
*/
|
||||
class UpdateCalendarEventDialog extends Component<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code UpdateCalendarEventDialog} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
descriptionKey = 'calendarSync.confirmAddLink'
|
||||
onSubmit = { this._onSubmit } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
182
react/features/calendar-sync/components/styles.ts
Normal file
182
react/features/calendar-sync/components/styles.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { ColorPalette } from '../../base/styles/components/styles/ColorPalette';
|
||||
import { createStyleSheet } from '../../base/styles/functions.any';
|
||||
import BaseTheme from '../../base/ui/components/BaseTheme';
|
||||
|
||||
const NOTIFICATION_SIZE = 55;
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Component}s of the feature meeting-list i.e.
|
||||
* {@code CalendarList}.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
|
||||
/**
|
||||
* Button style of the open settings button.
|
||||
*/
|
||||
noPermissionMessageButton: {
|
||||
backgroundColor: ColorPalette.blue,
|
||||
borderColor: ColorPalette.blue,
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
height: 30,
|
||||
justifyContent: 'center',
|
||||
margin: 15,
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
|
||||
/**
|
||||
* Text style of the open settings button.
|
||||
*/
|
||||
noPermissionMessageButtonText: {
|
||||
color: ColorPalette.white
|
||||
},
|
||||
|
||||
/**
|
||||
* Text style of the no permission message.
|
||||
*/
|
||||
noPermissionMessageText: {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* Top level view of the no permission message.
|
||||
*/
|
||||
noPermissionMessageView: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
padding: 20
|
||||
},
|
||||
|
||||
/**
|
||||
* The top level container of the notification.
|
||||
*/
|
||||
notificationContainer: {
|
||||
alignSelf: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute'
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style for the container when the notification is displayed
|
||||
* on the side (narrow view).
|
||||
*/
|
||||
notificationContainerSide: {
|
||||
top: 100
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style for the container when the notification is displayed
|
||||
* on the top (wide view).
|
||||
*/
|
||||
notificationContainerTop: {
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* The top level container of the notification.
|
||||
*/
|
||||
notificationContent: {
|
||||
alignSelf: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
height: NOTIFICATION_SIZE,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 10
|
||||
},
|
||||
|
||||
/**
|
||||
* Color for upcoming meeting notification.
|
||||
*/
|
||||
notificationContentNext: {
|
||||
backgroundColor: '#eeb231'
|
||||
},
|
||||
|
||||
/**
|
||||
* Color for already ongoing meeting notifications.
|
||||
*/
|
||||
notificationContentPast: {
|
||||
backgroundColor: 'red'
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style for the content when the notification is displayed
|
||||
* on the side (narrow view).
|
||||
*/
|
||||
notificationContentSide: {
|
||||
borderBottomRightRadius: NOTIFICATION_SIZE,
|
||||
borderTopRightRadius: NOTIFICATION_SIZE
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style for the content when the notification is displayed
|
||||
* on the top (wide view).
|
||||
*/
|
||||
notificationContentTop: {
|
||||
borderBottomLeftRadius: NOTIFICATION_SIZE / 2,
|
||||
borderBottomRightRadius: NOTIFICATION_SIZE / 2,
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
|
||||
/**
|
||||
* The icon of the notification.
|
||||
*/
|
||||
notificationIcon: {
|
||||
color: 'white',
|
||||
fontSize: '1.5rem'
|
||||
},
|
||||
|
||||
/**
|
||||
* The container that contains the icon.
|
||||
*/
|
||||
notificationIconContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
height: NOTIFICATION_SIZE,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* A single line of text of the notification.
|
||||
*/
|
||||
notificationText: {
|
||||
color: 'white',
|
||||
fontSize: '0.875rem'
|
||||
},
|
||||
|
||||
/**
|
||||
* The container for all the lines if the notification.
|
||||
*/
|
||||
notificationTextContainer: {
|
||||
flexDirection: 'column',
|
||||
height: NOTIFICATION_SIZE,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* The touchable component.
|
||||
*/
|
||||
touchableView: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
calendarSync: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
flex: 1,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
calendarSyncDisabled: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
flex: 1,
|
||||
opacity: 0.8,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user