Grow features/settings from features/app-settings and features/settings-menu

This commit is contained in:
Lyubo Marinov 2018-02-26 10:14:46 -06:00
parent e23d4317eb
commit 9f69c4d730
55 changed files with 648 additions and 674 deletions

View File

@ -33,14 +33,14 @@ import java.net.URL;
/** /**
* Base Activity for applications integrating Jitsi Meet at a higher level. It * Base Activity for applications integrating Jitsi Meet at a higher level. It
* contains all the required wiring between the {@code JKConferenceView} and * contains all the required wiring between the {@code JitsiMeetView} and
* the Activity lifecycle methods already implemented. * the Activity lifecycle methods already implemented.
* *
* In this activity we use a single {@code JKConferenceView} instance. This * In this activity we use a single {@code JitsiMeetView} instance. This
* instance gives us access to a view which displays the welcome page and the * instance gives us access to a view which displays the welcome page and the
* conference itself. All lifetime methods associated with this Activity are * conference itself. All lifetime methods associated with this Activity are
* hooked to the React Native subsystem via proxy calls through the * hooked to the React Native subsystem via proxy calls through the
* {@code JKConferenceView} static methods. * {@code JitsiMeetView} static methods.
*/ */
public class JitsiMeetActivity extends AppCompatActivity { public class JitsiMeetActivity extends AppCompatActivity {
/** /**

View File

@ -47,16 +47,18 @@
}, },
"welcomepage":{ "welcomepage":{
"appDescription": "Go ahead, video chat with the whole team. In fact, invite everyone you know. __app__ is a fully encrypted, 100% open source video conferencing solution that you can use all day, every day, for free — with no account needed.", "appDescription": "Go ahead, video chat with the whole team. In fact, invite everyone you know. __app__ is a fully encrypted, 100% open source video conferencing solution that you can use all day, every day, for free — with no account needed.",
"audioOnlyLabel": "Voice", "audioVideoSwitch": {
"audio": "Voice",
"video": "Video"
},
"go": "GO", "go": "GO",
"hintText": "Enter a room name you want to join to, or simply create a new room name, eg. MeetingWithJohn",
"join": "JOIN", "join": "JOIN",
"privacy": "Privacy", "privacy": "Privacy",
"roomname": "Enter room name", "roomname": "Enter room name",
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback", "sendFeedback": "Send feedback",
"terms": "Terms", "terms": "Terms",
"title": "More secure, more flexible, and completely free video conferencing", "title": "More secure, more flexible, and completely free video conferencing"
"videoEnabledLabel": "Video"
}, },
"startupoverlay": { "startupoverlay": {
"policyText": " ", "policyText": " ",
@ -503,7 +505,7 @@
"title": "Call info", "title": "Call info",
"tooltip": "Get access info about the meeting" "tooltip": "Get access info about the meeting"
}, },
"settingsScreen": { "settingsView": {
"alertOk": "OK", "alertOk": "OK",
"alertTitle": "Warning", "alertTitle": "Warning",
"alertURLText": "The entered server URL is invalid", "alertURLText": "The entered server URL is invalid",
@ -515,8 +517,5 @@
"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"
},
"sideBar": {
"settings": "Settings"
} }
} }

View File

@ -1,17 +1,18 @@
/* global $, APP, interfaceConfig */ /* global $, APP, interfaceConfig */
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { i18next } from '../../../../react/features/base/i18n'; import { i18next } from '../../../../react/features/base/i18n';
import { SettingsMenu } from '../../../../react/features/settings-menu'; import { SettingsMenu } from '../../../../react/features/settings';
/* eslint-enable no-unused-vars */
import UIUtil from '../../util/UIUtil'; import UIUtil from '../../util/UIUtil';
/* eslint-enable no-unused-vars */
export default { export default {
init() { init() {
const settingsMenuContainer = document.createElement('div'); const settingsMenuContainer = document.createElement('div');
@ -31,8 +32,7 @@ export default {
ReactDOM.render( ReactDOM.render(
<Provider store = { APP.store }> <Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }> <I18nextProvider i18n = { i18next }>
<SettingsMenu <SettingsMenu { ...props } />
{ ...props } />
</I18nextProvider> </I18nextProvider>
</Provider>, </Provider>,
settingsMenuContainer settingsMenuContainer

View File

@ -76,13 +76,13 @@ export const VIDEO_MUTE = 'video.mute';
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createApiEvent = function(action, attributes = {}) { export function createApiEvent(action, attributes = {}) {
return { return {
action, action,
attributes, attributes,
source: 'jitsi-meet-api' source: 'jitsi-meet-api'
}; };
}; }
/** /**
* Creates an event which indicates that the audio-only mode has been changed. * Creates an event which indicates that the audio-only mode has been changed.
@ -91,11 +91,11 @@ export const createApiEvent = function(action, attributes = {}) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createAudioOnlyChangedEvent = function(enabled) { export function createAudioOnlyChangedEvent(enabled) {
return { return {
action: `audio.only.${enabled ? 'enabled' : 'disabled'}` action: `audio.only.${enabled ? 'enabled' : 'disabled'}`
}; };
}; }
/** /**
* Creates an event which indicates that a device was changed. * Creates an event which indicates that a device was changed.
@ -106,7 +106,7 @@ export const createAudioOnlyChangedEvent = function(enabled) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createDeviceChangedEvent = function(mediaType, deviceType) { export function createDeviceChangedEvent(mediaType, deviceType) {
return { return {
action: 'device.changed', action: 'device.changed',
attributes: { attributes: {
@ -114,7 +114,7 @@ export const createDeviceChangedEvent = function(mediaType, deviceType) {
'media_type': mediaType 'media_type': mediaType
} }
}; };
}; }
/** /**
* Creates an event which specifies that the feedback dialog has been opened. * Creates an event which specifies that the feedback dialog has been opened.
@ -122,11 +122,11 @@ export const createDeviceChangedEvent = function(mediaType, deviceType) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createFeedbackOpenEvent = function() { export function createFeedbackOpenEvent() {
return { return {
action: 'feedback.opened' action: 'feedback.opened'
}; };
}; }
/** /**
* Creates an event which indicates that the invite dialog was closed. This is * Creates an event which indicates that the invite dialog was closed. This is
@ -136,11 +136,11 @@ export const createFeedbackOpenEvent = function() {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createInviteDialogClosedEvent = function() { export function createInviteDialogClosedEvent() {
return { return {
action: 'invite.dialog.closed' action: 'invite.dialog.closed'
}; };
}; }
/** /**
* Creates a "page reload" event. * Creates a "page reload" event.
@ -152,17 +152,16 @@ export const createInviteDialogClosedEvent = function() {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createPageReloadScheduledEvent export function createPageReloadScheduledEvent(reason, timeout, details) {
= function(reason, timeout, details) { return {
return { action: 'page.reload.scheduled',
action: 'page.reload.scheduled', attributes: {
attributes: { reason,
reason, timeout,
timeout, ...details
...details }
}
};
}; };
}
/** /**
* Creates a "pinned" or "unpinned" event. * Creates a "pinned" or "unpinned" event.
@ -173,17 +172,16 @@ export const createPageReloadScheduledEvent
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createPinnedEvent export function createPinnedEvent(action, participantId, attributes) {
= function(action, participantId, attributes) { return {
return { type: TYPE_TRACK,
type: TYPE_TRACK, action,
action, actionSubject: 'participant',
actionSubject: 'participant', objectType: 'participant',
objectType: 'participant', objectId: participantId,
objectId: participantId, attributes
attributes };
}; }
};
/** /**
* Creates an event which indicates that a button in the profile panel was * Creates an event which indicates that a button in the profile panel was
@ -194,16 +192,15 @@ export const createPinnedEvent
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createProfilePanelButtonEvent export function createProfilePanelButtonEvent(buttonName, attributes = {}) {
= function(buttonName, attributes = {}) { return {
return { action: 'clicked',
action: 'clicked', actionSubject: buttonName,
actionSubject: buttonName, attributes,
attributes, source: 'profile.panel',
source: 'profile.panel', type: TYPE_UI
type: TYPE_UI
};
}; };
}
/** /**
* Creates an event which indicates that a specific button on one of the * Creates an event which indicates that a specific button on one of the
@ -215,14 +212,14 @@ export const createProfilePanelButtonEvent
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createRecordingDialogEvent = function(dialogName, buttonName) { export function createRecordingDialogEvent(dialogName, buttonName) {
return { return {
action: 'clicked', action: 'clicked',
actionSubject: buttonName, actionSubject: buttonName,
source: `${dialogName}.recording.dialog`, source: `${dialogName}.recording.dialog`,
type: TYPE_UI type: TYPE_UI
}; };
}; }
/** /**
* Creates an event which specifies that the "confirm" button on the remote * Creates an event which specifies that the "confirm" button on the remote
@ -233,7 +230,7 @@ export const createRecordingDialogEvent = function(dialogName, buttonName) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createRemoteMuteConfirmedEvent = function(participantId) { export function createRemoteMuteConfirmedEvent(participantId) {
return { return {
action: 'clicked', action: 'clicked',
actionSubject: 'remote.mute.dialog.confirm.button', actionSubject: 'remote.mute.dialog.confirm.button',
@ -243,7 +240,7 @@ export const createRemoteMuteConfirmedEvent = function(participantId) {
source: 'remote.mute.dialog', source: 'remote.mute.dialog',
type: TYPE_UI type: TYPE_UI
}; };
}; }
/** /**
* Creates an event which indicates that one of the buttons in the "remote * Creates an event which indicates that one of the buttons in the "remote
@ -254,16 +251,15 @@ export const createRemoteMuteConfirmedEvent = function(participantId) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createRemoteVideoMenuButtonEvent export function createRemoteVideoMenuButtonEvent(buttonName, attributes) {
= function(buttonName, attributes) { return {
return { action: 'clicked',
action: 'clicked', actionSubject: buttonName,
actionSubject: buttonName, attributes,
attributes, source: 'remote.video.menu',
source: 'remote.video.menu', type: TYPE_UI
type: TYPE_UI
};
}; };
}
/** /**
* Creates an event indicating that an action related to screen sharing * Creates an event indicating that an action related to screen sharing
@ -273,12 +269,12 @@ export const createRemoteVideoMenuButtonEvent
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createScreenSharingEvent = function(action) { export function createScreenSharingEvent(action) {
return { return {
action, action,
actionSubject: 'screen.sharing' actionSubject: 'screen.sharing'
}; };
}; }
/** /**
* The local participant failed to send a "selected endpoint" message to the * The local participant failed to send a "selected endpoint" message to the
@ -288,7 +284,7 @@ export const createScreenSharingEvent = function(action) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createSelectParticipantFailedEvent = function(error) { export function createSelectParticipantFailedEvent(error) {
const event = { const event = {
action: 'select.participant.failed' action: 'select.participant.failed'
}; };
@ -298,7 +294,7 @@ export const createSelectParticipantFailedEvent = function(error) {
} }
return event; return event;
}; }
/** /**
* Creates an event associated with the "shared video" feature. * Creates an event associated with the "shared video" feature.
@ -308,13 +304,13 @@ export const createSelectParticipantFailedEvent = function(error) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createSharedVideoEvent = function(action, attributes = {}) { export function createSharedVideoEvent(action, attributes = {}) {
return { return {
action, action,
attributes, attributes,
actionSubject: 'shared.video' actionSubject: 'shared.video'
}; };
}; }
/** /**
* Creates an event associated with a shortcut being pressed, released or * Creates an event associated with a shortcut being pressed, released or
@ -331,17 +327,19 @@ export const createSharedVideoEvent = function(action, attributes = {}) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createShortcutEvent export function createShortcutEvent(
= function(shortcut, action = ACTION_SHORTCUT_TRIGGERED, attributes = {}) { shortcut,
return { action = ACTION_SHORTCUT_TRIGGERED,
action, attributes = {}) {
actionSubject: 'keyboard.shortcut', return {
actionSubjectId: shortcut, action,
attributes, actionSubject: 'keyboard.shortcut',
source: 'keyboard.shortcut', actionSubjectId: shortcut,
type: TYPE_UI attributes,
}; source: 'keyboard.shortcut',
type: TYPE_UI
}; };
}
/** /**
* Creates an event which indicates the "start audio only" configuration. * Creates an event which indicates the "start audio only" configuration.
@ -350,14 +348,14 @@ export const createShortcutEvent
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createStartAudioOnlyEvent = function(audioOnly) { export function createStartAudioOnlyEvent(audioOnly) {
return { return {
action: 'start.audio.only', action: 'start.audio.only',
attributes: { attributes: {
enabled: audioOnly enabled: audioOnly
} }
}; };
}; }
/** /**
* Creates an event which indicates the "start muted" configuration. * Creates an event which indicates the "start muted" configuration.
@ -372,17 +370,19 @@ export const createStartAudioOnlyEvent = function(audioOnly) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createStartMutedConfigurationEvent export function createStartMutedConfigurationEvent(
= function(source, audioMute, videoMute) { source,
return { audioMute,
action: 'start.muted.configuration', videoMute) {
attributes: { return {
source, action: 'start.muted.configuration',
'audio_mute': audioMute, attributes: {
'video_mute': videoMute source,
} 'audio_mute': audioMute,
}; 'video_mute': videoMute
}
}; };
}
/** /**
* Creates an event which indicates the delay for switching between simulcast * Creates an event which indicates the delay for switching between simulcast
@ -392,12 +392,12 @@ export const createStartMutedConfigurationEvent
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createStreamSwitchDelayEvent = function(attributes) { export function createStreamSwitchDelayEvent(attributes) {
return { return {
action: 'stream.switch.delay', action: 'stream.switch.delay',
attributes attributes
}; };
}; }
/** /**
* Automatically changing the mute state of a media track in order to match * Automatically changing the mute state of a media track in order to match
@ -409,7 +409,7 @@ export const createStreamSwitchDelayEvent = function(attributes) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createSyncTrackStateEvent = function(mediaType, muted) { export function createSyncTrackStateEvent(mediaType, muted) {
return { return {
action: 'sync.track.state', action: 'sync.track.state',
attributes: { attributes: {
@ -417,7 +417,7 @@ export const createSyncTrackStateEvent = function(mediaType, muted) {
muted muted
} }
}; };
}; }
/** /**
* Creates an event associated with a toolbar button being clicked/pressed. By * Creates an event associated with a toolbar button being clicked/pressed. By
@ -431,7 +431,7 @@ export const createSyncTrackStateEvent = function(mediaType, muted) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createToolbarEvent = function(buttonName, attributes = {}) { export function createToolbarEvent(buttonName, attributes = {}) {
return { return {
action: 'clicked', action: 'clicked',
actionSubject: buttonName, actionSubject: buttonName,
@ -439,7 +439,7 @@ export const createToolbarEvent = function(buttonName, attributes = {}) {
source: 'toolbar.button', source: 'toolbar.button',
type: TYPE_UI type: TYPE_UI
}; };
}; }
/** /**
* Creates an event which indicates that a local track was muted. * Creates an event which indicates that a local track was muted.
@ -452,7 +452,7 @@ export const createToolbarEvent = function(buttonName, attributes = {}) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createTrackMutedEvent = function(mediaType, reason, muted = true) { export function createTrackMutedEvent(mediaType, reason, muted = true) {
return { return {
action: 'track.muted', action: 'track.muted',
attributes: { attributes: {
@ -461,7 +461,7 @@ export const createTrackMutedEvent = function(mediaType, reason, muted = true) {
reason reason
} }
}; };
}; }
/** /**
* Creates an event for an action on the welcome page. * Creates an event for an action on the welcome page.
@ -472,12 +472,11 @@ export const createTrackMutedEvent = function(mediaType, reason, muted = true) {
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export const createWelcomePageEvent export function createWelcomePageEvent(action, actionSubject, attributes = {}) {
= function(action, actionSubject, attributes = {}) { return {
return { action,
action, actionSubject,
actionSubject, attributes,
attributes, source: 'welcomePage'
source: 'welcomePage'
};
}; };
}

View File

@ -1,19 +0,0 @@
/**
* The type of (redux) action which signals the request
* to hide the app settings screen.
*
* {
* type: HIDE_APP_SETTINGS
* }
*/
export const HIDE_APP_SETTINGS = Symbol('HIDE_APP_SETTINGS');
/**
* The type of (redux) action which signals the request
* to show the app settings screen where available.
*
* {
* type: SHOW_APP_SETTINGS
* }
*/
export const SHOW_APP_SETTINGS = Symbol('SHOW_APP_SETTINGS');

View File

@ -1,29 +0,0 @@
// @flow
import { HIDE_APP_SETTINGS, SHOW_APP_SETTINGS } from './actionTypes';
/**
* Redux-signals the request to hide the app settings screen.
*
* @returns {{
* type: HIDE_APP_SETTINGS
* }}
*/
export function hideAppSettings() {
return {
type: HIDE_APP_SETTINGS
};
}
/**
* Redux-signals the request to open the app settings screen.
*
* @returns {{
* type: SHOW_APP_SETTINGS
* }}
*/
export function showAppSettings() {
return {
type: SHOW_APP_SETTINGS
};
}

View File

@ -1,238 +0,0 @@
// @flow
import React from 'react';
import {
Alert,
Modal,
SafeAreaView,
ScrollView,
Switch,
Text,
TextInput,
View
} from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { Header } from '../../base/react';
import { PlatformElements } from '../../base/styles';
import { hideAppSettings } from '../actions';
import { normalizeUserInputURL } from '../functions';
import { BackButton, FormRow, FormSectionHeader } from './_';
import { _mapStateToProps, AbstractAppSettings } from './AbstractAppSettings';
import styles from './styles';
/**
* The native container rendering the app settings page.
*
* @extends AbstractAppSettings
*/
class AppSettings extends AbstractAppSettings {
_urlField: Object;
/**
* Instantiates a new {@code AppSettings} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
this._onBlurServerURL = this._onBlurServerURL.bind(this);
this._onRequestClose = this._onRequestClose.bind(this);
this._setURLFieldReference = this._setURLFieldReference.bind(this);
this._showURLAlert = this._showURLAlert.bind(this);
}
/**
* Implements React's {@link Component#render()}, renders the settings page.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _profile, t } = this.props;
return (
<Modal
animationType = 'slide'
onRequestClose = { this._onRequestClose }
presentationStyle = 'fullScreen'
supportedOrientations = { [
'landscape',
'portrait'
] }
visible = { this.props._visible }>
<View style = { PlatformElements.page }>
<Header>
<BackButton
onPress = { this._onRequestClose } />
<Text
style = { [
styles.text,
PlatformElements.headerText
] } >
{ t('settingsScreen.header') }
</Text>
</Header>
<SafeAreaView style = { styles.settingsForm }>
<ScrollView>
<FormSectionHeader
i18nLabel = 'settingsScreen.profileSection' />
<FormRow
fieldSeparator = { true }
i18nLabel = 'settingsScreen.displayName' >
<TextInput
onChangeText = { this._onChangeDisplayName }
placeholder = 'John Doe'
value = { _profile.displayName } />
</FormRow>
<FormRow
i18nLabel = 'settingsScreen.email' >
<TextInput
keyboardType = { 'email-address' }
onChangeText = { this._onChangeEmail }
placeholder = 'email@example.com'
value = { _profile.email } />
</FormRow>
<FormSectionHeader
i18nLabel
= 'settingsScreen.conferenceSection' />
<FormRow
fieldSeparator = { true }
i18nLabel = 'settingsScreen.serverURL' >
<TextInput
autoCapitalize = 'none'
onBlur = { this._onBlurServerURL }
onChangeText = { this._onChangeServerURL }
placeholder = { this.props._serverURL }
value = { _profile.serverURL } />
</FormRow>
<FormRow
fieldSeparator = { true }
i18nLabel
= 'settingsScreen.startWithAudioMuted' >
<Switch
onValueChange = {
this._onStartAudioMutedChange
}
value = {
_profile.startWithAudioMuted
} />
</FormRow>
<FormRow
i18nLabel
= 'settingsScreen.startWithVideoMuted' >
<Switch
onValueChange = {
this._onStartVideoMutedChange
}
value = {
_profile.startWithVideoMuted
} />
</FormRow>
</ScrollView>
</SafeAreaView>
</View>
</Modal>
);
}
_onBlurServerURL: () => void;
/**
* Handler the server URL lose focus event. Here we validate the server URL
* and update it to the normalized version, or show an error if incorrect.
*
* @private
* @returns {void}
*/
_onBlurServerURL() {
this._processServerURL(false /* hideOnSuccess */);
}
_onChangeDisplayName: (string) => void;
_onChangeEmail: (string) => void;
_onChangeServerURL: (string) => void;
_onStartAudioMutedChange: (boolean) => void;
_onStartVideoMutedChange: (boolean) => void;
/**
* Processes the server URL. It normalizes it and an error alert is
* displayed in case it's incorrect.
*
* @param {boolean} hideOnSuccess - True if the dialog should be hidden if
* normalization / validation succeeds, false otherwise.
* @private
* @returns {void}
*/
_processServerURL(hideOnSuccess: boolean) {
const { serverURL } = this.props._profile;
const normalizedURL = normalizeUserInputURL(serverURL);
if (normalizedURL === null) {
this._showURLAlert();
} else {
this._onChangeServerURL(normalizedURL);
if (hideOnSuccess) {
this.props.dispatch(hideAppSettings());
}
}
}
_onRequestClose: () => void;
/**
* Handles the back button.
* Also invokes normalizeUserInputURL to validate the URL entered
* by the user.
*
* @returns {void}
*/
_onRequestClose() {
this._processServerURL(true /* hideOnSuccess */);
}
_setURLFieldReference: (React$ElementRef<*> | null) => void;
/**
* Stores a reference to the URL field for later use.
*
* @protected
* @param {Object} component - The field component.
* @returns {void}
*/
_setURLFieldReference(component) {
this._urlField = component;
}
_showURLAlert: () => void;
/**
* Shows an alert telling the user that the URL he/she entered was invalid.
*
* @returns {void}
*/
_showURLAlert() {
const { t } = this.props;
Alert.alert(
t('settingsScreen.alertTitle'),
t('settingsScreen.alertURLText'),
[
{
onPress: () => this._urlField.focus(),
text: t('settingsScreen.alertOk')
}
]
);
}
}
export default translate(connect(_mapStateToProps)(AppSettings));

View File

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

View File

@ -1,3 +0,0 @@
export { default as BackButton } from './BackButton';
export { default as FormRow } from './FormRow';
export { default as FormSectionHeader } from './FormSectionHeader';

View File

@ -1,33 +0,0 @@
import {
BoxModel,
ColorPalette,
createStyleSheet
} from '../../base/styles';
/**
* The styles of the React {@code Components} of the feature
* {@code app-settings}.
*/
export default createStyleSheet({
/**
* Style of the ScrollView to be able to scroll the content.
*/
scrollView: {
flex: 1
},
/**
* Style of the settings screen content (form).
*/
settingsForm: {
flex: 1,
margin: BoxModel.margin
},
/**
* Global {@code Text} color for the page.
*/
text: {
color: ColorPalette.black
}
});

View File

@ -1,31 +0,0 @@
// @flow
import {
HIDE_APP_SETTINGS,
SHOW_APP_SETTINGS
} from './actionTypes';
import { ReducerRegistry } from '../base/redux';
const DEFAULT_STATE = {
visible: false
};
ReducerRegistry.register(
'features/app-settings', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case HIDE_APP_SETTINGS:
return {
...state,
visible: false
};
case SHOW_APP_SETTINGS:
return {
...state,
visible: true
};
}
return state;
});

View File

@ -125,17 +125,20 @@ function _connectionEstablished({ dispatch }, next, action) {
*/ */
function _conferenceFailedOrLeft({ dispatch, getState }, next, action) { function _conferenceFailedOrLeft({ dispatch, getState }, next, action) {
const result = next(action); const result = next(action);
const state = getState(); const state = getState();
const { audioOnly } = state['features/base/conference']; const { audioOnly } = state['features/base/conference'];
const { startAudioOnly } = state['features/base/profile'].profile; const { startAudioOnly } = state['features/base/profile'].profile;
// FIXME: Consider implementing a standalone audio-only feature // FIXME: Consider implementing a standalone audio-only feature that handles
// that handles all these state changes. // all these state changes.
if (audioOnly && !startAudioOnly) { if (audioOnly) {
sendAnalytics(createAudioOnlyChangedEvent(false)); if (!startAudioOnly) {
logger.log('Audio only disabled'); sendAnalytics(createAudioOnlyChangedEvent(false));
dispatch(setAudioOnly(false)); logger.log('Audio only disabled');
} else if (!audioOnly && startAudioOnly) { dispatch(setAudioOnly(false));
}
} else if (startAudioOnly) {
sendAnalytics(createAudioOnlyChangedEvent(true)); sendAnalytics(createAudioOnlyChangedEvent(true));
logger.log('Audio only enabled'); logger.log('Audio only enabled');
dispatch(setAudioOnly(true)); dispatch(setAudioOnly(true));

View File

@ -32,7 +32,36 @@ type Props = {
export default class Header extends Component<Props> { export default class Header extends Component<Props> {
/** /**
* Constructor of the Header component. * The style of button-like React {@code Component}s rendered in
* {@code Header}.
*
* @returns {Object}
*/
static get buttonStyle() {
return styles.headerButton;
}
/**
* The style of a React {@code Component} rendering a {@code Header} as its
* child.
*
* @returns {Object}
*/
static get pageStyle() {
return styles.page;
}
/**
* The style of text rendered in {@code Header}.
*
* @returns {Object}
*/
static get textStyle() {
return styles.headerText;
}
/**
* Initializes a new {@code Header} instance.
* *
* @inheritdoc * @inheritdoc
*/ */
@ -77,8 +106,8 @@ export default class Header extends Component<Props> {
_getIOS10CompatiblePadding: () => Object; _getIOS10CompatiblePadding: () => Object;
/** /**
* Adds a padding for iOS 10 (and older) devices to avoid clipping * Adds a padding for iOS 10 (and older) devices to avoid clipping with the
* with the status bar. * status bar.
* Note: This is a workaround for iOS 10 (and older) devices only to fix * Note: This is a workaround for iOS 10 (and older) devices only to fix
* usability, but it doesn't take orientation into account, so unnecessary * usability, but it doesn't take orientation into account, so unnecessary
* padding is rendered in some cases. * padding is rendered in some cases.
@ -99,5 +128,4 @@ export default class Header extends Component<Props> {
return null; return null;
} }
} }

View File

@ -1,4 +1,4 @@
/* @flow */ // @flow
import React, { Component } from 'react'; import React, { Component } from 'react';
import { import {
@ -10,9 +10,8 @@ import {
import styles, { SIDEBAR_WIDTH } from './styles'; import styles, { SIDEBAR_WIDTH } from './styles';
/** /**
* The type of the React {@code Component} props of {@link SideBar} * The type of the React {@code Component} props of {@link SideBar}.
*/ */
type Props = { type Props = {
@ -38,18 +37,21 @@ type Props = {
show: boolean show: boolean
} }
/**
* The type of the React {@code Component} state of {@link SideBar}.
*/
type State = { type State = {
/**
* Indicates whether the side bar is visible or not.
*/
showSideBar: boolean,
/** /**
* Indicates whether the side overlay should be rendered or not. * Indicates whether the side overlay should be rendered or not.
*/ */
showOverlay: boolean, showOverlay: boolean,
/**
* Indicates whether the side bar is visible or not.
*/
showSideBar: boolean,
/** /**
* The native animation object. * The native animation object.
*/ */
@ -63,7 +65,7 @@ export default class SideBar extends Component<Props, State> {
_mounted: boolean; _mounted: boolean;
/** /**
* Component's contructor. * Initializes a new {@code SideBar} instance.
* *
* @inheritdoc * @inheritdoc
*/ */
@ -71,15 +73,15 @@ export default class SideBar extends Component<Props, State> {
super(props); super(props);
this.state = { this.state = {
showSideBar: false,
showOverlay: false, showOverlay: false,
showSideBar: false,
sliderAnimation: new Animated.Value(-SIDEBAR_WIDTH) sliderAnimation: new Animated.Value(-SIDEBAR_WIDTH)
}; };
this._setShow = this._setShow.bind(this);
this._getContainerStyle = this._getContainerStyle.bind(this); this._getContainerStyle = this._getContainerStyle.bind(this);
this._onHideMenu = this._onHideMenu.bind(this); this._onHideMenu = this._onHideMenu.bind(this);
this._setShow = this._setShow.bind(this);
this._setShow(props.show); this._setShow(props.show);
} }
@ -171,8 +173,8 @@ export default class SideBar extends Component<Props, State> {
/** /**
* Sets the side menu visible or hidden. * Sets the side menu visible or hidden.
* *
* @private
* @param {boolean} show - The new expected visibility value. * @param {boolean} show - The new expected visibility value.
* @private
* @returns {void} * @returns {void}
*/ */
_setShow(show) { _setShow(show) {
@ -183,15 +185,17 @@ export default class SideBar extends Component<Props, State> {
}); });
} }
Animated.timing(this.state.sliderAnimation, { Animated
toValue: show ? 0 : -SIDEBAR_WIDTH .timing(
}).start(animationState => { this.state.sliderAnimation,
if (animationState.finished && !show) { { toValue: show ? 0 : -SIDEBAR_WIDTH })
this.setState({ .start(animationState => {
showOverlay: false if (animationState.finished && !show) {
}); this.setState({
} showOverlay: false
}); });
}
});
} }
if (this._mounted) { if (this._mounted) {
@ -200,5 +204,4 @@ export default class SideBar extends Component<Props, State> {
}); });
} }
} }
} }

View File

@ -1,8 +1,7 @@
export { default as Container } from './Container'; export { default as Container } from './Container';
export { default as Header } from './Header';
export { default as Link } from './Link'; export { default as Link } from './Link';
export { default as LoadingIndicator } from './LoadingIndicator'; export { default as LoadingIndicator } from './LoadingIndicator';
export { default as Header } from './Header';
export { default as SideBar } from './SideBar'; export { default as SideBar } from './SideBar';
export * from './styles';
export { default as TintedView } from './TintedView';
export { default as Text } from './Text'; export { default as Text } from './Text';
export { default as TintedView } from './TintedView';

View File

@ -14,11 +14,20 @@ export const STATUSBAR_COLOR = ColorPalette.blueHighlight;
export const SIDEBAR_WIDTH = 250; export const SIDEBAR_WIDTH = 250;
/** /**
* The styles of the React {@code Components} of the generic components * The styles of the generic React {@code Components} of the app.
* in the app.
*/ */
export default createStyleSheet({ export default createStyleSheet({
/**
* Platform specific header button (e.g. back, menu...etc).
*/
headerButton: {
alignSelf: 'center',
color: ColorPalette.white,
fontSize: 26,
paddingRight: 22
},
/** /**
* Style of the header overlay to cover the unsafe areas. * Style of the header overlay to cover the unsafe areas.
*/ */
@ -26,6 +35,29 @@ export default createStyleSheet({
backgroundColor: HEADER_COLOR backgroundColor: HEADER_COLOR
}, },
/**
* Generic style for a label placed in the header.
*/
headerText: {
color: ColorPalette.white,
fontSize: 20
},
/**
* The top-level element of a page.
*/
page: {
alignItems: 'stretch',
bottom: 0,
flex: 1,
flexDirection: 'column',
left: 0,
overflow: 'hidden',
position: 'absolute',
right: 0,
top: 0
},
/** /**
* Base style of Header * Base style of Header
*/ */

View File

@ -1,41 +0,0 @@
import { ColorPalette } from './ColorPalette';
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
},
/**
* The topmost level element of a page.
*/
page: {
alignItems: 'stretch',
bottom: 0,
flex: 1,
flexDirection: 'column',
left: 0,
overflow: 'hidden',
position: 'absolute',
right: 0,
top: 0
}
});

View File

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

View File

@ -1 +0,0 @@
export * from './components';

View File

@ -0,0 +1,10 @@
/**
* The type of (redux) action which sets the visibility of the view/UI rendering
* the app's settings.
*
* {
* type: SET_SETTINGS_VIEW_VISIBLE
* visible: boolean
* }
*/
export const SET_SETTINGS_VIEW_VISIBLE = Symbol('SET_SETTINGS_VIEW_VISIBLE');

View File

@ -0,0 +1,20 @@
// @flow
import { SET_SETTINGS_VIEW_VISIBLE } from './actionTypes';
/**
* Sets the visibility of the view/UI which renders the app's settings.
*
* @param {boolean} visible - If the view/UI which renders the app's settings is
* to be made visible, {@code true}; otherwise, {@code false}.
* @returns {{
* type: SET_SETTINGS_VIEW_VISIBLE,
* visible: boolean
* }}
*/
export function setSettingsViewVisible(visible: boolean) {
return {
type: SET_SETTINGS_VIEW_VISIBLE,
visible
};
}

View File

@ -5,7 +5,8 @@ import { Component } from 'react';
import { getProfile, updateProfile } from '../../base/profile'; import { getProfile, updateProfile } from '../../base/profile';
/** /**
* The type of the React {@code Component} props of {@link AbstractAppSettings} * The type of the React {@code Component} props of
* {@link AbstractSettingsView}.
*/ */
type Props = { type Props = {
@ -20,7 +21,7 @@ type Props = {
_serverURL: string, _serverURL: string,
/** /**
* The visibility prop of the settings screen. * Whether {@link AbstractSettingsView} is visible.
*/ */
_visible: boolean, _visible: boolean,
@ -41,10 +42,10 @@ type Props = {
* *
* @abstract * @abstract
*/ */
export class AbstractAppSettings extends Component<Props> { export class AbstractSettingsView extends Component<Props> {
/** /**
* Initializes a new {@code AbstractAppSettings} instance. * Initializes a new {@code AbstractSettingsView} instance.
* *
* @param {Props} props - The React {@code Component} props to initialize * @param {Props} props - The React {@code Component} props to initialize
* the component. * the component.
@ -52,6 +53,7 @@ export class AbstractAppSettings extends Component<Props> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
// Bind event handlers so they are only bound once per instance.
this._onChangeDisplayName = this._onChangeDisplayName.bind(this); this._onChangeDisplayName = this._onChangeDisplayName.bind(this);
this._onChangeEmail = this._onChangeEmail.bind(this); this._onChangeEmail = this._onChangeEmail.bind(this);
this._onChangeServerURL = this._onChangeServerURL.bind(this); this._onChangeServerURL = this._onChangeServerURL.bind(this);
@ -66,8 +68,8 @@ export class AbstractAppSettings extends Component<Props> {
/** /**
* Handles the display name field value change. * Handles the display name field value change.
* *
* @protected
* @param {string} text - The value typed in the name field. * @param {string} text - The value typed in the name field.
* @protected
* @returns {void} * @returns {void}
*/ */
_onChangeDisplayName(text) { _onChangeDisplayName(text) {
@ -81,8 +83,8 @@ export class AbstractAppSettings extends Component<Props> {
/** /**
* Handles the email field value change. * Handles the email field value change.
* *
* @protected
* @param {string} text - The value typed in the email field. * @param {string} text - The value typed in the email field.
* @protected
* @returns {void} * @returns {void}
*/ */
_onChangeEmail(text) { _onChangeEmail(text) {
@ -96,8 +98,8 @@ export class AbstractAppSettings extends Component<Props> {
/** /**
* Handles the server name field value change. * Handles the server name field value change.
* *
* @protected
* @param {string} text - The server URL typed in the server field. * @param {string} text - The server URL typed in the server field.
* @protected
* @returns {void} * @returns {void}
*/ */
_onChangeServerURL(text) { _onChangeServerURL(text) {
@ -111,9 +113,9 @@ export class AbstractAppSettings extends Component<Props> {
/** /**
* Handles the start audio muted change event. * Handles the start audio muted change event.
* *
* @param {boolean} newValue - The new value for the start audio muted
* option.
* @protected * @protected
* @param {boolean} newValue - The new value for the
* start audio muted option.
* @returns {void} * @returns {void}
*/ */
_onStartAudioMutedChange(newValue) { _onStartAudioMutedChange(newValue) {
@ -127,9 +129,9 @@ export class AbstractAppSettings extends Component<Props> {
/** /**
* Handles the start video muted change event. * Handles the start video muted change event.
* *
* @param {boolean} newValue - The new value for the start video muted
* option.
* @protected * @protected
* @param {boolean} newValue - The new value for the
* start video muted option.
* @returns {void} * @returns {void}
*/ */
_onStartVideoMutedChange(newValue) { _onStartVideoMutedChange(newValue) {
@ -143,8 +145,8 @@ export class AbstractAppSettings extends Component<Props> {
/** /**
* Updates the persisted profile on any change. * Updates the persisted profile on any change.
* *
* @private
* @param {Object} updateObject - The partial update object for the profile. * @param {Object} updateObject - The partial update object for the profile.
* @private
* @returns {void} * @returns {void}
*/ */
_updateProfile(updateObject: Object) { _updateProfile(updateObject: Object) {
@ -157,19 +159,20 @@ export class AbstractAppSettings extends Component<Props> {
/** /**
* Maps (parts of) the redux state to the React {@code Component} props of * Maps (parts of) the redux state to the React {@code Component} props of
* {@code AbstractAppSettings}. * {@code AbstractSettingsView}.
* *
* @param {Object} state - The redux state. * @param {Object} state - The redux state.
* @protected * @protected
* @returns {Object} * @returns {{
* _profile: Object,
* _serverURL: string,
* _visible: boolean
* }}
*/ */
export function _mapStateToProps(state: Object) { export function _mapStateToProps(state: Object) {
const _serverURL = state['features/app'].app._getDefaultURL();
const _profile = getProfile(state);
return { return {
_profile, _profile: getProfile(state),
_serverURL, _serverURL: state['features/app'].app._getDefaultURL(),
_visible: state['features/app-settings'].visible _visible: state['features/settings'].visible
}; };
} }

View File

@ -0,0 +1 @@
export * from './web';

View File

@ -0,0 +1 @@
export * from './_';

View File

@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { TouchableOpacity } from 'react-native'; import { TouchableOpacity } from 'react-native';
import { Icon } from '../../../base/font-icons'; import { Icon } from '../../../base/font-icons';
import { PlatformElements } from '../../../base/styles'; import { Header } from '../../../base/react';
/** /**
* The type of the React {@code Component} props of {@link BackButton} * The type of the React {@code Component} props of {@link BackButton}
@ -40,7 +40,7 @@ export default class BackButton extends Component<Props> {
<Icon <Icon
name = { 'arrow_back' } name = { 'arrow_back' }
style = { [ style = { [
PlatformElements.headerButton, Header.buttonStyle,
this.props.style this.props.style
] } /> ] } />
</TouchableOpacity> </TouchableOpacity>

View File

@ -47,6 +47,7 @@ class FormRow extends Component<Props> {
super(props); super(props);
React.Children.only(this.props.children); React.Children.only(this.props.children);
this._getDefaultFieldProps = this._getDefaultFieldProps.bind(this); this._getDefaultFieldProps = this._getDefaultFieldProps.bind(this);
this._getRowStyle = this._getRowStyle.bind(this); this._getRowStyle = this._getRowStyle.bind(this);
} }
@ -63,10 +64,10 @@ class FormRow extends Component<Props> {
// Some field types need additional props to look good and standardized // Some field types need additional props to look good and standardized
// on a form. // on a form.
const newChild = React.cloneElement( const newChild
this.props.children, = React.cloneElement(
this._getDefaultFieldProps(this.props.children) this.props.children,
); this._getDefaultFieldProps(this.props.children));
return ( return (
<View <View
@ -74,7 +75,8 @@ class FormRow extends Component<Props> {
<View style = { styles.fieldLabelContainer } > <View style = { styles.fieldLabelContainer } >
<Text <Text
style = { [ style = { [
styles.text, styles.fieldLabelText styles.text,
styles.fieldLabelText
] } > ] } >
{ t(this.props.i18nLabel) } { t(this.props.i18nLabel) }
</Text> </Text>
@ -96,8 +98,8 @@ class FormRow extends Component<Props> {
* - TextInput * - TextInput
* - Switch (needs no addition props ATM). * - Switch (needs no addition props ATM).
* *
* @private
* @param {Object} field - The field (child) component. * @param {Object} field - The field (child) component.
* @private
* @returns {Object} * @returns {Object}
*/ */
_getDefaultFieldProps(field: Object) { _getDefaultFieldProps(field: Object) {

View File

@ -0,0 +1,251 @@
// @flow
import React from 'react';
import {
Alert,
Modal,
SafeAreaView,
ScrollView,
Switch,
Text,
TextInput,
View
} from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n';
import { Header } from '../../../base/react';
import {
AbstractSettingsView,
_mapStateToProps
} from '../AbstractSettingsView';
import { setSettingsViewVisible } from '../../actions';
import BackButton from './BackButton';
import FormRow from './FormRow';
import FormSectionHeader from './FormSectionHeader';
import { normalizeUserInputURL } from '../../functions';
import styles from './styles';
/**
* The native container rendering the app settings page.
*
* @extends AbstractSettingsView
*/
class SettingsView extends AbstractSettingsView {
_urlField: Object;
/**
* Initializes a new {@code SettingsView} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onBlurServerURL = this._onBlurServerURL.bind(this);
this._onRequestClose = this._onRequestClose.bind(this);
this._setURLFieldReference = this._setURLFieldReference.bind(this);
this._showURLAlert = this._showURLAlert.bind(this);
}
/**
* Implements React's {@link Component#render()}, renders the settings page.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Modal
animationType = 'slide'
onRequestClose = { this._onRequestClose }
presentationStyle = 'fullScreen'
supportedOrientations = { [
'landscape',
'portrait'
] }
visible = { this.props._visible }>
<View style = { Header.pageStyle }>
{ this._renderHeader() }
{ this._renderBody() }
</View>
</Modal>
);
}
_onBlurServerURL: () => void;
/**
* Handler the server URL lose focus event. Here we validate the server URL
* and update it to the normalized version, or show an error if incorrect.
*
* @private
* @returns {void}
*/
_onBlurServerURL() {
this._processServerURL(false /* hideOnSuccess */);
}
_onChangeDisplayName: (string) => void;
_onChangeEmail: (string) => void;
_onChangeServerURL: (string) => void;
_onRequestClose: () => void;
/**
* Handles the back button. Also invokes normalizeUserInputURL to validate
* the URL entered by the user.
*
* @returns {void}
*/
_onRequestClose() {
this._processServerURL(true /* hideOnSuccess */);
}
_onStartAudioMutedChange: (boolean) => void;
_onStartVideoMutedChange: (boolean) => void;
/**
* Processes the server URL. It normalizes it and an error alert is
* displayed in case it's incorrect.
*
* @param {boolean} hideOnSuccess - True if the dialog should be hidden if
* normalization / validation succeeds, false otherwise.
* @private
* @returns {void}
*/
_processServerURL(hideOnSuccess: boolean) {
const { serverURL } = this.props._profile;
const normalizedURL = normalizeUserInputURL(serverURL);
if (normalizedURL === null) {
this._showURLAlert();
} else {
this._onChangeServerURL(normalizedURL);
if (hideOnSuccess) {
this.props.dispatch(setSettingsViewVisible(false));
}
}
}
/**
* Renders the body (under the header) of {@code SettingsView}.
*
* @private
* @returns {React$Element}
*/
_renderBody() {
const { _profile } = this.props;
return (
<SafeAreaView style = { styles.settingsForm }>
<ScrollView>
<FormSectionHeader
i18nLabel = 'settingsView.profileSection' />
<FormRow
fieldSeparator = { true }
i18nLabel = 'settingsView.displayName'>
<TextInput
onChangeText = { this._onChangeDisplayName }
placeholder = 'John Doe'
value = { _profile.displayName } />
</FormRow>
<FormRow i18nLabel = 'settingsView.email'>
<TextInput
keyboardType = { 'email-address' }
onChangeText = { this._onChangeEmail }
placeholder = 'email@example.com'
value = { _profile.email } />
</FormRow>
<FormSectionHeader
i18nLabel = 'settingsView.conferenceSection' />
<FormRow
fieldSeparator = { true }
i18nLabel = 'settingsView.serverURL'>
<TextInput
autoCapitalize = 'none'
onBlur = { this._onBlurServerURL }
onChangeText = { this._onChangeServerURL }
placeholder = { this.props._serverURL }
value = { _profile.serverURL } />
</FormRow>
<FormRow
fieldSeparator = { true }
i18nLabel = 'settingsView.startWithAudioMuted'>
<Switch
onValueChange = { this._onStartAudioMutedChange }
value = { _profile.startWithAudioMuted } />
</FormRow>
<FormRow i18nLabel = 'settingsView.startWithVideoMuted'>
<Switch
onValueChange = { this._onStartVideoMutedChange }
value = { _profile.startWithVideoMuted } />
</FormRow>
</ScrollView>
</SafeAreaView>
);
}
/**
* Renders the header of {@code SettingsView}.
*
* @private
* @returns {React$Element}
*/
_renderHeader() {
return (
<Header>
<BackButton onPress = { this._onRequestClose } />
<Text
style = { [
styles.text,
Header.textStyle
] }>
{ this.props.t('settingsView.header') }
</Text>
</Header>
);
}
_setURLFieldReference: (React$ElementRef<*> | null) => void;
/**
* Stores a reference to the URL field for later use.
*
* @param {Object} component - The field component.
* @protected
* @returns {void}
*/
_setURLFieldReference(component) {
this._urlField = component;
}
_showURLAlert: () => void;
/**
* Shows an alert telling the user that the URL he/she entered was invalid.
*
* @returns {void}
*/
_showURLAlert() {
const { t } = this.props;
Alert.alert(
t('settingsView.alertTitle'),
t('settingsView.alertURLText'),
[
{
onPress: () => this._urlField.focus(),
text: t('settingsView.alertOk')
}
]
);
}
}
export default translate(connect(_mapStateToProps)(SettingsView));

View File

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

View File

@ -7,8 +7,7 @@ export const ANDROID_UNDERLINE_COLOR = 'transparent';
const TEXT_SIZE = 17; const TEXT_SIZE = 17;
/** /**
* The styles of the native components of the feature * The styles of the native components of the feature {@code settings}.
* {@code app-settings}.
*/ */
export default createStyleSheet({ export default createStyleSheet({
/** /**

View File

@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { translate } from '../../base/i18n'; import { translate } from '../../../base/i18n';
import { openDeviceSelectionDialog } from '../../device-selection'; import { openDeviceSelectionDialog } from '../../../device-selection';
/** /**
* Implements a React {@link Component} which displays a button for opening the * Implements a React {@link Component} which displays a button for opening the

View File

@ -5,7 +5,7 @@ import DropdownMenu, {
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { DEFAULT_LANGUAGE, LANGUAGES, translate } from '../../base/i18n'; import { DEFAULT_LANGUAGE, LANGUAGES, translate } from '../../../base/i18n';
/** /**
* Implements a React {@link Component} which displays a dropdown for changing * Implements a React {@link Component} which displays a dropdown for changing

View File

@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { setFollowMe, setStartMutedPolicy } from '../../base/conference'; import { setFollowMe, setStartMutedPolicy } from '../../../base/conference';
import { translate } from '../../base/i18n'; import { translate } from '../../../base/i18n';
/** /**
* Implements a React {@link Component} which displays checkboxes for enabling * Implements a React {@link Component} which displays checkboxes for enabling

View File

@ -2,8 +2,11 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { translate } from '../../base/i18n'; import { translate } from '../../../base/i18n';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants'; import {
getLocalParticipant,
PARTICIPANT_ROLE
} from '../../../base/participants';
import DeviceSelectionButton from './DeviceSelectionButton'; import DeviceSelectionButton from './DeviceSelectionButton';
import LanguageSelectDropdown from './LanguageSelectDropdown'; import LanguageSelectDropdown from './LanguageSelectDropdown';

View File

@ -14,6 +14,7 @@ export function normalizeUserInputURL(url: string) {
if (url) { if (url) {
url = url.replace(/\s/g, '').toLowerCase(); url = url.replace(/\s/g, '').toLowerCase();
const urlRegExp = new RegExp('^(\\w+://)?(.+)$'); const urlRegExp = new RegExp('^(\\w+://)?(.+)$');
const urlComponents = urlRegExp.exec(url); const urlComponents = urlRegExp.exec(url);
@ -21,8 +22,7 @@ export function normalizeUserInputURL(url: string) {
url = `https://${urlComponents[2]}`; url = `https://${urlComponents[2]}`;
} }
const parsedURI const parsedURI = parseStandardURIString(url);
= parseStandardURIString(url);
if (!parsedURI.host) { if (!parsedURI.host) {
return null; return null;

View File

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

View File

@ -3,35 +3,34 @@
import { SET_ROOM } from '../base/conference'; import { SET_ROOM } from '../base/conference';
import { MiddlewareRegistry } from '../base/redux'; import { MiddlewareRegistry } from '../base/redux';
import { hideAppSettings } from './actions'; import { setSettingsViewVisible } from './actions';
/** /**
* The Redux middleware to trigger settings screen show or hide * The redux middleware to set the visibility of {@link SettingsView}.
* when necessary.
* *
* @param {Store} store - The Redux store. * @param {Store} store - The redux store.
* @returns {Function} * @returns {Function}
*/ */
MiddlewareRegistry.register(store => next => action => { MiddlewareRegistry.register(store => next => action => {
switch (action.type) { switch (action.type) {
case SET_ROOM: case SET_ROOM:
return _closeAppSettings(store, next, action); return _hideSettingsView(store, next, action);
} }
return next(action); return next(action);
}); });
/** /**
* Hides the settings screen. * Hides {@link SettingsView}.
* *
* @param {Store} store - The redux store. * @param {Store} store - The redux store.
* @param {Dispatch} next - The redux dispatch function. * @param {Dispatch} next - The redux {@code dispatch} function.
* @param {Action} action - The redux action. * @param {Action} action - The redux action.
* @private * @private
* @returns {Object} The new state. * @returns {Object} The new state.
*/ */
function _closeAppSettings({ dispatch }, next, action) { function _hideSettingsView({ dispatch }, next, action) {
dispatch(hideAppSettings()); dispatch(setSettingsViewVisible(false));
return next(action); return next(action);
} }

View File

@ -0,0 +1,17 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { SET_SETTINGS_VIEW_VISIBLE } from './actionTypes';
ReducerRegistry.register('features/settings', (state = {}, action) => {
switch (action.type) {
case SET_SETTINGS_VIEW_VISIBLE:
return {
...state,
visible: action.visible
};
}
return state;
});

View File

@ -1,4 +1,10 @@
/** /**
* Action type to signal the need to hide or show the side bar. * The type of the (redux) action which sets the visibility of
* {@link WelcomePageSideBar}.
*
* {
* type: SET_SIDEBAR_VISIBLE,
* visible: boolean
* }
*/ */
export const SET_SIDEBAR_VISIBILITY = Symbol('SET_SIDEBAR_VISIBILITY'); export const SET_SIDEBAR_VISIBLE = Symbol('SET_SIDEBAR_VISIBLE');

View File

@ -1,19 +1,20 @@
// @flow // @flow
import { SET_SIDEBAR_VISIBILITY } from './actionTypes'; import { SET_SIDEBAR_VISIBLE } from './actionTypes';
/** /**
* Redux action to hide or show the status bar. * Sets the visibility of {@link WelcomePageSideBar}.
* *
* @param {boolean} visible - The new value of the visibility. * @param {boolean} visible - If the {@code WelcomePageSideBar} is to be made
* visible, {@code true}; otherwise, {@code false}.
* @returns {{ * @returns {{
* type: SET_SIDEBAR_VISIBILITY, * type: SET_SIDEBAR_VISIBLE,
* sideBarVisible: boolean * visible: boolean
* }} * }}
*/ */
export function setSideBarVisibility(visible: boolean) { export function setSideBarVisible(visible: boolean) {
return { return {
type: SET_SIDEBAR_VISIBILITY, type: SET_SIDEBAR_VISIBLE,
sideBarVisible: visible visible
}; };
} }

View File

@ -3,11 +3,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Linking, Text, TouchableOpacity, View } from 'react-native'; import { Linking, Text, TouchableOpacity, View } from 'react-native';
import styles from './styles';
import { Icon } from '../../base/font-icons'; import { Icon } from '../../base/font-icons';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import styles from './styles';
type Props = { type Props = {
/** /**
@ -43,13 +43,14 @@ type Props = {
class SideBarItem extends Component<Props> { class SideBarItem extends Component<Props> {
/** /**
* Contructor of the SideBarItem Component. * Initializes a new {@code SideBarItem} instance.
* *
* @inheritdoc * @inheritdoc
*/ */
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
// Bind event handlers so they are only bound once per instance.
this._onOpenURL = this._onOpenURL.bind(this); this._onOpenURL = this._onOpenURL.bind(this);
} }

View File

@ -11,26 +11,21 @@ import {
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppSettings } from '../../app-settings';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { Icon } from '../../base/font-icons'; 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 { import { LoadingIndicator, Header, Text } from '../../base/react';
LoadingIndicator, import { ColorPalette } from '../../base/styles';
Header,
Text
} from '../../base/react';
import { ColorPalette, PlatformElements } from '../../base/styles';
import { import {
createDesiredLocalTracks, createDesiredLocalTracks,
destroyLocalTracks destroyLocalTracks
} from '../../base/tracks'; } from '../../base/tracks';
import { RecentList } from '../../recent-list'; import { RecentList } from '../../recent-list';
import { SettingsView } from '../../settings';
import { setSideBarVisibility } from '../actions';
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
import { setSideBarVisible } from '../actions';
import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay'; import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay';
import styles, { import styles, {
PLACEHOLDER_TEXT_COLOR, PLACEHOLDER_TEXT_COLOR,
@ -62,6 +57,7 @@ class WelcomePage extends AbstractWelcomePage {
this.state.hintBoxAnimation = new Animated.Value(0); this.state.hintBoxAnimation = new Animated.Value(0);
// Bind event handlers so they are only bound once per instance.
this._getHintBoxStyle = this._getHintBoxStyle.bind(this); this._getHintBoxStyle = this._getHintBoxStyle.bind(this);
this._onFieldFocusChange = this._onFieldFocusChange.bind(this); this._onFieldFocusChange = this._onFieldFocusChange.bind(this);
this._onShowSideBar = this._onShowSideBar.bind(this); this._onShowSideBar = this._onShowSideBar.bind(this);
@ -97,20 +93,21 @@ class WelcomePage extends AbstractWelcomePage {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const { buttonStyle, pageStyle, textStyle } = Header;
const { t, _profile } = this.props; const { t, _profile } = this.props;
return ( return (
<LocalVideoTrackUnderlay style = { styles.welcomePage }> <LocalVideoTrackUnderlay style = { styles.welcomePage }>
<View style = { PlatformElements.page }> <View style = { pageStyle }>
<Header style = { styles.header }> <Header style = { styles.header }>
<TouchableOpacity onPress = { this._onShowSideBar } > <TouchableOpacity onPress = { this._onShowSideBar } >
<Icon <Icon
name = 'menu' name = 'menu'
style = { PlatformElements.headerButton } /> style = { buttonStyle } />
</TouchableOpacity> </TouchableOpacity>
<View style = { styles.audioVideoSwitchContainer }> <View style = { styles.audioVideoSwitchContainer }>
<Text style = { PlatformElements.headerText } > <Text style = { textStyle } >
{ t('welcomepage.videoEnabledLabel') } { t('welcomepage.audioVideoSwitch.video') }
</Text> </Text>
<Switch <Switch
onTintColor = { SWITCH_UNDER_COLOR } onTintColor = { SWITCH_UNDER_COLOR }
@ -118,8 +115,8 @@ class WelcomePage extends AbstractWelcomePage {
style = { styles.audioVideoSwitch } style = { styles.audioVideoSwitch }
thumbTintColor = { SWITCH_THUMB_COLOR } thumbTintColor = { SWITCH_THUMB_COLOR }
value = { _profile.startAudioOnly } /> value = { _profile.startAudioOnly } />
<Text style = { PlatformElements.headerText } > <Text style = { textStyle } >
{ t('welcomepage.audioOnlyLabel') } { t('welcomepage.audioVideoSwitch.audio') }
</Text> </Text>
</View> </View>
</Header> </Header>
@ -145,7 +142,7 @@ class WelcomePage extends AbstractWelcomePage {
} }
<RecentList disabled = { this.state._fieldFocused } /> <RecentList disabled = { this.state._fieldFocused } />
</SafeAreaView> </SafeAreaView>
<AppSettings /> <SettingsView />
</View> </View>
<WelcomePageSideBar /> <WelcomePageSideBar />
</LocalVideoTrackUnderlay> </LocalVideoTrackUnderlay>
@ -204,7 +201,7 @@ class WelcomePage extends AbstractWelcomePage {
*/ */
_onShowSideBar() { _onShowSideBar() {
Keyboard.dismiss(); Keyboard.dismiss();
this.props.dispatch(setSideBarVisibility(true)); this.props.dispatch(setSideBarVisible(true));
} }
/** /**
@ -234,17 +231,14 @@ class WelcomePage extends AbstractWelcomePage {
const { t } = this.props; const { t } = this.props;
return ( return (
<Animated.View <Animated.View style = { this._getHintBoxStyle() }>
style = { this._getHintBoxStyle() }>
<View style = { styles.hintTextContainer } > <View style = { styles.hintTextContainer } >
<Text> <Text>
{ t('welcomepage.hintText') } { t('welcomepage.roomnameHint') }
</Text> </Text>
</View> </View>
<View style = { styles.hintButtonContainer } > <View style = { styles.hintButtonContainer } >
{ { this._renderJoinButton() }
this._renderJoinButton()
}
</View> </View>
</Animated.View> </Animated.View>
); );

View File

@ -4,12 +4,6 @@ import React, { Component } from 'react';
import { SafeAreaView, ScrollView, Text } from 'react-native'; import { SafeAreaView, ScrollView, Text } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SideBarItem from './SideBarItem';
import styles from './styles';
import { setSideBarVisibility } from '../actions';
import { showAppSettings } from '../../app-settings';
import { import {
Avatar, Avatar,
getAvatarURL, getAvatarURL,
@ -20,6 +14,11 @@ import {
Header, Header,
SideBar SideBar
} from '../../base/react'; } from '../../base/react';
import { setSettingsViewVisible } from '../../settings';
import { setSideBarVisible } from '../actions';
import SideBarItem from './SideBarItem';
import styles from './styles';
/** /**
* The URL at which the privacy policy is available to the user. * The URL at which the privacy policy is available to the user.
@ -71,6 +70,7 @@ class WelcomePageSideBar extends Component<Props> {
constructor(props) { constructor(props) {
super(props); super(props);
// Bind event handlers so they are only bound once per instance.
this._onHideSideBar = this._onHideSideBar.bind(this); this._onHideSideBar = this._onHideSideBar.bind(this);
this._onOpenSettings = this._onOpenSettings.bind(this); this._onOpenSettings = this._onOpenSettings.bind(this);
} }
@ -122,19 +122,19 @@ class WelcomePageSideBar extends Component<Props> {
_onHideSideBar: () => void; _onHideSideBar: () => void;
/** /**
* Invoked when the sidebar has closed itslef (e.g. overlay pressed). * Invoked when the sidebar has closed itself (e.g. overlay pressed).
* *
* @private * @private
* @returns {void} * @returns {void}
*/ */
_onHideSideBar() { _onHideSideBar() {
this.props.dispatch(setSideBarVisibility(false)); this.props.dispatch(setSideBarVisible(false));
} }
_onOpenSettings: () => void; _onOpenSettings: () => void;
/** /**
* Opens the settings screen. * Shows the {@link SettingsView}.
* *
* @private * @private
* @returns {void} * @returns {void}
@ -142,8 +142,8 @@ class WelcomePageSideBar extends Component<Props> {
_onOpenSettings() { _onOpenSettings() {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(setSideBarVisibility(false)); dispatch(setSideBarVisible(false));
dispatch(showAppSettings()); dispatch(setSettingsViewVisible(true));
} }
} }

View File

@ -98,14 +98,7 @@ export default createStyleSheet({
*/ */
hintButtonContainer: { hintButtonContainer: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'flex-end' justifyContent: 'center'
},
/**
* Container for the text on the hint box.
*/
hintTextContainer: {
marginBottom: 2 * BoxModel.margin
}, },
/** /**
@ -123,6 +116,13 @@ export default createStyleSheet({
paddingVertical: 2 * BoxModel.padding paddingVertical: 2 * BoxModel.padding
}, },
/**
* Container for the text on the hint box.
*/
hintTextContainer: {
marginBottom: 2 * BoxModel.margin
},
/** /**
* Container for the items in the side bar. * Container for the items in the side bar.
*/ */
@ -142,7 +142,7 @@ export default createStyleSheet({
}, },
/** /**
* Top level screen style * Top-level screen style.
*/ */
page: { page: {
flex: 1, flex: 1,

View File

@ -1,5 +1,5 @@
import './reducer';
import './route';
export * from './components'; export * from './components';
export * from './functions'; export * from './functions';
import './reducer';
import './route';

View File

@ -1,23 +1,20 @@
import { ReducerRegistry } from '../base/redux'; // @flow
import { SET_SIDEBAR_VISIBILITY } from './actionTypes';
const DEFAULT_STATE = { import { ReducerRegistry } from '../base/redux';
sideBarVisible: false import { SET_SIDEBAR_VISIBLE } from './actionTypes';
};
/** /**
* Reduces the Redux actions of the feature features/recording. * Reduces redux actions for the purposes of {@code features/welcome}.
*/ */
ReducerRegistry.register('features/welcome', ReducerRegistry.register('features/welcome', (state = {}, action) => {
(state = DEFAULT_STATE, action) => { switch (action.type) {
switch (action.type) { case SET_SIDEBAR_VISIBLE:
case SET_SIDEBAR_VISIBILITY: return {
return { ...state,
...state, sideBarVisible: action.visible
sideBarVisible: action.sideBarVisible };
};
default: default:
return state; return state;
} }
}); });