Compare commits

..

1 Commits

Author SHA1 Message Date
hristoterezov
9fa1b210f5 feat(loadScript): Use the function from js-utils 2018-03-22 13:15:14 -05:00
37 changed files with 221 additions and 747 deletions

View File

@@ -255,7 +255,6 @@
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDebug;
CODE_SIGN_ENTITLEMENTS = app.entitlements;
CURRENT_PROJECT_VERSION = 1;
@@ -287,7 +286,6 @@
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconRelease;
CODE_SIGN_ENTITLEMENTS = app.entitlements;
CURRENT_PROJECT_VERSION = 1;

View File

@@ -32,8 +32,8 @@ class ViewController: UIViewController {
@IBAction func openJitsiMeet(sender: Any?) {
let jitsiMeetCoordinator = JitsiMeetPresentationCoordinator()
self.jitsiMeetCoordinator = jitsiMeetCoordinator
jitsiMeetCoordinator.jitsiMeetView.welcomePageEnabled = true
jitsiMeetCoordinator.jitsiMeetView.load(nil)
jitsiMeetCoordinator.jitsiMeetView().welcomePageEnabled = true
jitsiMeetCoordinator.jitsiMeetView().load(nil)
jitsiMeetCoordinator.show()
}
}

View File

@@ -405,7 +405,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -458,7 +458,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";

View File

@@ -20,20 +20,8 @@ import Foundation
/// an external window that can be resized and dragged with custom PiP mode
open class JitsiMeetPresentationCoordinator: NSObject {
public let meetViewController: JitsiMeetViewController
public let meetWindow: PiPWindow
public var isInPiP: Bool {
get {
return meetWindow.isInPiP
}
}
public var jitsiMeetView: JitsiMeetView {
get {
return meetViewController.jitsiMeetView
}
}
fileprivate let meetViewController: JitsiMeetViewController
fileprivate let meetWindow: PiPWindow
public init(meetViewController: JitsiMeetViewController? = nil,
meetWindow: PiPWindow? = nil) {
@@ -46,12 +34,14 @@ open class JitsiMeetPresentationCoordinator: NSObject {
configureMeetViewController()
}
/// Show window with jitsi meet and perform a completion closure
public func jitsiMeetView() -> JitsiMeetView {
return meetViewController.jitsiMeetView
}
open func show(completion: CompletionAction? = nil) {
meetWindow.show(completion: completion)
}
/// Hide window with jitsi meet and perform a completion closure
open func hide(completion: CompletionAction? = nil) {
meetWindow.hide(completion: completion)
}
@@ -87,7 +77,7 @@ extension JitsiMeetPresentationCoordinator: JitsiMeetViewControllerDelegate {
switch to {
case .enterPictureInPicture:
meetWindow.enterPictureInPicture()
case .sizeChange:
case .traitChange:
// resize to full screen if rotation happens
if meetWindow.isInPiP {
meetWindow.exitPictureInPicture()

View File

@@ -19,8 +19,8 @@ public enum JitsiMeetPresentationUpdate {
/// The conference wants to enter Picture-in-Picture
case enterPictureInPicture
/// A screen size change (usually screen rotation)
case sizeChange
/// A system traitCollectionChange (usually screen rotation)
case traitChange
}
public protocol JitsiMeetViewControllerDelegate: class {
@@ -59,10 +59,9 @@ open class JitsiMeetViewController: UIViewController {
jitsiMeetView.delegate = self
}
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
delegate?.performPresentationUpdate(to: .sizeChange)
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
delegate?.performPresentationUpdate(to: .traitChange)
}
}

View File

@@ -32,17 +32,7 @@ open class PiPWindow: UIWindow {
}
/// The size ratio for root view controller view when in PiP mode
public var pipSizeRatio: CGFloat = {
let deviceIdiom = UIScreen.main.traitCollection.userInterfaceIdiom
switch (deviceIdiom) {
case .pad:
return 0.25
case .phone:
return 0.33
default:
return 0.25
}
}()
public var pipSizeRatio: CGFloat = 0.333
/// The PiP state of this contents of the window
private(set) var isInPiP: Bool = false

View File

@@ -539,16 +539,11 @@
"later": "Later",
"next": "Upcoming",
"nextMeeting": "next meeting",
"now": "Now",
"permissionButton": "Open settings",
"permissionMessage": "Calendar permission is required to list your meetings in the app."
"now": "Now"
},
"recentList": {
"today": "Today",
"yesterday": "Yesterday",
"earlier": "Earlier"
},
"sectionList": {
"pullToRefresh": "Pull to refresh"
}
}

13
package-lock.json generated
View File

@@ -6785,6 +6785,11 @@
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.2.tgz",
"integrity": "sha512-pH3vDzpczdsKHdZ9xxR3O46unSjisgVx0IImay7Zz2EdhRVbCkj+nthx9OuuWEhakx9FAO+fNVGrF0rZ2oMOvw=="
},
"immutable": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
"integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
},
"import-local": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-0.1.1.tgz",
@@ -8639,6 +8644,14 @@
"gauge": "1.2.7"
}
},
"nuclear-js": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/nuclear-js/-/nuclear-js-1.4.0.tgz",
"integrity": "sha1-bJwAGwZz8K6dj4sYjE2gTtaTp74=",
"requires": {
"immutable": "3.8.2"
}
},
"num2fraction": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",

View File

