diff --git a/ios/app/src/Info.plist b/ios/app/src/Info.plist index 3fdb58a58..ca7cd2ab8 100644 --- a/ios/app/src/Info.plist +++ b/ios/app/src/Info.plist @@ -56,13 +56,13 @@ NSCalendarsUsageDescription - See your scheduled conferences in the app. + See your scheduled meetings in the app. NSCameraUsageDescription - Participate in conferences with video. + Participate in meetings with video. NSLocationWhenInUseUsageDescription NSMicrophoneUsageDescription - Participate in conferences with voice. + Participate in meetings with voice. UIBackgroundModes audio diff --git a/lang/main.json b/lang/main.json index 3adb24898..414b8981f 100644 --- a/lang/main.json +++ b/lang/main.json @@ -608,7 +608,7 @@ "now": "Now", "ongoingMeeting": "ongoing meeting", "permissionButton": "Open settings", - "permissionMessage": "Calendar permission is required to list your meetings in the app." + "permissionMessage": "The Calendar permission is required to see your meetings in the app." }, "recentList": { "today": "Today", diff --git a/react/features/base/react/components/native/AbstractPagedList.js b/react/features/base/react/components/native/AbstractPagedList.js index 0e31df853..eca687487 100644 --- a/react/features/base/react/components/native/AbstractPagedList.js +++ b/react/features/base/react/components/native/AbstractPagedList.js @@ -5,11 +5,14 @@ import { View } from 'react-native'; import styles from './styles'; +/** + * The type of the React {@code Component} props of {@link AbstractPagedList}. + */ type Props = { /** - * The index (starting from 0) of the page that should be rendered - * active as default. + * The zero-based index of the page that should be rendered (selected) by + * default. */ defaultPage: number, @@ -30,16 +33,20 @@ type Props = { /** * The pages of the PagedList component to be rendered. - * Note: page.component may be undefined and then they don't need to be - * rendered. + * + * Note: An element's {@code component} may be {@code undefined} and then it + * won't need to be rendered. */ pages: Array<{ - component: Object, + component: ?Object, icon: string | number, title: string }> }; +/** + * The type of the React {@code Component} state of {@link AbstractPagedList}. + */ type State = { /** @@ -53,7 +60,7 @@ type State = { */ export default class AbstractPagedList extends Component { /** - * Constructor of the component. + * Initializes a new {@code AbstractPagedList} instance. * * @inheritdoc */ @@ -63,15 +70,19 @@ export default class AbstractPagedList extends Component { this.state = { pageIndex: this._validatePageIndex(props.defaultPage) }; + + // Bind event handlers so they are only bound once per instance. + this._maybeRefreshSelectedPage + = this._maybeRefreshSelectedPage.bind(this); } /** - * Implements React's {@code Component} componentDidMount. + * Implements React's {@link Component#componentDidMount}. * * @inheritdoc */ componentDidMount() { - this._maybeRefreshActivePage(); + this._maybeRefreshSelectedPage(); } /** @@ -80,8 +91,8 @@ export default class AbstractPagedList extends Component { * @inheritdoc */ render() { - const { disabled, pages } = this.props; - const enabledPages = pages.filter(page => page.component); + const { disabled } = this.props; + const pages = this.props.pages.filter(({ component }) => component); return ( { disabled ? styles.pagedListContainerDisabled : null ] }> { - enabledPages.length > 1 + pages.length > 1 ? this._renderPagedList(disabled) - : enabledPages.length === 1 + : pages.length === 1 ? React.createElement( - /* type */ enabledPages[0].component, + + // $FlowExpectedError + /* type */ pages[0].component, /* props */ { disabled, style: styles.pagedList - }) : null + }) + : null } ); } - _platformSpecificPageSelect: number => void - - /** - * Method to be overriden by the components implementing this abstract class - * to handle platform specific actions on page select. - * - * @protected - * @param {number} pageIndex - The selected page index. - * @returns {void} - */ - _platformSpecificPageSelect(pageIndex) { - this._selectPage(pageIndex); - } - - _maybeRefreshActivePage: () => void + _maybeRefreshSelectedPage: () => void; /** * Components that this PagedList displays may have a refresh function to @@ -128,13 +128,15 @@ export default class AbstractPagedList extends Component { * @private * @returns {void} */ - _maybeRefreshActivePage() { + _maybeRefreshSelectedPage() { const selectedPage = this.props.pages[this.state.pageIndex]; + let component; - if (selectedPage && selectedPage.component) { - const { refresh } = selectedPage.component; + if (selectedPage && (component = selectedPage.component)) { + const { refresh } = component; - typeof refresh === 'function' && refresh(this.props.dispatch); + typeof refresh === 'function' + && refresh.call(component, this.props.dispatch); } } @@ -145,25 +147,22 @@ export default class AbstractPagedList extends Component { /** * Sets the selected page. * - * @param {number} pageIndex - The index of the active page. + * @param {number} pageIndex - The index of the selected page. * @protected * @returns {void} */ _selectPage(pageIndex: number) { - const validatedPageIndex = this._validatePageIndex(pageIndex); + // eslint-disable-next-line no-param-reassign + pageIndex = this._validatePageIndex(pageIndex); const { onSelectPage } = this.props; - if (typeof onSelectPage === 'function') { - onSelectPage(validatedPageIndex); - } + typeof onSelectPage === 'function' && onSelectPage(pageIndex); - this.setState({ - pageIndex: validatedPageIndex - }, () => this._maybeRefreshActivePage()); + this.setState({ pageIndex }, this._maybeRefreshSelectedPage); } - _validatePageIndex: number => number + _validatePageIndex: number => number; /** * Validates the requested page index and returns a safe value. @@ -173,10 +172,10 @@ export default class AbstractPagedList extends Component { * @returns {number} */ _validatePageIndex(pageIndex) { - // pageIndex may point to a non existing page if some of the pages are + // pageIndex may point to a non-existing page if some of the pages are // disabled (their component property is undefined). const maxPageIndex - = this.props.pages.filter(page => page.component).length - 1; + = this.props.pages.filter(({ component }) => component).length - 1; return Math.max(0, Math.min(maxPageIndex, pageIndex)); } diff --git a/react/features/base/react/components/native/PagedList.android.js b/react/features/base/react/components/native/PagedList.android.js index c9612e0b0..6563eb708 100644 --- a/react/features/base/react/components/native/PagedList.android.js +++ b/react/features/base/react/components/native/PagedList.android.js @@ -86,18 +86,6 @@ class PagedList extends AbstractPagedList { } } - /** - * Platform specific actions to run on page select. - * - * @private - * @param {number} pageIndex - The selected page index. - * @returns {void} - */ - _platformSpecificPageSelect(pageIndex) { - this._viewPager.setPage(pageIndex); - this._selectPage(pageIndex); - } - /** * Renders a single page of the page list. * diff --git a/react/features/calendar-sync/actions.js b/react/features/calendar-sync/actions.js index 3c3950958..cdbbc0357 100644 --- a/react/features/calendar-sync/actions.js +++ b/react/features/calendar-sync/actions.js @@ -1,9 +1,9 @@ // @flow import { + REFRESH_CALENDAR, SET_CALENDAR_AUTHORIZATION, - SET_CALENDAR_EVENTS, - REFRESH_CALENDAR + SET_CALENDAR_EVENTS } from './actionTypes'; /** diff --git a/react/features/calendar-sync/components/MeetingList.native.js b/react/features/calendar-sync/components/MeetingList.native.js index ea1be7222..db8913766 100644 --- a/react/features/calendar-sync/components/MeetingList.native.js +++ b/react/features/calendar-sync/components/MeetingList.native.js @@ -21,7 +21,7 @@ type Props = { /** * The current state of the calendar access permission. */ - _authorization: string, + _authorization: ?string, /** * The calendar event list. @@ -73,7 +73,7 @@ class MeetingList extends Component { } /** - * Constructor of the MeetingList component. + * Initializes a new {@code MeetingList} instance. * * @inheritdoc */ @@ -85,32 +85,26 @@ class MeetingList extends Component { = this._getRenderListEmptyComponent.bind(this); this._onPress = this._onPress.bind(this); this._onRefresh = this._onRefresh.bind(this); + this._toDateString = this._toDateString.bind(this); this._toDisplayableItem = this._toDisplayableItem.bind(this); this._toDisplayableList = this._toDisplayableList.bind(this); - this._toDateString = this._toDateString.bind(this); } /** - * Implements the React Components's render. + * Implements React's {@link Component#render}. * * @inheritdoc */ render() { - const { _authorization, disabled } = this.props; + const { disabled } = this.props; return ( ); } @@ -122,10 +116,17 @@ class MeetingList extends Component { * of the default one in the {@link NavigateSectionList}. * * @private - * @returns {Function} + * @returns {?React$Component} */ _getRenderListEmptyComponent() { - const { t } = this.props; + 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 undefined; + } return ( @@ -168,6 +169,24 @@ class MeetingList extends Component { this.props.dispatch(refreshCalendar(true)); } + _toDateString: Object => string; + + /** + * Generates a date (interval) string for a given event. + * + * @param {Object} event - The event. + * @private + * @returns {string} + */ + _toDateString(event) { + const startDateTime + = getLocalizedDateFormatter(event.startDate).format('lll'); + const endTime + = getLocalizedDateFormatter(event.endDate).format('LT'); + + return `${startDateTime} - ${endTime}`; + } + _toDisplayableItem: Object => Object; /** @@ -199,7 +218,9 @@ class MeetingList extends Component { */ _toDisplayableList() { const { _eventList, t } = this.props; + const now = Date.now(); + const { createSection } = NavigateSectionList; const nowSection = createSection(t('calendarSync.now'), 'now'); const nextSection = createSection(t('calendarSync.next'), 'next'); @@ -227,31 +248,11 @@ class MeetingList extends Component { nextSection, laterSection ]) { - if (section.data.length) { - sectionList.push(section); - } + section.data.length && sectionList.push(section); } return sectionList; } - - _toDateString: Object => string; - - /** - * Generates a date (interval) string for a given event. - * - * @param {Object} event - The event. - * @private - * @returns {string} - */ - _toDateString(event) { - const startDateTime - = getLocalizedDateFormatter(event.startDate).format('lll'); - const endTime - = getLocalizedDateFormatter(event.endDate).format('LT'); - - return `${startDateTime} - ${endTime}`; - } } /** @@ -259,15 +260,16 @@ class MeetingList extends Component { * * @param {Object} state - The redux state. * @returns {{ - * _eventList: Array + * _authorization: ?string, + * _eventList: Array * }} */ function _mapStateToProps(state: Object) { - const calendarSyncState = state['features/calendar-sync']; + const { authorization, events } = state['features/calendar-sync']; return { - _authorization: calendarSyncState.authorization, - _eventList: calendarSyncState.events + _authorization: authorization, + _eventList: events }; } diff --git a/react/features/calendar-sync/components/styles.js b/react/features/calendar-sync/components/styles.js index 9acd47cd3..9481dd1a5 100644 --- a/react/features/calendar-sync/components/styles.js +++ b/react/features/calendar-sync/components/styles.js @@ -34,7 +34,8 @@ export default createStyleSheet({ */ noPermissionMessageText: { backgroundColor: 'transparent', - color: 'rgba(255, 255, 255, 0.6)' + color: 'rgba(255, 255, 255, 0.6)', + textAlign: 'center' }, /** diff --git a/react/features/calendar-sync/reducer.js b/react/features/calendar-sync/reducer.js index 50ca82707..f4e3ebb85 100644 --- a/react/features/calendar-sync/reducer.js +++ b/react/features/calendar-sync/reducer.js @@ -43,16 +43,10 @@ CALENDAR_ENABLED break; case SET_CALENDAR_AUTHORIZATION: - return { - ...state, - authorization: action.authorization - }; + return set(state, 'authorization', action.authorization); case SET_CALENDAR_EVENTS: - return { - ...state, - events: action.events - }; + return set(state, 'events', action.events); } return state; diff --git a/react/features/welcome/actionTypes.js b/react/features/welcome/actionTypes.js index f8bb6ecc0..177e67000 100644 --- a/react/features/welcome/actionTypes.js +++ b/react/features/welcome/actionTypes.js @@ -12,13 +12,13 @@ export const SET_SIDEBAR_VISIBLE = Symbol('SET_SIDEBAR_VISIBLE'); /** - * Action to update the default page index of the {@code WelcomePageLists} - * component. + * The type of (redux) action to set the default page index of + * {@link WelcomePageLists}. * * { - * type: SET_WELCOME_PAGE_LIST_DEFAULT_PAGE, + * type: SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE, * pageIndex: number * } */ -export const SET_WELCOME_PAGE_LIST_DEFAULT_PAGE +export const SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE = Symbol('SET_WELCOME_PAGE_LIST_DEFAULT_PAGE'); diff --git a/react/features/welcome/actions.js b/react/features/welcome/actions.js index f29cd1148..ee2a2db93 100644 --- a/react/features/welcome/actions.js +++ b/react/features/welcome/actions.js @@ -2,26 +2,9 @@ import { SET_SIDEBAR_VISIBLE, - SET_WELCOME_PAGE_LIST_DEFAULT_PAGE + SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE } from './actionTypes'; -/** - * Action to update the default page index of the {@code WelcomePageLists} - * component. - * - * @param {number} pageIndex - The index of the selected page. - * @returns {{ - * type: SET_WELCOME_PAGE_LIST_DEFAULT_PAGE, - * pageIndex: number - * }} - */ -export function setWelcomePageListDefaultPage(pageIndex: number) { - return { - type: SET_WELCOME_PAGE_LIST_DEFAULT_PAGE, - pageIndex - }; -} - /** * Sets the visibility of {@link WelcomePageSideBar}. * @@ -38,3 +21,19 @@ export function setSideBarVisible(visible: boolean) { visible }; } + +/** + * Sets the default page index of {@link WelcomePageLists}. + * + * @param {number} pageIndex - The index of the default page. + * @returns {{ + * type: SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE, + * pageIndex: number + * }} + */ +export function setWelcomePageListsDefaultPage(pageIndex: number) { + return { + type: SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE, + pageIndex + }; +} diff --git a/react/features/welcome/components/WelcomePageLists.js b/react/features/welcome/components/WelcomePageLists.js index fd30f4a56..68d20910f 100644 --- a/react/features/welcome/components/WelcomePageLists.js +++ b/react/features/welcome/components/WelcomePageLists.js @@ -9,8 +9,11 @@ import { PagedList } from '../../base/react'; import { MeetingList } from '../../calendar-sync'; import { RecentList } from '../../recent-list'; -import { setWelcomePageListDefaultPage } from '../actions'; +import { setWelcomePageListsDefaultPage } from '../actions'; +/** + * The type of the React {@code Component} props of {@link WelcomePageLists}. + */ type Props = { /** @@ -50,17 +53,19 @@ const IOS_RECENT_LIST_ICON = require('../../../../images/history.png'); class WelcomePageLists extends Component { /** * The pages to be rendered. - * Note: The component field may be undefined if a feature (such as - * Calendar) is disabled, and that means that the page must not be rendered. + * + * Note: An element's {@code component} may be {@code undefined} if a + * feature (such as Calendar) is disabled, and that means that the page must + * not be rendered. */ pages: Array<{ - component: Object, + component: ?Object, icon: string | number, title: string - }> + }>; /** - * Component contructor. + * Initializes a new {@code WelcomePageLists} instance. * * @inheritdoc */ @@ -68,28 +73,32 @@ class WelcomePageLists extends Component { super(props); const { t } = props; - const isAndroid = Platform.OS === 'android'; + const android = Platform.OS === 'android'; - this.pages = [ { - component: RecentList, - icon: isAndroid ? 'restore' : IOS_RECENT_LIST_ICON, - title: t('welcomepage.recentList') - }, { - component: MeetingList, - icon: isAndroid ? 'event_note' : IOS_CALENDAR_ICON, - title: t('welcomepage.calendar') - } ]; + this.pages = [ + { + component: RecentList, + icon: android ? 'restore' : IOS_RECENT_LIST_ICON, + title: t('welcomepage.recentList') + }, + { + component: MeetingList, + icon: android ? 'event_note' : IOS_CALENDAR_ICON, + title: t('welcomepage.calendar') + } + ]; + // Bind event handlers so they are only bound once per instance. this._onSelectPage = this._onSelectPage.bind(this); } /** - * Implements React Component's render. + * Implements React's {@link Component#render}. * * @inheritdoc */ render() { - const { disabled, _defaultPage } = this.props; + const { _defaultPage } = this.props; if (typeof _defaultPage === 'undefined') { return null; @@ -98,13 +107,13 @@ class WelcomePageLists extends Component { return ( ); } - _onSelectPage: number => void + _onSelectPage: number => void; /** * Callback for the {@code PagedList} page select action. @@ -114,9 +123,7 @@ class WelcomePageLists extends Component { * @returns {void} */ _onSelectPage(pageIndex) { - const { dispatch } = this.props; - - dispatch(setWelcomePageListDefaultPage(pageIndex)); + this.props.dispatch(setWelcomePageListsDefaultPage(pageIndex)); } } @@ -127,18 +134,20 @@ class WelcomePageLists extends Component { * @param {Object} state - The redux state. * @protected * @returns {{ - * _hasRecentListEntries: boolean + * _defaultPage: number * }} */ function _mapStateToProps(state: Object) { - const { defaultPage } = state['features/welcome']; - const recentList = state['features/recent-list']; - const _hasRecentListEntries = Boolean(recentList && recentList.length); + let { defaultPage } = state['features/welcome']; + + if (typeof defaultPage === 'undefined') { + const recentList = state['features/recent-list']; + + defaultPage = recentList && recentList.length ? 0 : 1; + } return { - _defaultPage: defaultPage === 'undefined' - ? _hasRecentListEntries ? 0 : 1 - : defaultPage + _defaultPage: defaultPage }; } diff --git a/react/features/welcome/reducer.js b/react/features/welcome/reducer.js index 72a31a4fb..df382ddf7 100644 --- a/react/features/welcome/reducer.js +++ b/react/features/welcome/reducer.js @@ -1,42 +1,37 @@ // @flow -import { ReducerRegistry } from '../base/redux'; +import { ReducerRegistry, set } from '../base/redux'; import { PersistenceRegistry } from '../base/storage'; + import { SET_SIDEBAR_VISIBLE, - SET_WELCOME_PAGE_LIST_DEFAULT_PAGE + SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE } from './actionTypes'; /** - * The Redux store name this feature uses. + * The name of the redux store/state property which is the root of the redux + * state of the feature {@code welcome}. */ const STORE_NAME = 'features/welcome'; /** - * Sets up the persistence of the feature {@code features/welcome}. + * Sets up the persistence of the feature {@code welcome}. */ PersistenceRegistry.register(STORE_NAME, { defaultPage: true }); /** - * Reduces redux actions for the purposes of {@code features/welcome}. + * Reduces redux actions for the purposes of the feature {@code welcome}. */ ReducerRegistry.register(STORE_NAME, (state = {}, action) => { switch (action.type) { case SET_SIDEBAR_VISIBLE: - return { - ...state, - sideBarVisible: action.visible - }; + return set(state, 'sideBarVisible', action.visible); - case SET_WELCOME_PAGE_LIST_DEFAULT_PAGE: - return { - ...state, - defaultPage: action.pageIndex - }; - - default: - return state; + case SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE: + return set(state, 'defaultPage', action.pageIndex); } + + return state; });