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.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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')
|
||||||
|
|
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 '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|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 './BoxModel';
|
||||||
export * from './ColorPalette';
|
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 './helpers';
|
||||||
export * from './loadScript';
|
export * from './loadScript';
|
||||||
export * from './randomUtil';
|
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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 { 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 />
|
||||||
|
|
|
@ -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
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue