Add calendar-sync feature
This commit is contained in:
parent
4dbcaf851f
commit
bba480f329
|
@ -23,6 +23,8 @@ import org.jitsi.meet.sdk.JitsiMeetActivity;
|
|||
import org.jitsi.meet.sdk.JitsiMeetView;
|
||||
import org.jitsi.meet.sdk.JitsiMeetViewListener;
|
||||
|
||||
import com.calendarevents.CalendarEventsPackage;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
@ -103,4 +105,10 @@ public class MainActivity extends JitsiMeetActivity {
|
|||
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
CalendarEventsPackage.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ dependencies {
|
|||
compile project(':react-native-sound')
|
||||
compile project(':react-native-vector-icons')
|
||||
compile project(':react-native-webrtc')
|
||||
compile project(':react-native-calendar-events')
|
||||
}
|
||||
|
||||
// Build process helpers
|
||||
|
|
|
@ -114,6 +114,7 @@ public class JitsiMeetView extends FrameLayout {
|
|||
.setApplication(application)
|
||||
.setBundleAssetName("index.android.bundle")
|
||||
.setJSMainModulePath("index.android")
|
||||
.addPackage(new com.calendarevents.CalendarEventsPackage())
|
||||
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
|
||||
.addPackage(new com.facebook.react.shell.MainReactPackage())
|
||||
.addPackage(new com.i18n.reactnativei18n.ReactNativeI18n())
|
||||
|
|
|
@ -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')
|
||||
include ':react-native-webrtc'
|
||||
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')
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
|
@ -30,6 +30,8 @@ target 'JitsiMeet' do
|
|||
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
|
||||
pod 'RNSound', :path => '../node_modules/react-native-sound'
|
||||
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
|
||||
pod 'react-native-calendar-events',
|
||||
:path => '../node_modules/react-native-calendar-events'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
|
|
|
@ -3,6 +3,8 @@ PODS:
|
|||
- React/Core (= 0.51.0)
|
||||
- react-native-background-timer (2.0.0):
|
||||
- React
|
||||
- react-native-calendar-events (1.4.3):
|
||||
- React
|
||||
- react-native-fetch-blob (0.10.6):
|
||||
- React/Core
|
||||
- react-native-keep-awake (2.0.6):
|
||||
|
@ -52,6 +54,7 @@ PODS:
|
|||
|
||||
DEPENDENCIES:
|
||||
- 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-keep-awake (from `../node_modules/react-native-keep-awake`)
|
||||
- react-native-locale-detector (from `../node_modules/react-native-locale-detector`)
|
||||
|
@ -75,6 +78,8 @@ EXTERNAL SOURCES:
|
|||
:path: ../node_modules/react-native
|
||||
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:
|
||||
:path: ../node_modules/react-native-fetch-blob
|
||||
react-native-keep-awake:
|
||||
|
@ -93,6 +98,7 @@ EXTERNAL SOURCES:
|
|||
SPEC CHECKSUMS:
|
||||
React: 541ba768b9855e10cdc76f55427a5cd0653ca806
|
||||
react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594
|
||||
react-native-calendar-events: fe6fbc8ed337a7423c98f2c9012b25f20444de09
|
||||
react-native-fetch-blob: 63394b1d7b0781547b3e4463b3195790177b1222
|
||||
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
|
||||
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
|
||||
|
@ -101,6 +107,6 @@ SPEC CHECKSUMS:
|
|||
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
|
||||
yoga: 17521bbb0dd54a47c0b3ac43253e78cdac7488e0
|
||||
|
||||
PODFILE CHECKSUM: 1e6ce4da1b385720c726f3f131a6aaf08bf9c0ba
|
||||
PODFILE CHECKSUM: 4a5a310403b99b9c2d619e0b18da89bf0fe5858c
|
||||
|
||||
COCOAPODS: 1.4.0
|
||||
|
|
|
@ -55,6 +55,8 @@
|
|||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>Displays the user's meetings in the app.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Participate in conferences with video.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"audio": "Voice",
|
||||
"video": "Video"
|
||||
},
|
||||
"calendar": "Calendar",
|
||||
"go": "GO",
|
||||
"join": "JOIN",
|
||||
"privacy": "Privacy",
|
||||
|
@ -526,5 +527,10 @@
|
|||
"serverURL": "Server URL",
|
||||
"startWithAudioMuted": "Start with audio muted",
|
||||
"startWithVideoMuted": "Start with video muted"
|
||||
},
|
||||
"calendarSync": {
|
||||
"later": "Later",
|
||||
"next": "Upcoming",
|
||||
"now": "Now"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9845,6 +9845,11 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.0.0.tgz",
|
||||
"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": {
|
||||
"version": "3.27.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-callstats/-/react-native-callstats-3.27.0.tgz",
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
"react-i18next": "4.8.0",
|
||||
"react-native": "0.51.0",
|
||||
"react-native-background-timer": "2.0.0",
|
||||
"react-native-calendar-events": "1.4.3",
|
||||
"react-native-callstats": "3.27.0",
|
||||
"react-native-fetch-blob": "0.10.8",
|
||||
"react-native-img-cache": "1.5.2",
|
||||
|
|
|
@ -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';
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export * from './dateUtil';
|
||||
export * from './helpers';
|
||||
export * from './loadScript';
|
||||
export * from './randomUtil';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -1,5 +1,11 @@
|
|||
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 { Icon } from '../../base/font-icons';
|
||||
|
@ -57,7 +63,7 @@ class RecentList extends AbstractRecentList {
|
|||
= this.dataSource.cloneWithRows(getRecentRooms(_recentList));
|
||||
|
||||
return (
|
||||
<View
|
||||
<SafeAreaView
|
||||
style = { [
|
||||
styles.container,
|
||||
enabled ? null : styles.containerDisabled
|
||||
|
@ -66,7 +72,7 @@ class RecentList extends AbstractRecentList {
|
|||
dataSource = { listViewDataSource }
|
||||
enableEmptySections = { true }
|
||||
renderRow = { this._renderRow } />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -16,17 +16,17 @@ import { Icon } from '../../base/font-icons';
|
|||
import { MEDIA_TYPE } from '../../base/media';
|
||||
import { updateProfile } from '../../base/profile';
|
||||
import { LoadingIndicator, Header, Text } from '../../base/react';
|
||||
import { ColorPalette } from '../../base/styles';
|
||||
import { ColorPalette, PlatformElements } from '../../base/styles';
|
||||
import {
|
||||
createDesiredLocalTracks,
|
||||
destroyLocalTracks
|
||||
} from '../../base/tracks';
|
||||
import { RecentList } from '../../recent-list';
|
||||
import { SettingsView } from '../../settings';
|
||||
|
||||
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
|
||||
import { setSideBarVisible } from '../actions';
|
||||
import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay';
|
||||
import PagedList from './PagedList';
|
||||
import styles, {
|
||||
PLACEHOLDER_TEXT_COLOR,
|
||||
SWITCH_THUMB_COLOR,
|
||||
|
@ -114,27 +114,31 @@ class WelcomePage extends AbstractWelcomePage {
|
|||
</View>
|
||||
</Header>
|
||||
<SafeAreaView style = { styles.roomContainer } >
|
||||
<TextInput
|
||||
accessibilityLabel = { 'Input room name.' }
|
||||
autoCapitalize = 'none'
|
||||
autoComplete = { false }
|
||||
autoCorrect = { false }
|
||||
autoFocus = { false }
|
||||
onBlur = { this._onFieldFocusChange(false) }
|
||||
onChangeText = { this._onRoomChange }
|
||||
onFocus = { this._onFieldFocusChange(true) }
|
||||
onSubmitEditing = { this._onJoin }
|
||||
placeholder = { t('welcomepage.roomname') }
|
||||
placeholderTextColor = { PLACEHOLDER_TEXT_COLOR }
|
||||
returnKeyType = { 'go' }
|
||||
style = { styles.textInput }
|
||||
underlineColorAndroid = 'transparent'
|
||||
value = { this.state.room } />
|
||||
{
|
||||
this._renderHintBox()
|
||||
}
|
||||
<RecentList enabled = { !this.state._fieldFocused } />
|
||||
<View style = { PlatformElements.paddedView } >
|
||||
<TextInput
|
||||
accessibilityLabel = { 'Input room name.' }
|
||||
autoCapitalize = 'none'
|
||||
autoComplete = { false }
|
||||
autoCorrect = { false }
|
||||
autoFocus = { false }
|
||||
onBlur = { this._onFieldFocusChange(false) }
|
||||
onChangeText = { this._onRoomChange }
|
||||
onFocus = { this._onFieldFocusChange(true) }
|
||||
onSubmitEditing = { this._onJoin }
|
||||
placeholder = { t('welcomepage.roomname') }
|
||||
placeholderTextColor = {
|
||||
PLACEHOLDER_TEXT_COLOR
|
||||
}
|
||||
returnKeyType = { 'go' }
|
||||
style = { styles.textInput }
|
||||
underlineColorAndroid = 'transparent'
|
||||
value = { this.state.room } />
|
||||
{
|
||||
this._renderHintBox()
|
||||
}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
<PagedList disabled = { this.state._fieldFocused } />
|
||||
<SettingsView />
|
||||
</View>
|
||||
<WelcomePageSideBar />
|
||||
|
|
|
@ -149,15 +149,42 @@ export default createStyleSheet({
|
|||
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.
|
||||
*/
|
||||
roomContainer: {
|
||||
alignSelf: 'stretch',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
margin: BoxModel.margin,
|
||||
marginTop: BoxModel.margin * 2
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue