Add calendar-sync feature

This commit is contained in:
zbettenbuk 2018-02-08 19:50:19 +01:00 committed by Lyubo Marinov
parent 4dbcaf851f
commit bba480f329
30 changed files with 1070 additions and 30 deletions

View File

@ -23,6 +23,8 @@ import org.jitsi.meet.sdk.JitsiMeetActivity;
import org.jitsi.meet.sdk.JitsiMeetView; import org.jitsi.meet.sdk.JitsiMeetView;
import org.jitsi.meet.sdk.JitsiMeetViewListener; import org.jitsi.meet.sdk.JitsiMeetViewListener;
import com.calendarevents.CalendarEventsPackage;
import java.util.Map; import java.util.Map;
/** /**
@ -103,4 +105,10 @@ public class MainActivity extends JitsiMeetActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
} }
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
CalendarEventsPackage.onRequestPermissionsResult(requestCode, permissions, grantResults);
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
} }

View File

@ -32,6 +32,7 @@ dependencies {
compile project(':react-native-sound') compile project(':react-native-sound')
compile project(':react-native-vector-icons') compile project(':react-native-vector-icons')
compile project(':react-native-webrtc') compile project(':react-native-webrtc')
compile project(':react-native-calendar-events')
} }
// Build process helpers // Build process helpers

View File

@ -114,6 +114,7 @@ public class JitsiMeetView extends FrameLayout {
.setApplication(application) .setApplication(application)
.setBundleAssetName("index.android.bundle") .setBundleAssetName("index.android.bundle")
.setJSMainModulePath("index.android") .setJSMainModulePath("index.android")
.addPackage(new com.calendarevents.CalendarEventsPackage())
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage()) .addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
.addPackage(new com.facebook.react.shell.MainReactPackage()) .addPackage(new com.facebook.react.shell.MainReactPackage())
.addPackage(new com.i18n.reactnativei18n.ReactNativeI18n()) .addPackage(new com.i18n.reactnativei18n.ReactNativeI18n())

View File

@ -17,3 +17,5 @@ include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-webrtc' include ':react-native-webrtc'
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android') project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
include ':react-native-calendar-events'
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')

BIN
images/calendar@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
images/calendar@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -30,6 +30,8 @@ target 'JitsiMeet' do
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc' pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'RNSound', :path => '../node_modules/react-native-sound' pod 'RNSound', :path => '../node_modules/react-native-sound'
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons' pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
pod 'react-native-calendar-events',
:path => '../node_modules/react-native-calendar-events'
end end
post_install do |installer| post_install do |installer|

View File

@ -3,6 +3,8 @@ PODS:
- React/Core (= 0.51.0) - React/Core (= 0.51.0)
- react-native-background-timer (2.0.0): - react-native-background-timer (2.0.0):
- React - React
- react-native-calendar-events (1.4.3):
- React
- react-native-fetch-blob (0.10.6): - react-native-fetch-blob (0.10.6):
- React/Core - React/Core
- react-native-keep-awake (2.0.6): - react-native-keep-awake (2.0.6):
@ -52,6 +54,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- react-native-background-timer (from `../node_modules/react-native-background-timer`) - react-native-background-timer (from `../node_modules/react-native-background-timer`)
- react-native-calendar-events (from `../node_modules/react-native-calendar-events`)
- react-native-fetch-blob (from `../node_modules/react-native-fetch-blob`) - react-native-fetch-blob (from `../node_modules/react-native-fetch-blob`)
- react-native-keep-awake (from `../node_modules/react-native-keep-awake`) - react-native-keep-awake (from `../node_modules/react-native-keep-awake`)
- react-native-locale-detector (from `../node_modules/react-native-locale-detector`) - react-native-locale-detector (from `../node_modules/react-native-locale-detector`)
@ -75,6 +78,8 @@ EXTERNAL SOURCES:
:path: ../node_modules/react-native :path: ../node_modules/react-native
react-native-background-timer: react-native-background-timer:
:path: ../node_modules/react-native-background-timer :path: ../node_modules/react-native-background-timer
react-native-calendar-events:
:path: ../node_modules/react-native-calendar-events
react-native-fetch-blob: react-native-fetch-blob:
:path: ../node_modules/react-native-fetch-blob :path: ../node_modules/react-native-fetch-blob
react-native-keep-awake: react-native-keep-awake:
@ -93,6 +98,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
React: 541ba768b9855e10cdc76f55427a5cd0653ca806 React: 541ba768b9855e10cdc76f55427a5cd0653ca806
react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594 react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594
react-native-calendar-events: fe6fbc8ed337a7423c98f2c9012b25f20444de09
react-native-fetch-blob: 63394b1d7b0781547b3e4463b3195790177b1222 react-native-fetch-blob: 63394b1d7b0781547b3e4463b3195790177b1222
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94 react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1 react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
@ -101,6 +107,6 @@ SPEC CHECKSUMS:
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44 RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
yoga: 17521bbb0dd54a47c0b3ac43253e78cdac7488e0 yoga: 17521bbb0dd54a47c0b3ac43253e78cdac7488e0
PODFILE CHECKSUM: 1e6ce4da1b385720c726f3f131a6aaf08bf9c0ba PODFILE CHECKSUM: 4a5a310403b99b9c2d619e0b18da89bf0fe5858c
COCOAPODS: 1.4.0 COCOAPODS: 1.4.0

View File

@ -55,6 +55,8 @@
</dict> </dict>
</dict> </dict>
</dict> </dict>
<key>NSCalendarsUsageDescription</key>
<string>Displays the user's meetings in the app.</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Participate in conferences with video.</string> <string>Participate in conferences with video.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>

View File

@ -51,6 +51,7 @@
"audio": "Voice", "audio": "Voice",
"video": "Video" "video": "Video"
}, },
"calendar": "Calendar",
"go": "GO", "go": "GO",
"join": "JOIN", "join": "JOIN",
"privacy": "Privacy", "privacy": "Privacy",
@ -526,5 +527,10 @@
"serverURL": "Server URL", "serverURL": "Server URL",
"startWithAudioMuted": "Start with audio muted", "startWithAudioMuted": "Start with audio muted",
"startWithVideoMuted": "Start with video muted" "startWithVideoMuted": "Start with video muted"
},
"calendarSync": {
"later": "Later",
"next": "Upcoming",
"now": "Now"
} }
} }

5
package-lock.json generated
View File

@ -9845,6 +9845,11 @@
"resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.0.0.tgz",
"integrity": "sha512-vLNJIedXQZN4p3ChFsAgVHacnJqQMnLl+wBsnZuliRkmsjEHo8kQOA9fnLih/OoiDi1O3eHQvXC5L8f+RYiKgw==" "integrity": "sha512-vLNJIedXQZN4p3ChFsAgVHacnJqQMnLl+wBsnZuliRkmsjEHo8kQOA9fnLih/OoiDi1O3eHQvXC5L8f+RYiKgw=="
}, },
"react-native-calendar-events": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/react-native-calendar-events/-/react-native-calendar-events-1.4.3.tgz",
"integrity": "sha1-KYBOi0TWlG5pq1ogkC2USe0xXEc="
},
"react-native-callstats": { "react-native-callstats": {
"version": "3.27.0", "version": "3.27.0",
"resolved": "https://registry.npmjs.org/react-native-callstats/-/react-native-callstats-3.27.0.tgz", "resolved": "https://registry.npmjs.org/react-native-callstats/-/react-native-callstats-3.27.0.tgz",

View File

@ -55,6 +55,7 @@
"react-i18next": "4.8.0", "react-i18next": "4.8.0",
"react-native": "0.51.0", "react-native": "0.51.0",
"react-native-background-timer": "2.0.0", "react-native-background-timer": "2.0.0",
"react-native-calendar-events": "1.4.3",
"react-native-callstats": "3.27.0", "react-native-callstats": "3.27.0",
"react-native-fetch-blob": "0.10.8", "react-native-fetch-blob": "0.10.8",
"react-native-img-cache": "1.5.2", "react-native-img-cache": "1.5.2",

View File

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

View File

@ -1,2 +1,3 @@
export * from './BoxModel'; export * from './BoxModel';
export * from './ColorPalette'; export * from './ColorPalette';
export * from './PlatformElements';

View File

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

View File

@ -1,3 +1,4 @@
export * from './dateUtil';
export * from './helpers'; export * from './helpers';
export * from './loadScript'; export * from './loadScript';
export * from './randomUtil'; export * from './randomUtil';

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default as MeetingList } from './MeetingList';

View File

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

View File

@ -0,0 +1,4 @@
export * from './components';
import './middleware';
import './reducer';

View File

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

View File

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

View File

@ -1,5 +1,11 @@
import React from 'react'; import React from 'react';
import { ListView, Text, TouchableHighlight, View } from 'react-native'; import {
ListView,
SafeAreaView,
Text,
TouchableHighlight,
View
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Icon } from '../../base/font-icons'; import { Icon } from '../../base/font-icons';
@ -57,7 +63,7 @@ class RecentList extends AbstractRecentList {
= this.dataSource.cloneWithRows(getRecentRooms(_recentList)); = this.dataSource.cloneWithRows(getRecentRooms(_recentList));
return ( return (
<View <SafeAreaView
style = { [ style = { [
styles.container, styles.container,
enabled ? null : styles.containerDisabled enabled ? null : styles.containerDisabled
@ -66,7 +72,7 @@ class RecentList extends AbstractRecentList {
dataSource = { listViewDataSource } dataSource = { listViewDataSource }
enableEmptySections = { true } enableEmptySections = { true }
renderRow = { this._renderRow } /> renderRow = { this._renderRow } />
</View> </SafeAreaView>
); );
} }

View File

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

View File

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

View File

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

View File

@ -16,17 +16,17 @@ import { Icon } from '../../base/font-icons';
import { MEDIA_TYPE } from '../../base/media'; import { MEDIA_TYPE } from '../../base/media';
import { updateProfile } from '../../base/profile'; import { updateProfile } from '../../base/profile';
import { LoadingIndicator, Header, Text } from '../../base/react'; import { LoadingIndicator, Header, Text } from '../../base/react';
import { ColorPalette } from '../../base/styles'; import { ColorPalette, PlatformElements } from '../../base/styles';
import { import {
createDesiredLocalTracks, createDesiredLocalTracks,
destroyLocalTracks destroyLocalTracks
} from '../../base/tracks'; } from '../../base/tracks';
import { RecentList } from '../../recent-list';
import { SettingsView } from '../../settings'; import { SettingsView } from '../../settings';
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
import { setSideBarVisible } from '../actions'; import { setSideBarVisible } from '../actions';
import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay'; import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay';
import PagedList from './PagedList';
import styles, { import styles, {
PLACEHOLDER_TEXT_COLOR, PLACEHOLDER_TEXT_COLOR,
SWITCH_THUMB_COLOR, SWITCH_THUMB_COLOR,
@ -114,27 +114,31 @@ class WelcomePage extends AbstractWelcomePage {
</View> </View>
</Header> </Header>
<SafeAreaView style = { styles.roomContainer } > <SafeAreaView style = { styles.roomContainer } >
<TextInput <View style = { PlatformElements.paddedView } >
accessibilityLabel = { 'Input room name.' } <TextInput
autoCapitalize = 'none' accessibilityLabel = { 'Input room name.' }
autoComplete = { false } autoCapitalize = 'none'
autoCorrect = { false } autoComplete = { false }
autoFocus = { false } autoCorrect = { false }
onBlur = { this._onFieldFocusChange(false) } autoFocus = { false }
onChangeText = { this._onRoomChange } onBlur = { this._onFieldFocusChange(false) }
onFocus = { this._onFieldFocusChange(true) } onChangeText = { this._onRoomChange }
onSubmitEditing = { this._onJoin } onFocus = { this._onFieldFocusChange(true) }
placeholder = { t('welcomepage.roomname') } onSubmitEditing = { this._onJoin }
placeholderTextColor = { PLACEHOLDER_TEXT_COLOR } placeholder = { t('welcomepage.roomname') }
returnKeyType = { 'go' } placeholderTextColor = {
style = { styles.textInput } PLACEHOLDER_TEXT_COLOR
underlineColorAndroid = 'transparent' }
value = { this.state.room } /> returnKeyType = { 'go' }
{ style = { styles.textInput }
this._renderHintBox() underlineColorAndroid = 'transparent'
} value = { this.state.room } />
<RecentList enabled = { !this.state._fieldFocused } /> {
this._renderHintBox()
}
</View>
</SafeAreaView> </SafeAreaView>
<PagedList disabled = { this.state._fieldFocused } />
<SettingsView /> <SettingsView />
</View> </View>
<WelcomePageSideBar /> <WelcomePageSideBar />

View File

@ -149,15 +149,42 @@ export default createStyleSheet({
flexDirection: 'column' flexDirection: 'column'
}, },
pageIndicator: {
backgroundColor: 'rgba(255, 255, 255, 0.2)',
height: 3,
marginHorizontal: 7,
width: 20
},
pageIndicatorActive: {
backgroundColor: 'rgba(255, 255, 255, 0.8)'
},
pageIndicatorContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
padding: 12
},
/**
* Top level style of the paged list.
*/
pagedList: {
flex: 1
},
pagedListContainer: {
flex: 1,
flexDirection: 'column'
},
/** /**
* Container for room name input box and 'join' button. * Container for room name input box and 'join' button.
*/ */
roomContainer: { roomContainer: {
alignSelf: 'stretch', alignSelf: 'stretch',
flex: 1, flexDirection: 'column'
flexDirection: 'column',
margin: BoxModel.margin,
marginTop: BoxModel.margin * 2
}, },
/** /**