@@ -49,6 +49,7 @@
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0503ec4d3f175b154b1c6fd7037520ce0768fa58",
"lodash": "4.17.4",
"moment": "2.19.4",
"nuclear-js": "1.4.0",
"postis": "2.2.0",
"prop-types": "15.6.0",
"react": "16.2.0",

View File

@@ -129,21 +129,16 @@ export function createFeedbackOpenEvent() {
}
/**
* Creates an event for an action regarding the AddPeopleDialog (invites).
* Creates an event which indicates that the invite dialog was closed. This is
* not a TYPE_UI event, since it is not necessarily the result of a user
* interaction.
*
* @param {string} action - The action that the event represents.
* @param {string} actionSubject - The subject that was acted upon.
* @param {boolean} attributes - Additional attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createInviteDialogEvent(
action, actionSubject, attributes = {}) {
export function createInviteDialogClosedEvent() {
return {
action,
actionSubject,
attributes,
source: 'inviteDialog'
action: 'invite.dialog.closed'
};
}

View File

@@ -65,7 +65,6 @@ const WHITELISTED_KEYS = [
'firefox_fake_device',
'forceJVB121Ratio',
'gatherStats',
'googleApiApplicationClientID',
'hiddenDomain',
'hosts',
'iAmRecorder',

View File

@@ -10,10 +10,6 @@ import {
import styles, { UNDERLAY_COLOR } from './styles';
import { translate } from '../../../i18n';
import { Icon } from '../../../font-icons';
type Props = {
/**
@@ -21,11 +17,6 @@ type Props = {
*/
disabled: boolean,
/**
* The translate function.
*/
t: Function,
/**
* Function to be invoked when an item is pressed. The item's URL is passed.
*/
@@ -36,11 +27,6 @@ type Props = {
*/
onRefresh: Function,
/**
* Function to override the rendered default empty list component.
*/
renderListEmptyComponent: Function,
/**
* Sections to be rendered in the following format:
*
@@ -67,7 +53,7 @@ type Props = {
* property and navigates to (probably) meetings, such as the recent list
* or the meeting list components.
*/
class NavigateSectionList extends Component<Props> {
export default class NavigateSectionList extends Component<Props> {
/**
* Constructor of the NavigateSectionList component.
*
@@ -83,8 +69,6 @@ class NavigateSectionList extends Component<Props> {
this._renderItem = this._renderItem.bind(this);
this._renderItemLine = this._renderItemLine.bind(this);
this._renderItemLines = this._renderItemLines.bind(this);
this._renderListEmptyComponent
= this._renderListEmptyComponent.bind(this);
this._renderSection = this._renderSection.bind(this);
}
@@ -96,16 +80,12 @@ class NavigateSectionList extends Component<Props> {
* @inheritdoc
*/
render() {
const { renderListEmptyComponent, sections } = this.props;
const { sections } = this.props;
return (
<SafeAreaView
style = { styles.container } >
<SectionList
ListEmptyComponent = {
renderListEmptyComponent
|| this._renderListEmptyComponent
}
keyExtractor = { this._getItemKey }
onRefresh = { this._onRefresh }
refreshing = { false }
@@ -294,34 +274,6 @@ class NavigateSectionList extends Component<Props> {
return null;
}
_renderListEmptyComponent: () => Object
/**
* Renders a component to display when the list is empty.
*
* @private
* @param {Object} section - The section being rendered.
* @returns {React$Node}
*/
_renderListEmptyComponent() {
const { t, onRefresh } = this.props;
if (typeof onRefresh === 'function') {
return (
<View style = { styles.pullToRefresh }>
<Text style = { styles.pullToRefreshText }>
{ t('sectionList.pullToRefresh') }
</Text>
<Icon
name = 'menu-down'
style = { styles.pullToRefreshIcon } />
</View>
);
}
return null;
}
_renderSection: Object => Object
/**
@@ -341,5 +293,3 @@ class NavigateSectionList extends Component<Props> {
);
}
}
export default translate(NavigateSectionList);

View File

@@ -180,25 +180,6 @@ const SECTION_LIST_STYLES = {
fontWeight: 'normal'
},
pullToRefresh: {
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
padding: 20
},
pullToRefreshIcon: {
backgroundColor: 'transparent',
color: OVERLAY_FONT_COLOR,
fontSize: 20
},
pullToRefreshText: {
backgroundColor: 'transparent',
color: OVERLAY_FONT_COLOR
},
touchableView: {
flexDirection: 'row'
}

View File

@@ -6,8 +6,6 @@ import React, { Component } from 'react';
import InlineDialogFailure from './InlineDialogFailure';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* A MultiSelect that is also auto-completing.
*/
@@ -292,9 +290,7 @@ class MultiSelectAutocomplete extends Component {
error: false
});
})
.catch(error => {
logger.error('MultiSelectAutocomplete error in query', error);
.catch(() => {
this.setState({
error: true,
loading: false,

View File

@@ -7,16 +7,8 @@ import type { Dispatch } from 'redux';
/**
* Size threshold for determining if we are in reduced UI mode or not.
*
* FIXME The logic to base {@code reducedUI} on a hardcoded width or height is
* very brittle because it's completely disconnected from the UI which wants to
* be rendered and, naturally, it broke on iPad where even the secondary Toolbar
* didn't fit in the height. We do need to measure the actual UI at runtime and
* determine whether and how to render it. I'm bumping from 240 to 300 because I
* don't have the time now to refactor {@code ReducedUIDetector} or rip it out
* completely.
*/
const REDUCED_UI_THRESHOLD = 300;
const REDUCED_UI_THRESHOLD = 240;
/**
* Sets the aspect ratio of the app's user interface based on specific width and

View File

@@ -1,28 +0,0 @@
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Wrapper around fetch GET requests to handle json-ifying the response
* and logging errors.
*
* @param {string} url - The URL to perform a GET against.
* @returns {Promise<Object>} The response body, in JSON format, will be
* through the Promise.
*/
export function doGetJSON(url) {
return fetch(url)
.then(response => {
const jsonify = response.json();
if (response.ok) {
return jsonify;
}
return jsonify
.then(result => Promise.reject(result));
})
.catch(error => {
logger.error('Error performing get:', url, error);
return Promise.reject(error);
});
}

View File

@@ -1,5 +1,4 @@
export * from './helpers';
export * from './httpUtils';
export * from './loadScript';
export * from './randomUtil';
export * from './uri';

View File

@@ -1,17 +1,17 @@
// @flow
declare var JitsiMeetJS: Object;
import { loadScript as loadScriptF } from 'js-utils';
/**
* Loads a script from a specific URL. The script will be interpreted upon load.
*
* @param {string} url - The url to be loaded.
* @returns {Promise} Resolved with no arguments when the script is loaded and
* rejected with the error from JitsiMeetJS.ScriptUtil.loadScript method.
* rejected with the error from loadScriptF method.
*/
export function loadScript(url: string): Promise<void> {
return new Promise((resolve, reject) =>
JitsiMeetJS.util.ScriptUtil.loadScript(
loadScriptF(
url,
/* async */ true,
/* prepend */ false,

View File

@@ -1,12 +1,5 @@
// @flow
/**
* Action to signal that calendar access has already been requested
* since the app started, so no new request should be done unless the
* user explicitly tries to refresh the calendar view.
*/
export const CALENDAR_ACCESS_REQUESTED = Symbol('CALENDAR_ACCESS_REQUESTED');
/**
* Action to update the current calendar entry list in the store.
*/

View File

@@ -1,28 +1,10 @@
// @flow
import {
CALENDAR_ACCESS_REQUESTED,
NEW_CALENDAR_ENTRY_LIST,
NEW_KNOWN_DOMAIN,
REFRESH_CALENDAR_ENTRY_LIST
} from './actionTypes';
/**
* Sends an action to signal that a calendar access has been requested. For
* more info see the {@link CALENDAR_ACCESS_REQUESTED}.
*
* @param {string | undefined} status - The result of the last calendar
* access request.
* @returns {{
* type: CALENDAR_ACCESS_REQUESTED
* }}
*/
export function updateCalendarAccessStatus(status: ?string) {
return {
status,
type: CALENDAR_ACCESS_REQUESTED
};
}
/**
* Sends an action to add a new known domain if not present yet.
*
@@ -42,16 +24,12 @@ export function maybeAddNewKnownDomain(domainName: string) {
/**
* Sends an action to refresh the entry list (fetches new data).
*
* @param {boolean|undefined} forcePermission - Whether to force to re-ask
* for the permission or not.
* @returns {{
* type: REFRESH_CALENDAR_ENTRY_LIST,
* forcePermission: boolean
* type: REFRESH_CALENDAR_ENTRY_LIST
* }}
*/
export function refreshCalendarEntryList(forcePermission: boolean = false) {
export function refreshCalendarEntryList() {
return {
forcePermission,
type: REFRESH_CALENDAR_ENTRY_LIST
};
}

View File

@@ -1,16 +1,12 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
import styles from './styles';
import { refreshCalendarEntryList } from '../actions';
import { appNavigate } from '../../app';
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
import { openSettings } from '../../mobile/permissions';
type Props = {
@@ -32,11 +28,6 @@ type Props = {
*/
displayed: boolean,
/**
* The current state of the calendar access permission.
*/
_calendarAccessStatus: string,
/**
* The calendar event list.
*/
@@ -52,6 +43,8 @@ type Props = {
* Component to display a list of events from the (mobile) user's calendar.
*/
class MeetingList extends Component<Props> {
_initialLoaded: boolean
/**
* Default values for the component's props.
*/
@@ -67,14 +60,6 @@ class MeetingList extends Component<Props> {
constructor(props) {
super(props);
const { dispatch, displayed } = props;
if (displayed) {
dispatch(refreshCalendarEntryList());
}
this._getRenderListEmptyComponent
= this._getRenderListEmptyComponent.bind(this);
this._onPress = this._onPress.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this._toDisplayableItem = this._toDisplayableItem.bind(this);
@@ -88,11 +73,16 @@ class MeetingList extends Component<Props> {
* @inheritdoc
*/
componentWillReceiveProps(newProps) {
const { displayed } = this.props;
if (newProps.displayed && !displayed) {
// This is a conditional logic to refresh the calendar entries (thus
// to request access to calendar) on component first receives a
// displayed=true prop - to avoid requesting calendar access on
// app start.
if (!this._initialLoaded
&& newProps.displayed
&& !this.props.displayed) {
const { dispatch } = this.props;
this._initialLoaded = true;
dispatch(refreshCalendarEntryList());
}
}
@@ -110,45 +100,10 @@ class MeetingList extends Component<Props> {
disabled = { disabled }
onPress = { this._onPress }
onRefresh = { this._onRefresh }
renderListEmptyComponent = {
this._getRenderListEmptyComponent
}
sections = { this._toDisplayableList() } />
);
}
_getRenderListEmptyComponent: () => Object
/**
* Returns a list empty component if a custom one has to be rendered instead
* of the default one in the {@link NavigateSectionList}.
*
* @private
* @returns {Component}
*/
_getRenderListEmptyComponent() {
const { _calendarAccessStatus, t } = this.props;
if (_calendarAccessStatus === 'denied') {
return (
<View style = { styles.noPermissionMessageView }>
<Text style = { styles.noPermissionMessageText }>
{ t('calendarSync.permissionMessage') }
</Text>
<TouchableOpacity
onPress = { openSettings }
style = { styles.noPermissionMessageButton } >
<Text style = { styles.noPermissionMessageButtonText }>
{ t('calendarSync.permissionButton') }
</Text>
</TouchableOpacity>
</View>
);
}
return null;
}
_onPress: string => Function
/**
@@ -175,7 +130,7 @@ class MeetingList extends Component<Props> {
_onRefresh() {
const { dispatch } = this.props;
dispatch(refreshCalendarEntryList(true));
dispatch(refreshCalendarEntryList());
}
_toDisplayableItem: Object => Object
@@ -264,12 +219,12 @@ class MeetingList extends Component<Props> {
* @returns {string}
*/
_toDateString(event) {
const startDateTime
= getLocalizedDateFormatter(event.startDate).format('lll');
const endTime
= getLocalizedDateFormatter(event.endDate).format('LT');
/* eslint-disable max-len */
const startDateTime = getLocalizedDateFormatter(event.startDate).format('lll');
const endTime = getLocalizedDateFormatter(event.endDate).format('LT');
return `${startDateTime} - ${endTime}`;
/* eslint-enable max-len */
}
}
@@ -282,11 +237,8 @@ class MeetingList extends Component<Props> {
* }}
*/
export function _mapStateToProps(state: Object) {
const calendarSyncState = state['features/calendar-sync'];
return {
_calendarAccessStatus: calendarSyncState.calendarAccessStatus,
_eventList: calendarSyncState.events
_eventList: state['features/calendar-sync'].events
};
}

View File

@@ -1,4 +1,4 @@
import { ColorPalette, createStyleSheet } from '../../base/styles';
import { createStyleSheet } from '../../base/styles';
const NOTIFICATION_SIZE = 55;
@@ -8,46 +8,6 @@ const NOTIFICATION_SIZE = 55;
*/
export default createStyleSheet({
/**
* Button style of the open settings button.
*/
noPermissionMessageButton: {
backgroundColor: ColorPalette.blue,
borderColor: ColorPalette.blue,
borderRadius: 4,
borderWidth: 1,
height: 30,
justifyContent: 'center',
margin: 15,
paddingHorizontal: 20
},
/**
* Text style of the open settings button.
*/
noPermissionMessageButtonText: {
color: ColorPalette.white
},
/**
* Text style of the no permission message.
*/
noPermissionMessageText: {
backgroundColor: 'transparent',
color: 'rgba(255, 255, 255, 0.6)'
},
/**
* Top level view of the no permission message.
*/
noPermissionMessageView: {
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
padding: 20
},
/**
* The top level container of the notification.
*/

View File

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

View File

@@ -2,18 +2,13 @@
import Logger from 'jitsi-meet-logger';
import RNCalendarEvents from 'react-native-calendar-events';
import { APP_WILL_MOUNT } from '../app';
import { SET_ROOM } from '../base/conference';
import { MiddlewareRegistry } from '../base/redux';
import { APP_LINK_SCHEME, parseURIString } from '../base/util';
import { APP_STATE_CHANGED } from '../mobile/background';
import { APP_WILL_MOUNT } from '../app';
import {
maybeAddNewKnownDomain,
updateCalendarAccessStatus,
updateCalendarEntryList
} from './actions';
import { maybeAddNewKnownDomain, updateCalendarEntryList } from './actions';
import { REFRESH_CALENDAR_ENTRY_LIST } from './actionTypes';
const FETCH_END_DAYS = 10;
@@ -25,15 +20,12 @@ MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case APP_STATE_CHANGED:
_maybeClearAccessStatus(store, action);
break;
case APP_WILL_MOUNT:
_ensureDefaultServer(store);
_fetchCalendarEntries(store, false, false);
_fetchCalendarEntries(store, false);
break;
case REFRESH_CALENDAR_ENTRY_LIST:
_fetchCalendarEntries(store, true, action.forcePermission);
_fetchCalendarEntries(store, true);
break;
case SET_ROOM:
_parseAndAddDomain(store);
@@ -42,53 +34,34 @@ MiddlewareRegistry.register(store => next => action => {
return result;
});
/**
* Clears the calendar access status when the app comes back from
* the background. This is needed as some users may never quit the
* app, but puts it into the background and we need to try to request
* for a permission as often as possible, but not annoyingly often.
*
* @private
* @param {Object} store - The redux store.
* @param {Object} action - The Redux action.
* @returns {void}
*/
function _maybeClearAccessStatus(store, action) {
const { appState } = action;
if (appState === 'background') {
const { dispatch } = store;
dispatch(updateCalendarAccessStatus(undefined));
}
}
/**
* Ensures calendar access if possible and resolves the promise if it's granted.
*
* @private
* @param {boolean} promptForPermission - Flag to tell the app if it should
* prompt for a calendar permission if it wasn't granted yet.
* @param {Function} dispatch - The Redux dispatch function.
* @returns {Promise}
*/
function _ensureCalendarAccess(promptForPermission, dispatch) {
function _ensureCalendarAccess(promptForPermission) {
return new Promise((resolve, reject) => {
RNCalendarEvents.authorizationStatus()
.then(status => {
if (status === 'authorized') {
resolve(true);
resolve();
} else if (promptForPermission) {
RNCalendarEvents.authorizeEventStore()
.then(result => {
dispatch(updateCalendarAccessStatus(result));
resolve(result === 'authorized');
if (result === 'authorized') {
resolve();
} else {
reject(result);
}
})
.catch(error => {
reject(error);
});
} else {
resolve(false);
reject(status);
}
})
.catch(error => {
@@ -118,49 +91,64 @@ function _ensureDefaultServer(store) {
*
* @private
* @param {Object} store - The redux store.
* @param {boolean} maybePromptForPermission - Flag to tell the app if it should
* @param {boolean} promptForPermission - Flag to tell the app if it should
* prompt for a calendar permission if it wasn't granted yet.
* @param {boolean|undefined} forcePermission - Whether to force to re-ask
* for the permission or not.
* @returns {void}
*/
function _fetchCalendarEntries(
store,
maybePromptForPermission,
forcePermission
) {
const { dispatch } = store;
const state = store.getState()['features/calendar-sync'];
const { calendarAccessStatus } = state;
const promptForPermission
= (maybePromptForPermission && !calendarAccessStatus)
|| forcePermission;
function _fetchCalendarEntries(store, promptForPermission) {
_ensureCalendarAccess(promptForPermission)
.then(() => {
const startDate = new Date();
const endDate = new Date();
_ensureCalendarAccess(promptForPermission, dispatch)
.then(accessGranted => {
if (accessGranted) {
const startDate = new Date();
const endDate = new Date();
startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
RNCalendarEvents.fetchAllEvents(
startDate.getTime(),
endDate.getTime(),
[]
)
.then(events => {
const { knownDomains } = store.getState()['features/calendar-sync'];
const eventList = [];
RNCalendarEvents.fetchAllEvents(
startDate.getTime(),
endDate.getTime(),
[]
)
.then(events => {
const { knownDomains } = state;
if (events && events.length) {
for (const event of events) {
const jitsiURL = _getURLFromEvent(event, knownDomains);
const now = Date.now();
_updateCalendarEntries(events, knownDomains, dispatch);
})
.catch(error => {
logger.error('Error fetching calendar.', error);
});
} else {
logger.warn('Calendar access not granted.');
}
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);
@@ -221,70 +209,3 @@ function _parseAndAddDomain(store) {
store.dispatch(maybeAddNewKnownDomain(locationURL.host));
}
/**
* Updates the calendar entries in Redux when new list is received.
*
* @private
* @param {Object} event - An event returned from the native calendar.
* @param {Array<string>} knownDomains - The known domain list.
* @returns {CalendarEntry}
*/
function _parseCalendarEntry(event, knownDomains) {
if (event) {
const jitsiURL = _getURLFromEvent(event, knownDomains);
if (jitsiURL) {
const eventStartDate = Date.parse(event.startDate);
const eventEndDate = Date.parse(event.endDate);
if (isNaN(eventStartDate) || isNaN(eventEndDate)) {
logger.warn(
'Skipping invalid calendar event',
event.title,
event.startDate,
event.endDate
);
} else {
return {
endDate: eventEndDate,
id: event.id,
startDate: eventStartDate,
title: event.title,
url: jitsiURL
};
}
}
}
return null;
}
/**
* Updates the calendar entries in Redux when new list is received.
*
* @private
* @param {Array<CalendarEntry>} events - The new event list.
* @param {Array<string>} knownDomains - The known domain list.
* @param {Function} dispatch - The Redux dispatch function.
* @returns {void}
*/
function _updateCalendarEntries(events, knownDomains, dispatch) {
if (events && events.length) {
const eventList = [];
for (const event of events) {
const calendarEntry
= _parseCalendarEntry(event, knownDomains);
const now = Date.now();
if (calendarEntry && calendarEntry.endDate > now) {
eventList.push(calendarEntry);
}
}
dispatch(updateCalendarEntryList(eventList.sort((a, b) =>
a.startDate - b.startDate
).slice(0, MAX_LIST_LENGTH)));
}
}

View File

@@ -3,19 +3,13 @@
import { ReducerRegistry } from '../base/redux';
import { PersistenceRegistry } from '../base/storage';
import {
CALENDAR_ACCESS_REQUESTED,
NEW_CALENDAR_ENTRY_LIST,
NEW_KNOWN_DOMAIN
} from './actionTypes';
import { NEW_CALENDAR_ENTRY_LIST, NEW_KNOWN_DOMAIN } from './actionTypes';
/**
* ZB: this is an object, as further data is to come here, like:
* - known domain list
*/
const DEFAULT_STATE = {
/**
* Note: If features/calendar-sync ever gets persisted, do not persist the
* calendarAccessStatus value as it's needed to remain a runtime value to
* see if we need to re-request the calendar permission from the user.
*/
calendarAccessStatus: undefined,
events: [],
knownDomains: []
};
@@ -32,12 +26,6 @@ ReducerRegistry.register(
STORE_NAME,
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case CALENDAR_ACCESS_REQUESTED:
return {
...state,
calendarAccessStatus: action.status
};
case NEW_CALENDAR_ENTRY_LIST:
return {
...state,

View File

@@ -5,7 +5,8 @@ import {
UPDATE_DIAL_IN_NUMBERS_FAILED,
UPDATE_DIAL_IN_NUMBERS_SUCCESS
} from './actionTypes';
import { getDialInConferenceID, getDialInNumbers } from './functions';
declare var $: Function;
/**
* Opens the inline conference info dialog.
@@ -47,10 +48,12 @@ export function updateDialInNumbers() {
}
const { room } = state['features/base/conference'];
const conferenceIDURL
= `${dialInConfCodeUrl}?conference=${room}@${mucURL}`;
Promise.all([
getDialInNumbers(dialInNumbersUrl),
getDialInConferenceID(dialInConfCodeUrl, room, mucURL)
$.getJSON(dialInNumbersUrl),
$.getJSON(conferenceIDURL)
])
.then(([ dialInNumbers, { conference, id, message } ]) => {
if (!conference || !id) {

View File

@@ -2,11 +2,11 @@
import Avatar from '@atlaskit/avatar';
import InlineMessage from '@atlaskit/inline-message';
import { Immutable } from 'nuclear-js';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createInviteDialogEvent, sendAnalytics } from '../../analytics';
import { getInviteURL } from '../../base/connection';
import { Dialog, hideDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
@@ -114,10 +114,16 @@ class AddPeopleDialog extends Component<*, *> {
*/
addToCallInProgress: false,
// FIXME: Remove usage of Immutable. {@code MultiSelectAutocomplete}
// will default to having its internal implementation use a plain array
// if no {@link defaultValue} is passed in. As such is the case, this
// instance of Immutable.List gets overridden with an array on the first
// search.
/**
* The list of invite items.
*/
inviteItems: []
inviteItems: new Immutable.List()
};
/**
@@ -144,17 +150,6 @@ class AddPeopleDialog extends Component<*, *> {
};
}
/**
* Sends an analytics event to record the dialog has been shown.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
sendAnalytics(createInviteDialogEvent(
'invite.dialog.opened', 'dialog'));
}
/**
* React Component method that executes once component is updated.
*
@@ -174,17 +169,6 @@ class AddPeopleDialog extends Component<*, *> {
}
}
/**
* Sends an analytics event to record the dialog has been closed.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
sendAnalytics(createInviteDialogEvent(
'invite.dialog.closed', 'dialog'));
}
/**
* Renders the content of this component.
*
@@ -254,32 +238,6 @@ class AddPeopleDialog extends Component<*, *> {
return text.replace(/\D/g, '');
}
/**
* Helper for determining how many of each type of user is being invited.
* Used for logging and sending analytics related to invites.
*
* @param {Array} inviteItems - An array with the invite items, as created
* in {@link _parseQueryResults}.
* @private
* @returns {Object} An object with keys as user types and values as the
* number of invites for that type.
*/
_getInviteTypeCounts(inviteItems = []) {
const inviteTypeCounts = {};
inviteItems.forEach(i => {
const type = i.item.type;
if (!inviteTypeCounts[type]) {
inviteTypeCounts[type] = 0;
}
inviteTypeCounts[type]++;
});
return inviteTypeCounts;
}
_isAddDisabled: () => boolean;
/**
@@ -362,15 +320,6 @@ class AddPeopleDialog extends Component<*, *> {
* @returns {void}
*/
_onSubmit() {
const inviteTypeCounts
= this._getInviteTypeCounts(this.state.inviteItems);
sendAnalytics(createInviteDialogEvent(
'clicked', 'inviteButton', {
...inviteTypeCounts,
inviteAllowed: this._isAddDisabled()
}));
if (this._isAddDisabled()) {
return;
}
@@ -451,16 +400,7 @@ class AddPeopleDialog extends Component<*, *> {
// If any invites are left that means something failed to send
// so treat it as an error.
if (invitesLeftToSend.length) {
const erroredInviteTypeCounts
= this._getInviteTypeCounts(invitesLeftToSend);
logger.error(`${invitesLeftToSend.length} invites failed`,
erroredInviteTypeCounts);
sendAnalytics(createInviteDialogEvent(
'error', 'invite', {
...erroredInviteTypeCounts
}));
logger.error(`${invitesLeftToSend.length} invites failed`);
this.setState({
addToCallInProgress: false,
@@ -568,7 +508,7 @@ class AddPeopleDialog extends Component<*, *> {
let peopleSearchPromise;
if (this.props.enableAddPeople && text) {
if (this.props.enableAddPeople) {
peopleSearchPromise = searchDirectory(
_peopleSearchUrl,
_jwt,

View File

@@ -1,44 +1,8 @@
// @flow
import { doGetJSON } from '../base/util';
declare var $: Function;
declare var interfaceConfig: Object;
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Sends a GET request to obtain the conference ID necessary for identifying
* which conference to join after diaing the dial-in service.
*
* @param {string} baseUrl - The url for obtaining the conference ID (pin) for
* dialing into a conference.
* @param {string} roomName - The conference name to find the associated
* conference ID.
* @param {string} mucURL - In which MUC the conference exists.
* @returns {Promise} - The promise created by the request.
*/
export function getDialInConferenceID(
baseUrl: string,
roomName: string,
mucURL: string): Promise<Object> {
const conferenceIDURL = `${baseUrl}?conference=${roomName}@${mucURL}`;
return doGetJSON(conferenceIDURL);
}
/**
* Sends a GET request for phone numbers used to dial into a conference.
*
* @param {string} url - The service that returns confernce dial-in numbers.
* @returns {Promise} - The promise created by the request. The returned numbers
* may be an array of numbers or an object with countries as keys and arrays of
* phone number strings.
*/
export function getDialInNumbers(url: string): Promise<*> {
return doGetJSON(url);
}
/**
* Get the position of the invite option in the interfaceConfig.INVITE_OPTIONS
* list.
@@ -66,7 +30,7 @@ export function invitePeopleAndChatRooms( // eslint-disable-line max-params
inviteServiceUrl: string,
inviteUrl: string,
jwt: string,
inviteItems: Array<Object>): Promise<void> {
inviteItems: Object): Promise<void> {
if (!inviteItems || inviteItems.length === 0) {
return Promise.resolve();
}
@@ -114,24 +78,13 @@ export function searchDirectory( // eslint-disable-line max-params
): Promise<Array<Object>> {
const queryTypesString = JSON.stringify(queryTypes);
return fetch(`${serviceUrl}?query=${encodeURIComponent(text)}&queryTypes=${
queryTypesString}&jwt=${jwt}`)
.then(response => {
const jsonify = response.json();
if (response.ok) {
return jsonify;
}
return jsonify
.then(result => Promise.reject(result));
})
.catch(error => {
logger.error(
'Error searching directory:', error);
return Promise.reject(error);
});
return new Promise((resolve, reject) => {
$.getJSON(
`${serviceUrl}?query=${encodeURIComponent(text)}&queryTypes=${
queryTypesString}&jwt=${jwt}`,
resolve)
.catch((jqxhr, textStatus, error) => reject(error));
});
}
/**

View File

@@ -1,31 +0,0 @@
// @flow
import { Alert, Linking, NativeModules } from 'react-native';
import { Platform } from '../../base/react';
/**
* Opens the settings panel for the current platform.
*
* @private
* @returns {void}
*/
export function openSettings() {
switch (Platform.OS) {
case 'android':
NativeModules.AndroidSettings.open().catch(() => {
Alert.alert(
'Error opening settings',
'Please open settings and grant the required permissions',
[
{ text: 'OK' }
]
);
});
break;
case 'ios':
Linking.openURL('app-settings:');
break;
}
}

View File

@@ -1,3 +1 @@
export * from './functions';
import './middleware';

View File

@@ -1,10 +1,9 @@
/* @flow */
import { Alert } from 'react-native';
import { openSettings } from './functions';
import { Alert, Linking, NativeModules } from 'react-native';
import { isRoomValid } from '../../base/conference';
import { Platform } from '../../base/react';
import { MiddlewareRegistry } from '../../base/redux';
import { TRACK_CREATE_ERROR } from '../../base/tracks';
@@ -65,9 +64,35 @@ function _alertPermissionErrorWithSettings(trackType) {
[
{ text: 'Cancel' },
{
onPress: openSettings,
onPress: _openSettings,
text: 'Settings'
}
],
{ cancelable: false });
}
/**
* Opens the settings panel for the current platform.
*
* @private
* @returns {void}
*/
function _openSettings() {
switch (Platform.OS) {
case 'android':
NativeModules.AndroidSettings.open().catch(() => {
Alert.alert(
'Error opening settings',
'Please open settings and grant the required permissions',
[
{ text: 'OK' }
]
);
});
break;
case 'ios':
Linking.openURL('app-settings:');
break;
}
}

View File

@@ -14,11 +14,6 @@ type Props = {
*/
disabled: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The i18n translate function
*/

View File

@@ -2,10 +2,9 @@
import React from 'react';
import { View, TabBarIOS } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { MeetingList, refreshCalendarEntryList } from '../../calendar-sync';
import { MeetingList } from '../../calendar-sync';
import { RecentList } from '../../recent-list';
import AbstractPagedList from './AbstractPagedList';
@@ -60,7 +59,8 @@ class PagedList extends AbstractPagedList {
selected = { pageIndex === 1 }
title = { t('welcomepage.calendar') } >
<MeetingList
disabled = { disabled } />
disabled = { disabled }
displayed = { pageIndex === 1 } />
</TabBarIOS.Item>
</TabBarIOS>
</View>
@@ -81,17 +81,8 @@ class PagedList extends AbstractPagedList {
this.setState({
pageIndex: tabIndex
});
if (tabIndex === 1) {
/**
* This is a workaround as TabBarIOS doesn't invoke
* componentWillReciveProps on prop change of the
* MeetingList component.
*/
this.props.dispatch(refreshCalendarEntryList());
}
};
}
}
export default translate(connect()(PagedList));
export default translate(PagedList);

View File

@@ -6,7 +6,6 @@ local parse = neturl.parseQuery;
local st = require "util.stanza";
local get_room_from_jid = module:require "util".get_room_from_jid;
local wrap_async_run = module:require "util".wrap_async_run;
local update_presence_identity = module:require "util".update_presence_identity;
local timer = require "util.timer";
local MUC_NS = "http://jabber.org/protocol/muc";
@@ -115,9 +114,8 @@ end
-- @param token the token we received
-- @param room_name the room name
-- @param group name of the group (optional)
-- @param session the session to use for storing token specific fields
-- @return true if values are ok or false otherwise
function verify_token(token, room_name, group, session)
function verify_token(token, room_name, group)
if disableTokenVerification then
return true;
end
@@ -131,6 +129,7 @@ function verify_token(token, room_name, group, session)
return false;
end
local session = {};
session.auth_token = token;
local verified, reason = token_util:process_and_verify_token(session);
if not verified then
@@ -201,8 +200,7 @@ end);
-- @param name the display name fot the occupant (optional)
-- @param avatar the avatar to use for the new occupant (optional)
-- @param status the initial status to use for the new occupant (optional)
-- @param context the information that we will store for this poltergeist
function create_poltergeist_occupant(room, nick, name, avatar, status, context)
function create_poltergeist_occupant(room, nick, name, avatar, status)
log("debug", "create_poltergeist_occupant %s", nick);
-- Join poltergeist occupant to room, with the invited JID as their nick
local join_presence = st.presence({
@@ -229,14 +227,6 @@ function create_poltergeist_occupant(room, nick, name, avatar, status, context)
join:tag("password", { xmlns = MUC_NS }):text(room_password);
end
update_presence_identity(
join_presence,
context.user,
context.group,
context.creator_user,
context.creator_group
);
room:handle_first_presence(
prosody.hosts[poltergeist_component], join_presence);
@@ -400,9 +390,8 @@ function handle_create_poltergeist (event)
local name = params["name"];
local avatar = params["avatar"];
local status = params["status"];
local session = {};
if not verify_token(params["token"], room_name, group, session) then
if not verify_token(params["token"], room_name, group) then
return 403;
end
@@ -421,16 +410,8 @@ function handle_create_poltergeist (event)
else
username = generate_uuid();
store_username(room, user_id, username);
local context = {
user = {
id = user_id;
};
group = group;
creator_user = session.jitsi_meet_context_user;
creator_group = session.jitsi_meet_context_group;
};
create_poltergeist_occupant(
room, string.sub(username, 0, 8), name, avatar, status, context);
room, string.sub(username, 0, 8), name, avatar, status);
return 200;
end
end
@@ -449,7 +430,7 @@ function handle_update_poltergeist (event)
local group = params["group"];
local status = params["status"];
if not verify_token(params["token"], room_name, group, {}) then
if not verify_token(params["token"], room_name, group) then
return 403;
end
@@ -486,7 +467,7 @@ function handle_remove_poltergeist (event)
local room_name = params["room"];
local group = params["group"];
if not verify_token(params["token"], room_name, group, {}) then
if not verify_token(params["token"], room_name, group) then
return 403;
end

View File

@@ -1,5 +1,4 @@
local stanza = require "util.stanza";
local update_presence_identity = module:require "util".update_presence_identity;
-- For all received presence messages, if the jitsi_meet_context_(user|group)
-- values are set in the session, then insert them into the presence messages
@@ -7,13 +6,32 @@ local update_presence_identity = module:require "util".update_presence_identity;
function on_message(event)
if event and event["stanza"] then
if event.origin and event.origin.jitsi_meet_context_user then
-- First remove any 'identity' element if it already
-- exists
event.stanza:maptags(
function(tag)
for k, v in pairs(tag) do
if k == "name" and v == "identity" then
return nil
end
end
return tag
end
)
module:log("debug", "Presence after previous identity stripped: %s", tostring(event.stanza))
update_presence_identity(
event.stanza,
event.origin.jitsi_meet_context_user,
event.origin.jitsi_meet_context_group
);
event.stanza:tag("identity"):tag("user")
for k, v in pairs(event.origin.jitsi_meet_context_user) do
event.stanza:tag(k):text(v):up()
end
event.stanza:up()
-- Add the group information if it is present
if event.origin.jitsi_meet_context_group then
event.stanza:tag("group"):text(event.origin.jitsi_meet_context_group)
end
module:log("debug", "Sending presence with identity inserted %s", tostring(event.stanza))
end
end
end

View File

@@ -73,67 +73,8 @@ function wrap_async_run(event,handler)
return result;
end
--- Updates presence stanza, by adding identity node
-- @param stanza the presence stanza
-- @param user the user to which presence we are updating identity
-- @param group the group of the user to which presence we are updating identity
-- @param creator_user the user who created the user which presence we
-- are updating (this is the poltergeist case, where a user creates
-- a poltergeist), optional.
-- @param creator_group the group of the user who created the user which
-- presence we are updating (this is the poltergeist case, where a user creates
-- a poltergeist), optional.
function update_presence_identity(
stanza, user, group, creator_user, creator_group)
-- First remove any 'identity' element if it already
-- exists, so it cannot be spoofed by a client
stanza:maptags(
function(tag)
for k, v in pairs(tag) do
if k == "name" and v == "identity" then
return nil
end
end
return tag
end
)
module:log("debug",
"Presence after previous identity stripped: %s", tostring(stanza));
stanza:tag("identity"):tag("user");
for k, v in pairs(user) do
stanza:tag(k):text(v):up();
end
stanza:up();
-- Add the group information if it is present
if group then
stanza:tag("group"):text(group):up();
end
-- Add the creator user information if it is present
if creator_user then
stanza:tag("creator_user");
for k, v in pairs(creator_user) do
stanza:tag(k):text(v):up();
end
stanza:up();
-- Add the creator group information if it is present
if creator_group then
stanza:tag("creator_group"):text(creator_group):up();
end
stanza:up();
end
module:log("debug",
"Presence with identity inserted %s", tostring(stanza))
end
return {
get_room_from_jid = get_room_from_jid;
wrap_async_run = wrap_async_run;
room_jid_match_rewrite = room_jid_match_rewrite;
update_presence_identity = update_presence_identity;
room_jid_match_rewrite= room_jid_match_rewrite;
};

View File

@@ -139,9 +139,8 @@ module.exports = [
entry: {
'app.bundle': [
// babel-polyfill and fetch polyfill are required for IE11.
// XXX Required by at least IE11 at the time of this writing.
'babel-polyfill',
'whatwg-fetch',
'./app.js'
],