mirror of https://github.com/jitsi/jitsi-meet
parent
4dbcaf851f
commit
bba480f329
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.4 KiB |
@ -0,0 +1,49 @@ |
||||
import { ColorPalette } from './ColorPalette'; |
||||
import { BoxModel } from './BoxModel'; |
||||
|
||||
import { |
||||
createStyleSheet |
||||
} from '../../functions'; |
||||
|
||||
export const PlatformElements = createStyleSheet({ |
||||
|
||||
/** |
||||
* Platform specific header button (e.g. back, menu...etc). |
||||
*/ |
||||
headerButton: { |
||||
alignSelf: 'center', |
||||
color: ColorPalette.white, |
||||
fontSize: 26, |
||||
paddingRight: 22 |
||||
}, |
||||
|
||||
/** |
||||
* Generic style for a label placed in the header. |
||||
*/ |
||||
headerText: { |
||||
color: ColorPalette.white, |
||||
fontSize: 20 |
||||
}, |
||||
|
||||
/** |
||||
* An empty padded view to place components. |
||||
*/ |
||||
paddedView: { |
||||
padding: BoxModel.padding |
||||
}, |
||||
|
||||
/** |
||||
* The topmost level element of a page. |
||||
*/ |
||||
page: { |
||||
alignItems: 'stretch', |
||||
bottom: 0, |
||||
flex: 1, |
||||
flexDirection: 'column', |
||||
left: 0, |
||||
overflow: 'hidden', |
||||
position: 'absolute', |
||||
right: 0, |
||||
top: 0 |
||||
} |
||||
}); |
@ -1,2 +1,3 @@ |
||||
export * from './BoxModel'; |
||||
export * from './ColorPalette'; |
||||
export * from './PlatformElements'; |
||||
|
@ -0,0 +1,71 @@ |
||||
// @flow
|
||||
|
||||
import moment from 'moment'; |
||||
|
||||
import { i18next } from '../i18n'; |
||||
|
||||
// MomentJS uses static language bundle loading, so in order to support dynamic
|
||||
// language selection in the app we need to load all bundles that we support in
|
||||
// the app.
|
||||
// FIXME: If we decide to support MomentJS in other features as well we may need
|
||||
// to move this import and the lenient matcher to the i18n feature.
|
||||
require('moment/locale/bg'); |
||||
require('moment/locale/de'); |
||||
require('moment/locale/eo'); |
||||
require('moment/locale/es'); |
||||
require('moment/locale/fr'); |
||||
require('moment/locale/hy-am'); |
||||
require('moment/locale/it'); |
||||
require('moment/locale/nb'); |
||||
|
||||
// OC is not available. Please submit OC translation to the MomentJS project.
|
||||
|
||||
require('moment/locale/pl'); |
||||
require('moment/locale/pt'); |
||||
require('moment/locale/pt-br'); |
||||
require('moment/locale/ru'); |
||||
require('moment/locale/sk'); |
||||
require('moment/locale/sl'); |
||||
require('moment/locale/sv'); |
||||
require('moment/locale/tr'); |
||||
require('moment/locale/zh-cn'); |
||||
|
||||
/** |
||||
* Returns a localized date formatter initialized with a specific {@code Date} |
||||
* or time stamp ({@code number}). |
||||
* |
||||
* @private |
||||
* @param {Date | number} dateOrTimeStamp - The date or unix timestamp (ms) |
||||
* to format. |
||||
* @returns {Object} |
||||
*/ |
||||
export function getLocalizedDateFormatter(dateOrTimeStamp: Date | number) { |
||||
return moment(dateOrTimeStamp).locale(_getSupportedLocale()); |
||||
} |
||||
|
||||
/** |
||||
* A lenient locale matcher to match language and dialect if possible. |
||||
* |
||||
* @private |
||||
* @returns {string} |
||||
*/ |
||||
function _getSupportedLocale() { |
||||
const i18nLocale = i18next.language; |
||||
let supportedLocale; |
||||
|
||||
if (i18nLocale) { |
||||
const localeRegexp = new RegExp('^([a-z]{2,2})(-)*([a-z]{2,2})*$'); |
||||
const localeResult = localeRegexp.exec(i18nLocale.toLowerCase()); |
||||
|
||||
if (localeResult) { |
||||
const currentLocaleRegexp |
||||
= new RegExp( |
||||
`^${localeResult[1]}(-)*${`(${localeResult[3]})*` || ''}`); |
||||
|
||||
supportedLocale |
||||
= moment.locales().find(lang => currentLocaleRegexp.exec(lang)); |
||||
} |
||||
} |
||||
|
||||
return supportedLocale || 'en'; |
||||
} |
@ -0,0 +1,6 @@ |
||||
// @flow
|
||||
|
||||
/** |
||||
* Action to update the current calendar entry list in the store. |
||||
*/ |
||||
export const NEW_CALENDAR_ENTRY_LIST = Symbol('NEW_CALENDAR_ENTRY_LIST'); |
@ -0,0 +1,18 @@ |
||||
// @flow
|
||||
import { NEW_CALENDAR_ENTRY_LIST } from './actionTypes'; |
||||
|
||||
/** |
||||
* Sends an action to update the current calendar list in redux. |
||||
* |
||||
* @param {Array<Object>} events - The new list. |
||||
* @returns {{ |
||||
* type: NEW_CALENDAR_ENTRY_LIST, |
||||
* events: Array<Object> |
||||
* }} |
||||
*/ |
||||
export function updateCalendarEntryList(events: Array<Object>) { |
||||
return { |
||||
type: NEW_CALENDAR_ENTRY_LIST, |
||||
events |
||||
}; |
||||
} |
@ -0,0 +1,290 @@ |
||||
// @flow
|
||||
import React, { Component } from 'react'; |
||||
import { |
||||
SafeAreaView, |
||||
SectionList, |
||||
Text, |
||||
TouchableHighlight, |
||||
View |
||||
} from 'react-native'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { appNavigate } from '../../app'; |
||||
import { translate } from '../../base/i18n'; |
||||
import { getLocalizedDateFormatter } from '../../base/util'; |
||||
|
||||
import styles, { UNDERLAY_COLOR } from './styles'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Indicates if the list is disabled or not. |
||||
*/ |
||||
disabled: boolean, |
||||
|
||||
/** |
||||
* The Redux dispatch function. |
||||
*/ |
||||
dispatch: Function, |
||||
|
||||
/** |
||||
* The calendar event list. |
||||
*/ |
||||
_eventList: Array<Object>, |
||||
|
||||
/** |
||||
* The translate function. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* Component to display a list of events from the (mobile) user's calendar. |
||||
*/ |
||||
class MeetingList extends Component<Props> { |
||||
|
||||
/** |
||||
* Constructor of the MeetingList component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this._createSection = this._createSection.bind(this); |
||||
this._getItemKey = this._getItemKey.bind(this); |
||||
this._onJoin = this._onJoin.bind(this); |
||||
this._onSelect = this._onSelect.bind(this); |
||||
this._renderItem = this._renderItem.bind(this); |
||||
this._renderSection = this._renderSection.bind(this); |
||||
this._toDisplayableList = this._toDisplayableList.bind(this); |
||||
this._toDateString = this._toDateString.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements the React Components's render method. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { disabled } = this.props; |
||||
|
||||
return ( |
||||
<SafeAreaView |
||||
style = { [ |
||||
styles.container, |
||||
disabled ? styles.containerDisabled : null |
||||
] } > |
||||
<SectionList |
||||
keyExtractor = { this._getItemKey } |
||||
renderItem = { this._renderItem } |
||||
renderSectionHeader = { this._renderSection } |
||||
sections = { this._toDisplayableList() } |
||||
style = { styles.list } /> |
||||
</SafeAreaView> |
||||
); |
||||
} |
||||
|
||||
_createSection: string => Object; |
||||
|
||||
/** |
||||
* Creates a section object of a list of events. |
||||
* |
||||
* @private |
||||
* @param {string} i18Title - The i18 title of the section. |
||||
* @returns {Object} |
||||
*/ |
||||
_createSection(i18Title) { |
||||
return { |
||||
data: [], |
||||
key: `key-${i18Title}`, |
||||
title: this.props.t(i18Title) |
||||
}; |
||||
} |
||||
|
||||
_getItemKey: (Object, number) => string; |
||||
|
||||
/** |
||||
* Generates a unique id to every item. |
||||
* |
||||
* @private |
||||
* @param {Object} item - The item. |
||||
* @param {number} index - The item index. |
||||
* @returns {string} |
||||
*/ |
||||
_getItemKey(item, index) { |
||||
return `${index}-${item.id}-${item.startDate}`; |
||||
} |
||||
|
||||
_onJoin: string => void; |
||||
|
||||
/** |
||||
* Joins the selected URL. |
||||
* |
||||
* @param {string} url - The URL to join to. |
||||
* @returns {void} |
||||
*/ |
||||
_onJoin(url) { |
||||
const { disabled, dispatch } = this.props; |
||||
|
||||
!disabled && url && dispatch(appNavigate(url)); |
||||
} |
||||
|
||||
_onSelect: string => Function; |
||||
|
||||
/** |
||||
* Creates a function that when invoked, joins the given URL. |
||||
* |
||||
* @private |
||||
* @param {string} url - The URL to join to. |
||||
* @returns {Function} |
||||
*/ |
||||
_onSelect(url) { |
||||
return this._onJoin.bind(this, url); |
||||
} |
||||
|
||||
_renderItem: Object => Object; |
||||
|
||||
/** |
||||
* Renders a single item in the list. |
||||
* |
||||
* @private |
||||
* @param {Object} listItem - The item to render. |
||||
* @returns {Component} |
||||
*/ |
||||
_renderItem(listItem) { |
||||
const { item } = listItem; |
||||
|
||||
return ( |
||||
<TouchableHighlight |
||||
onPress = { this._onSelect(item.url) } |
||||
underlayColor = { UNDERLAY_COLOR }> |
||||
<View style = { styles.listItem }> |
||||
<View style = { styles.avatarContainer } > |
||||
<View style = { styles.avatar } > |
||||
<Text style = { styles.avatarContent }> |
||||
{ item.title.substr(0, 1).toUpperCase() } |
||||
</Text> |
||||
</View> |
||||
</View> |
||||
<View style = { styles.listItemDetails }> |
||||
<Text |
||||
numberOfLines = { 1 } |
||||
style = { [ |
||||
styles.listItemText, |
||||
styles.listItemTitle |
||||
] }> |
||||
{ item.title } |
||||
</Text> |
||||
<Text |
||||
numberOfLines = { 1 } |
||||
style = { styles.listItemText }> |
||||
{ item.url } |
||||
</Text> |
||||
<Text |
||||
numberOfLines = { 1 } |
||||
style = { styles.listItemText }> |
||||
{ this._toDateString(item) } |
||||
</Text> |
||||
</View> |
||||
</View> |
||||
</TouchableHighlight> |
||||
); |
||||
} |
||||
|
||||
_renderSection: Object => Object; |
||||
|
||||
/** |
||||
* Renders a section title. |
||||
* |
||||
* @private |
||||
* @param {Object} section - The section being rendered. |
||||
* @returns {Component} |
||||
*/ |
||||
_renderSection(section) { |
||||
return ( |
||||
<View style = { styles.listSection }> |
||||
<Text style = { styles.listSectionText }> |
||||
{ section.section.title } |
||||
</Text> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
_toDisplayableList: () => Array<Object> |
||||
|
||||
/** |
||||
* Transforms the event list to a displayable list |
||||
* with sections. |
||||
* |
||||
* @private |
||||
* @returns {Array<Object>} |
||||
*/ |
||||
_toDisplayableList() { |
||||
const { _eventList } = this.props; |
||||
const now = Date.now(); |
||||
const nowSection = this._createSection('calendarSync.now'); |
||||
const nextSection = this._createSection('calendarSync.next'); |
||||
const laterSection = this._createSection('calendarSync.later'); |
||||
|
||||
if (_eventList && _eventList.length) { |
||||
for (const event of _eventList) { |
||||
if (event.startDate < now && event.endDate > now) { |
||||
nowSection.data.push(event); |
||||
} else if (event.startDate > now) { |
||||
if (nextSection.data.length |
||||
&& nextSection.data[0].startDate !== event.startDate) { |
||||
laterSection.data.push(event); |
||||
} else { |
||||
nextSection.data.push(event); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
const sectionList = []; |
||||
|
||||
for (const section of [ |
||||
nowSection, |
||||
nextSection, |
||||
laterSection |
||||
]) { |
||||
if (section.data.length) { |
||||
sectionList.push(section); |
||||
} |
||||
} |
||||
|
||||
return sectionList; |
||||
} |
||||
|
||||
_toDateString: Object => string; |
||||
|
||||
/** |
||||
* Generates a date (interval) string for a given event. |
||||
* |
||||
* @private |
||||
* @param {Object} event - The event. |
||||
* @returns {string} |
||||
*/ |
||||
_toDateString(event) { |
||||
/* eslint-disable max-len */ |
||||
return `${getLocalizedDateFormatter(event.startDate).format('lll')} - ${getLocalizedDateFormatter(event.endDate).format('LT')}`; |
||||
/* eslint-enable max-len */ |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps redux state to component props. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @returns {{ |
||||
* _eventList: Array |
||||
* }} |
||||
*/ |
||||
export function _mapStateToProps(state: Object) { |
||||
return { |
||||
_eventList: state['features/calendar-sync'].events |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(MeetingList)); |
@ -0,0 +1 @@ |
||||
export { default as MeetingList } from './MeetingList'; |
@ -0,0 +1,109 @@ |
||||
import { createStyleSheet } from '../../base/styles'; |
||||
|
||||
const AVATAR_OPACITY = 0.4; |
||||
|
||||
const AVATAR_SIZE = 65; |
||||
|
||||
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)'; |
||||
|
||||
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)'; |
||||
|
||||
/** |
||||
* The styles of the React {@code Component}s of the feature recent-list i.e. |
||||
* {@code RecentList}. |
||||
*/ |
||||
export default createStyleSheet({ |
||||
|
||||
/** |
||||
* The style of the actual avatar. |
||||
* Recent-list copy! |
||||
*/ |
||||
avatar: { |
||||
alignItems: 'center', |
||||
backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`, |
||||
borderRadius: AVATAR_SIZE, |
||||
height: AVATAR_SIZE, |
||||
justifyContent: 'center', |
||||
width: AVATAR_SIZE |
||||
}, |
||||
|
||||
/** |
||||
* The style of the avatar container that makes the avatar rounded. |
||||
* Recent-list copy! |
||||
*/ |
||||
avatarContainer: { |
||||
alignItems: 'center', |
||||
flexDirection: 'row', |
||||
justifyContent: 'space-around', |
||||
padding: 5 |
||||
}, |
||||
|
||||
/** |
||||
* Simple {@code Text} content of the avatar (the actual initials). |
||||
* Recent-list copy! |
||||
*/ |
||||
avatarContent: { |
||||
backgroundColor: 'rgba(0, 0, 0, 0)', |
||||
color: OVERLAY_FONT_COLOR, |
||||
fontSize: 32, |
||||
fontWeight: '100', |
||||
textAlign: 'center' |
||||
}, |
||||
|
||||
/** |
||||
* The top level container style of the list. |
||||
*/ |
||||
container: { |
||||
flex: 1 |
||||
}, |
||||
|
||||
/** |
||||
* Shows the container disabled. |
||||
*/ |
||||
containerDisabled: { |
||||
opacity: 0.2 |
||||
}, |
||||
|
||||
list: { |
||||
flex: 1, |
||||
flexDirection: 'column' |
||||
}, |
||||
|
||||
listItem: { |
||||
alignItems: 'center', |
||||
flex: 1, |
||||
flexDirection: 'row', |
||||
paddingVertical: 5 |
||||
}, |
||||
|
||||
listItemDetails: { |
||||
flex: 1, |
||||
flexDirection: 'column', |
||||
overflow: 'hidden', |
||||
paddingHorizontal: 5 |
||||
}, |
||||
|
||||
listItemText: { |
||||
color: OVERLAY_FONT_COLOR, |
||||
fontSize: 16 |
||||
}, |
||||
|
||||
listItemTitle: { |
||||
fontWeight: 'bold', |
||||
fontSize: 18 |
||||
}, |
||||
|
||||
listSection: { |
||||
alignItems: 'center', |
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)', |
||||
flex: 1, |
||||
flexDirection: 'row', |
||||
padding: 5 |
||||
}, |
||||
|
||||
listSectionText: { |
||||
color: OVERLAY_FONT_COLOR, |
||||
fontSize: 14, |
||||
fontWeight: 'normal' |
||||
} |
||||
}); |
@ -0,0 +1,4 @@ |
||||
export * from './components'; |
||||
|
||||
import './middleware'; |
||||
import './reducer'; |
@ -0,0 +1,164 @@ |
||||
// @flow
|
||||
import Logger from 'jitsi-meet-logger'; |
||||
import RNCalendarEvents from 'react-native-calendar-events'; |
||||
|
||||
import { MiddlewareRegistry } from '../base/redux'; |
||||
|
||||
import { APP_WILL_MOUNT } from '../app'; |
||||
|
||||
import { updateCalendarEntryList } from './actions'; |
||||
|
||||
const FETCH_END_DAYS = 10; |
||||
const FETCH_START_DAYS = -1; |
||||
const MAX_LIST_LENGTH = 10; |
||||
const logger = Logger.getLogger(__filename); |
||||
|
||||
// this is to be dynamic later.
|
||||
const domainList = [ |
||||
'meet.jit.si', |
||||
'beta.meet.jit.si' |
||||
]; |
||||
|
||||
MiddlewareRegistry.register(store => next => action => { |
||||
const result = next(action); |
||||
|
||||
switch (action.type) { |
||||
case APP_WILL_MOUNT: |
||||
_fetchCalendarEntries(store); |
||||
} |
||||
|
||||
return result; |
||||
}); |
||||
|
||||
/** |
||||
* Ensures calendar access if possible and resolves the promise if it's granted. |
||||
* |
||||
* @private |
||||
* @returns {Promise} |
||||
*/ |
||||
function _ensureCalendarAccess() { |
||||
return new Promise((resolve, reject) => { |
||||
RNCalendarEvents.authorizationStatus() |
||||
.then(status => { |
||||
if (status === 'authorized') { |
||||
resolve(); |
||||
} else if (status === 'undetermined') { |
||||
RNCalendarEvents.authorizeEventStore() |
||||
.then(result => { |
||||
if (result === 'authorized') { |
||||
resolve(); |
||||
} else { |
||||
reject(result); |
||||
} |
||||
}) |
||||
.catch(error => { |
||||
reject(error); |
||||
}); |
||||
} else { |
||||
reject(status); |
||||
} |
||||
}) |
||||
.catch(error => { |
||||
reject(error); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Reads the user's calendar and updates the stored entries if need be. |
||||
* |
||||
* @private |
||||
* @param {Object} store - The redux store. |
||||
* @returns {void} |
||||
*/ |
||||
function _fetchCalendarEntries(store) { |
||||
_ensureCalendarAccess() |
||||
.then(() => { |
||||
const startDate = new Date(); |
||||
const endDate = new Date(); |
||||
|
||||
startDate.setDate(startDate.getDate() + FETCH_START_DAYS); |
||||
endDate.setDate(endDate.getDate() + FETCH_END_DAYS); |
||||
|
||||
RNCalendarEvents.fetchAllEvents( |
||||
startDate.getTime(), |
||||
endDate.getTime(), |
||||
[] |
||||
) |
||||
.then(events => { |
||||
const eventList = []; |
||||
|
||||
if (events && events.length) { |
||||
for (const event of events) { |
||||
const jitsiURL = _getURLFromEvent(event); |
||||
const now = Date.now(); |
||||
|
||||
if (jitsiURL) { |
||||
const eventStartDate = Date.parse(event.startDate); |
||||
const eventEndDate = Date.parse(event.endDate); |
||||
|
||||
if (isNaN(eventStartDate) || isNaN(eventEndDate)) { |
||||
logger.warn( |
||||
'Skipping calendar event due to invalid dates', |
||||
event.title, |
||||
event.startDate, |
||||
event.endDate |
||||
); |
||||
} else if (eventEndDate > now) { |
||||
eventList.push({ |
||||
endDate: eventEndDate, |
||||
id: event.id, |
||||
startDate: eventStartDate, |
||||
title: event.title, |
||||
url: jitsiURL |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
store.dispatch(updateCalendarEntryList(eventList.sort((a, b) => |
||||
a.startDate - b.startDate |
||||
).slice(0, MAX_LIST_LENGTH))); |
||||
}) |
||||
.catch(error => { |
||||
logger.error('Error fetching calendar.', error); |
||||
}); |
||||
}) |
||||
.catch(reason => { |
||||
logger.error('Error accessing calendar.', reason); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Retreives a jitsi URL from an event if present. |
||||
* |
||||
* @private |
||||
* @param {Object} event - The event to parse. |
||||
* @returns {string} |
||||
* |
||||
*/ |
||||
function _getURLFromEvent(event) { |
||||
const urlRegExp |
||||
= new RegExp(`http(s)?://(${domainList.join('|')})/[^\\s<>$]+`, 'gi'); |
||||
const fieldsToSearch = [ |
||||
event.title, |
||||
event.url, |
||||
event.location, |
||||
event.notes, |
||||
event.description |
||||
]; |
||||
let matchArray; |
||||
|
||||
for (const field of fieldsToSearch) { |
||||
if (typeof field === 'string') { |
||||
if ( |
||||
(matchArray = urlRegExp.exec(field)) !== null |
||||
) { |
||||
return matchArray[0]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return null; |
||||
} |
@ -0,0 +1,28 @@ |
||||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../base/redux'; |
||||
|
||||
import { NEW_CALENDAR_ENTRY_LIST } from './actionTypes'; |
||||
|
||||
/** |
||||
* ZB: this is an object, as further data is to come here, like: |
||||
* - known domain list |
||||
*/ |
||||
const DEFAULT_STATE = { |
||||
events: [] |
||||
}; |
||||
const STORE_NAME = 'features/calendar-sync'; |
||||
|
||||
ReducerRegistry.register( |
||||
STORE_NAME, |
||||
(state = DEFAULT_STATE, action) => { |
||||
switch (action.type) { |
||||
case NEW_CALENDAR_ENTRY_LIST: |
||||
return { |
||||
events: action.events |
||||
}; |
||||
|
||||
default: |
||||
return state; |
||||
} |
||||
}); |
@ -0,0 +1,48 @@ |
||||
// @flow
|
||||
|
||||
import { Component } from 'react'; |
||||
|
||||
/** |
||||
* The page to be displayed on render. |
||||
*/ |
||||
export const DEFAULT_PAGE = 0; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Indicates if the list is disabled or not. |
||||
*/ |
||||
disabled: boolean, |
||||
|
||||
/** |
||||
* The i18n translate function |
||||
*/ |
||||
t: Function |
||||
} |
||||
|
||||
type State = { |
||||
|
||||
/** |
||||
* The currently selected page. |
||||
*/ |
||||
pageIndex: number |
||||
} |
||||
|
||||
/** |
||||
* Abstract class for the platform specific paged lists. |
||||
*/ |
||||
export default class AbstractPagedList extends Component<Props, State> { |
||||
/** |
||||
* Constructor of the component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
pageIndex: DEFAULT_PAGE |
||||
}; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,97 @@ |
||||
// @flow
|
||||
import React from 'react'; |
||||
import { View, ViewPagerAndroid } from 'react-native'; |
||||
|
||||
import { MeetingList } from '../../calendar-sync'; |
||||
import { RecentList } from '../../recent-list'; |
||||
|
||||
import AbstractPagedList, { DEFAULT_PAGE } from './AbstractPagedList'; |
||||
import styles from './styles'; |
||||
|
||||
/** |
||||
* A platform specific component to render a paged or tabbed list/view. |
||||
* |
||||
* @extends PagedList |
||||
*/ |
||||
export default class PagedList extends AbstractPagedList { |
||||
|
||||
/** |
||||
* Constructor of the PagedList Component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor() { |
||||
super(); |
||||
this._getIndicatorStyle = this._getIndicatorStyle.bind(this); |
||||
this._onPageSelected = this._onPageSelected.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Renders the paged list. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { disabled } = this.props; |
||||
|
||||
return ( |
||||
<View style = { styles.pagedListContainer }> |
||||
<ViewPagerAndroid |
||||
initialPage = { DEFAULT_PAGE } |
||||
keyboardDismissMode = 'on-drag' |
||||
onPageSelected = { this._onPageSelected } |
||||
peekEnabled = { true } |
||||
style = { styles.pagedList }> |
||||
<View key = { 0 }> |
||||
<RecentList disabled = { disabled } /> |
||||
</View> |
||||
<View key = { 1 }> |
||||
<MeetingList disabled = { disabled } /> |
||||
</View> |
||||
</ViewPagerAndroid> |
||||
<View style = { styles.pageIndicatorContainer }> |
||||
<View style = { this._getIndicatorStyle(0) } /> |
||||
<View style = { this._getIndicatorStyle(1) } /> |
||||
</View> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
_getIndicatorStyle: number => Array<Object>; |
||||
|
||||
/** |
||||
* Constructs the style array of an idicator. |
||||
* |
||||
* @private |
||||
* @param {number} indicatorIndex - The index of the indicator. |
||||
* @returns {Array<Object>} |
||||
*/ |
||||
_getIndicatorStyle(indicatorIndex) { |
||||
const style = [ |
||||
styles.pageIndicator |
||||
]; |
||||
|
||||
if (this.state.pageIndex === indicatorIndex) { |
||||
style.push(styles.pageIndicatorActive); |
||||
} |
||||
|
||||
return style; |
||||
} |
||||
|
||||
_onPageSelected: Object => void; |
||||
|
||||
/** |
||||
* Updates the index of the currently selected page. |
||||
* |
||||
* @private |
||||
* @param {Object} event - The native event of the callback. |
||||
* @returns {void} |
||||
*/ |
||||
_onPageSelected({ nativeEvent: { position } }) { |
||||
if (this.state.pageIndex !== position) { |
||||
this.setState({ |
||||
pageIndex: position |
||||
}); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,82 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import { View, TabBarIOS } from 'react-native'; |
||||
|
||||
import { translate } from '../../base/i18n'; |
||||
import { MeetingList } from '../../calendar-sync'; |
||||
import { RecentList } from '../../recent-list'; |
||||
|
||||
import AbstractPagedList from './AbstractPagedList'; |
||||
import styles from './styles'; |
||||
|
||||
const CALENDAR_ICON = require('../../../../images/calendar.png'); |
||||
|
||||
/** |
||||
* A platform specific component to render a paged or tabbed list/view. |
||||
* |
||||
* @extends PagedList |
||||
*/ |
||||
class PagedList extends AbstractPagedList { |
||||
|
||||
/** |
||||
* Constructor of the PagedList Component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
this._onTabSelected = this._onTabSelected.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Renders the paged list. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { pageIndex } = this.state; |
||||
const { disabled, t } = this.props; |
||||
|
||||
return ( |
||||
<View style = { styles.pagedListContainer }> |
||||
<TabBarIOS |
||||
itemPositioning = 'fill' |
||||
style = { styles.pagedList }> |
||||
<TabBarIOS.Item |
||||
onPress = { this._onTabSelected(0) } |
||||
selected = { pageIndex === 0 } |
||||
systemIcon = 'history' > |
||||
<RecentList disabled = { disabled } /> |
||||
</TabBarIOS.Item> |
||||
<TabBarIOS.Item |
||||
icon = { CALENDAR_ICON } |
||||
onPress = { this._onTabSelected(1) } |
||||
selected = { pageIndex === 1 } |
||||
title = { t('welcomepage.calendar') } > |
||||
<MeetingList disabled = { disabled } /> |
||||
</TabBarIOS.Item> |
||||
</TabBarIOS> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
_onTabSelected: number => Function; |
||||
|
||||
/** |
||||
* Constructs a callback to update the selected tab. |
||||
* |
||||
* @private |
||||
* @param {number} tabIndex - The selected tab. |
||||
* @returns {Function} |
||||
*/ |
||||
_onTabSelected(tabIndex) { |
||||
return () => { |
||||
this.setState({ |
||||
pageIndex: tabIndex |
||||
}); |
||||
}; |
||||
} |
||||
} |
||||
|
||||
export default translate(PagedList); |
Loading…
Reference in new issue