[RN] Fix base/profile and recent-list bugs
This commit is contained in:
parent
e4da0e988e
commit
d727ee80b2
|
@ -4,7 +4,6 @@ import { setRoom } from '../base/conference';
|
|||
import { configWillLoad, loadConfigError, setConfig } from '../base/config';
|
||||
import { setLocationURL } from '../base/connection';
|
||||
import { loadConfig } from '../base/lib-jitsi-meet';
|
||||
import { getProfile } from '../base/profile';
|
||||
import { parseURIString } from '../base/util';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
|
||||
|
@ -15,7 +14,7 @@ declare var APP: Object;
|
|||
* Triggers an in-app navigation to a specific route. Allows navigation to be
|
||||
* abstracted between the mobile/React Native and Web/React applications.
|
||||
*
|
||||
* @param {(string|undefined)} uri - The URI to which to navigate. It may be a
|
||||
* @param {string|undefined} uri - The URI to which to navigate. It may be a
|
||||
* full URL with an HTTP(S) scheme, a full or partial URI with the app-specific
|
||||
* scheme, or a mere room name.
|
||||
* @returns {Function}
|
||||
|
@ -83,11 +82,10 @@ function _appNavigateToMandatoryLocation(
|
|||
});
|
||||
}
|
||||
|
||||
const profile = getProfile(getState());
|
||||
const profile = getState()['features/base/profile'];
|
||||
|
||||
return promise.then(() => dispatch(setConfig(
|
||||
_mergeConfigWithProfile(config, profile)
|
||||
)));
|
||||
return promise.then(() =>
|
||||
dispatch(setConfig(_mergeConfigWithProfile(config, profile))));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
localParticipantJoined,
|
||||
localParticipantLeft
|
||||
} from '../../base/participants';
|
||||
import { getProfile } from '../../base/profile';
|
||||
import '../../base/profile';
|
||||
import { Fragment, RouteRegistry } from '../../base/react';
|
||||
import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux';
|
||||
import { PersistenceRegistry } from '../../base/storage';
|
||||
|
@ -123,7 +123,7 @@ export class AbstractApp extends Component {
|
|||
*/
|
||||
componentWillMount() {
|
||||
this._init.then(() => {
|
||||
const { dispatch } = this._getStore();
|
||||
const { dispatch, getState } = this._getStore();
|
||||
|
||||
dispatch(appWillMount(this));
|
||||
|
||||
|
@ -144,7 +144,7 @@ export class AbstractApp extends Component {
|
|||
}
|
||||
|
||||
// Profile is the new React compatible settings.
|
||||
const profile = getProfile(this._getStore().getState());
|
||||
const profile = getState()['features/base/profile'];
|
||||
|
||||
if (profile) {
|
||||
localParticipant.email
|
||||
|
@ -381,7 +381,8 @@ export class AbstractApp extends Component {
|
|||
|
||||
return (
|
||||
this.props.defaultURL
|
||||
|| getProfile(this._getStore().getState()).serverURL
|
||||
|| this._getStore().getState()['features/base/profile']
|
||||
.serverURL
|
||||
|| DEFAULT_URL);
|
||||
}
|
||||
|
||||
|
|
|
@ -128,7 +128,7 @@ function _conferenceFailedOrLeft({ dispatch, getState }, next, action) {
|
|||
|
||||
const state = getState();
|
||||
const { audioOnly } = state['features/base/conference'];
|
||||
const { startAudioOnly } = state['features/base/profile'].profile;
|
||||
const { startAudioOnly } = state['features/base/profile'];
|
||||
|
||||
// FIXME: Consider implementing a standalone audio-only feature that handles
|
||||
// all these state changes.
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/* @flow */
|
||||
|
||||
/**
|
||||
* Retreives the current profile settings from redux store. The profile
|
||||
* is persisted to localStorage so it's a good candidate to store settings
|
||||
* in it.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getProfile(state: Object) {
|
||||
const profileStateSlice = state['features/base/profile'];
|
||||
|
||||
return profileStateSlice || {};
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
export * from './actions';
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import { setAudioOnly } from '../conference';
|
||||
import { getLocalParticipant, participantUpdated } from '../participants';
|
||||
import { getProfile } from '../profile';
|
||||
import { MiddlewareRegistry, toState } from '../redux';
|
||||
|
||||
import { PROFILE_UPDATED } from './actionTypes';
|
||||
|
@ -33,11 +32,11 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
* @param {Object} action - The redux action.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeUpdateStartAudioOnly(store, action) {
|
||||
const { profile } = action;
|
||||
|
||||
if (typeof profile.startAudioOnly === 'boolean') {
|
||||
store.dispatch(setAudioOnly(profile.startAudioOnly));
|
||||
function _maybeUpdateStartAudioOnly(
|
||||
{ dispatch },
|
||||
{ profile: { startAudioOnly } }) {
|
||||
if (typeof startAudioOnly === 'boolean') {
|
||||
dispatch(setAudioOnly(startAudioOnly));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,7 +49,7 @@ function _maybeUpdateStartAudioOnly(store, action) {
|
|||
function _updateLocalParticipant(store) {
|
||||
const state = toState(store);
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const profile = getProfile(state);
|
||||
const profile = state['features/base/profile'];
|
||||
|
||||
store.dispatch(participantUpdated({
|
||||
// Identify that the participant to update i.e. the local participant:
|
||||
|
|
|
@ -1,26 +1,53 @@
|
|||
// @flow
|
||||
|
||||
import { APP_WILL_MOUNT } from '../../app';
|
||||
import { ReducerRegistry } from '../redux';
|
||||
import { PersistenceRegistry } from '../storage';
|
||||
|
||||
import { PROFILE_UPDATED } from './actionTypes';
|
||||
|
||||
/**
|
||||
* The default/initial redux state of the feature {@code base/profile}.
|
||||
*
|
||||
* @type Object
|
||||
*/
|
||||
const DEFAULT_STATE = {};
|
||||
|
||||
const STORE_NAME = 'features/base/profile';
|
||||
|
||||
/**
|
||||
* Sets up the persistence of the feature base/profile.
|
||||
* Sets up the persistence of the feature {@code base/profile}.
|
||||
*/
|
||||
PersistenceRegistry.register(STORE_NAME);
|
||||
|
||||
ReducerRegistry.register(
|
||||
STORE_NAME, (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case PROFILE_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
...action.profile
|
||||
};
|
||||
}
|
||||
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
// XXX APP_WILL_MOUNT is the earliest redux action of ours dispatched in
|
||||
// the store. For the purposes of legacy support, make sure that the
|
||||
// deserialized base/profile's state is in the format deemed current by
|
||||
// the current app revision.
|
||||
if (state && typeof state === 'object') {
|
||||
// In an enterprise/internal build of Jitsi Meet for Android and iOS
|
||||
// we had base/profile's state as an object with property profile.
|
||||
const { profile } = state;
|
||||
|
||||
return state;
|
||||
});
|
||||
if (profile && typeof profile === 'object') {
|
||||
return { ...profile };
|
||||
}
|
||||
} else {
|
||||
// In the weird case that we have previously persisted/serialized
|
||||
// null.
|
||||
return DEFAULT_STATE;
|
||||
}
|
||||
break;
|
||||
|
||||
case PROFILE_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
...action.profile
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
// @flow
|
||||
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
import md5 from 'js-md5';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* The name of the localStorage store where the app persists its values to.
|
||||
* The name of the {@code localStorage} store where the app persists its values.
|
||||
*/
|
||||
const PERSISTED_STATE_NAME = 'jitsi-state';
|
||||
|
||||
/**
|
||||
* Mixed type of the element (subtree) config. If it's a boolean,
|
||||
* (and is true) we persist the entire subtree. If it's an object,
|
||||
* we perist a filtered subtree based on the properties in the
|
||||
* config object.
|
||||
* Mixed type of the element (subtree) config. If it's a {@code boolean} (and is
|
||||
* {@code true}), we persist the entire subtree. If it's an {@code Object}, we
|
||||
* perist a filtered subtree based on the properties of the config object.
|
||||
*/
|
||||
declare type ElementConfig = Object | boolean;
|
||||
declare type ElementConfig = boolean | Object;
|
||||
|
||||
/**
|
||||
* The type of the name-config pairs stored in this reducer.
|
||||
* The type of the name-config pairs stored in {@code PersistenceRegistry}.
|
||||
*/
|
||||
declare type PersistencyConfigMap = { [name: string]: ElementConfig };
|
||||
|
||||
|
@ -30,74 +28,64 @@ declare type PersistencyConfigMap = { [name: string]: ElementConfig };
|
|||
class PersistenceRegistry {
|
||||
_checksum: string;
|
||||
|
||||
_elements: PersistencyConfigMap;
|
||||
_elements: PersistencyConfigMap = {};
|
||||
|
||||
/**
|
||||
* Initializes a new {@ code PersistenceRegistry} instance.
|
||||
*/
|
||||
constructor() {
|
||||
this._elements = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the persisted redux state. This function takes the
|
||||
* {@link #_elements} into account as we may have persisted something in the
|
||||
* past that we don't want to retreive anymore. The next
|
||||
* {@link #persistState} will remove those values.
|
||||
* Returns the persisted redux state. Takes the {@link #_elements} into
|
||||
* account as we may have persisted something in the past that we don't want
|
||||
* to retreive anymore. The next {@link #persistState} will remove such
|
||||
* values.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getPersistedState() {
|
||||
let filteredPersistedState = {};
|
||||
let persistedState = window.localStorage.getItem(PERSISTED_STATE_NAME);
|
||||
|
||||
if (persistedState) {
|
||||
// This is the legacy implementation,
|
||||
// must be removed in a later version.
|
||||
try {
|
||||
persistedState = JSON.parse(persistedState);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Error parsing persisted state',
|
||||
persistedState,
|
||||
error);
|
||||
persistedState = {};
|
||||
}
|
||||
// localStorage key per feature
|
||||
for (const subtreeName of Object.keys(this._elements)) {
|
||||
// Assumes that the persisted value is stored under the same key as
|
||||
// the feature's redux state name.
|
||||
// TODO We'll need to introduce functions later that can control the
|
||||
// persist key's name. Similar to control serialization and
|
||||
// deserialization. But that should be a straightforward change.
|
||||
const persistedSubtree
|
||||
= this._getPersistedSubtree(
|
||||
subtreeName,
|
||||
this._elements[subtreeName]);
|
||||
|
||||
filteredPersistedState
|
||||
= this._getFilteredState(persistedState);
|
||||
|
||||
// legacy values must be written to the new store format and
|
||||
// old values to be deleted, so then it'll never be used again.
|
||||
this.persistState(filteredPersistedState);
|
||||
window.localStorage.removeItem(PERSISTED_STATE_NAME);
|
||||
} else {
|
||||
// new, split-keys implementation
|
||||
for (const subtreeName of Object.keys(this._elements)) {
|
||||
/*
|
||||
* this assumes that the persisted value is stored under the
|
||||
* same key as the feature's redux state name.
|
||||
* We'll need to introduce functions later that can control
|
||||
* the persist key's name. Similar to control serialization
|
||||
* and deserialization.
|
||||
* But that should be a straightforward change.
|
||||
*/
|
||||
const persistedSubtree
|
||||
= this._getPersistedSubtree(
|
||||
subtreeName,
|
||||
this._elements[subtreeName]
|
||||
);
|
||||
|
||||
if (persistedSubtree !== undefined) {
|
||||
filteredPersistedState[subtreeName] = persistedSubtree;
|
||||
}
|
||||
if (persistedSubtree !== undefined) {
|
||||
filteredPersistedState[subtreeName] = persistedSubtree;
|
||||
}
|
||||
}
|
||||
|
||||
// initialize checksum
|
||||
// legacy
|
||||
if (Object.keys(filteredPersistedState).length === 0) {
|
||||
const { localStorage } = window;
|
||||
let persistedState = localStorage.getItem(PERSISTED_STATE_NAME);
|
||||
|
||||
if (persistedState) {
|
||||
try {
|
||||
persistedState = JSON.parse(persistedState);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Error parsing persisted state',
|
||||
persistedState,
|
||||
error);
|
||||
persistedState = {};
|
||||
}
|
||||
|
||||
filteredPersistedState = this._getFilteredState(persistedState);
|
||||
|
||||
// Store into the new format and delete the old format so that
|
||||
// it's not used again.
|
||||
this.persistState(filteredPersistedState);
|
||||
localStorage.removeItem(PERSISTED_STATE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the checksum.
|
||||
this._checksum = this._calculateChecksum(filteredPersistedState);
|
||||
|
||||
this._checksum = this._calculateChecksum(filteredPersistedState);
|
||||
logger.info('redux state rehydrated as', filteredPersistedState);
|
||||
|
||||
return filteredPersistedState;
|
||||
|
@ -112,26 +100,25 @@ class PersistenceRegistry {
|
|||
*/
|
||||
persistState(state: Object) {
|
||||
const filteredState = this._getFilteredState(state);
|
||||
const newCheckSum = this._calculateChecksum(filteredState);
|
||||
const checksum = this._calculateChecksum(filteredState);
|
||||
|
||||
if (newCheckSum !== this._checksum) {
|
||||
if (checksum !== this._checksum) {
|
||||
for (const subtreeName of Object.keys(filteredState)) {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
subtreeName,
|
||||
JSON.stringify(filteredState[subtreeName]));
|
||||
} catch (error) {
|
||||
logger.error('Error persisting redux subtree',
|
||||
logger.error(
|
||||
'Error persisting redux subtree',
|
||||
subtreeName,
|
||||
filteredState[subtreeName],
|
||||
error
|
||||
);
|
||||
error);
|
||||
}
|
||||
}
|
||||
logger.info(
|
||||
`redux state persisted. ${this._checksum} -> ${
|
||||
newCheckSum}`);
|
||||
this._checksum = newCheckSum;
|
||||
`redux state persisted. ${this._checksum} -> ${checksum}`);
|
||||
this._checksum = checksum;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,8 +126,8 @@ class PersistenceRegistry {
|
|||
* Registers a new subtree config to be used for the persistency.
|
||||
*
|
||||
* @param {string} name - The name of the subtree the config belongs to.
|
||||
* @param {ElementConfig} config - The config object, or boolean
|
||||
* if the entire subtree needs to be persisted.
|
||||
* @param {ElementConfig} config - The config {@code Object}, or
|
||||
* {@code boolean} if the entire subtree needs to be persisted.
|
||||
* @returns {void}
|
||||
*/
|
||||
register(name: string, config?: ElementConfig = true) {
|
||||
|
@ -148,32 +135,81 @@ class PersistenceRegistry {
|
|||
}
|
||||
|
||||
/**
|
||||
* Calculates the checksum of the current or the new values of the state.
|
||||
* Calculates the checksum of a specific state.
|
||||
*
|
||||
* @param {Object} state - The redux state to calculate the checksum of.
|
||||
* @private
|
||||
* @param {Object} filteredState - The filtered/persisted redux state.
|
||||
* @returns {string}
|
||||
* @returns {string} The checksum of the specified {@code state}.
|
||||
*/
|
||||
_calculateChecksum(filteredState: Object) {
|
||||
_calculateChecksum(state: Object) {
|
||||
try {
|
||||
return md5.hex(JSON.stringify(filteredState) || '');
|
||||
return md5.hex(JSON.stringify(state) || '');
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Error calculating checksum for state',
|
||||
filteredState,
|
||||
error);
|
||||
logger.error('Error calculating checksum for state', state, error);
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a filtered state from the actual or the persisted redux state,
|
||||
* based on this registry.
|
||||
*
|
||||
* @param {Object} state - The actual or persisted redux state.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getFilteredState(state: Object) {
|
||||
const filteredState = {};
|
||||
|
||||
for (const name of Object.keys(this._elements)) {
|
||||
if (state[name]) {
|
||||
filteredState[name]
|
||||
= this._getFilteredSubtree(
|
||||
state[name],
|
||||
this._elements[name]);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a filtered subtree based on the config for persisting or for
|
||||
* retrieval.
|
||||
*
|
||||
* @param {Object} subtree - The redux state subtree.
|
||||
* @param {ElementConfig} subtreeConfig - The related config.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getFilteredSubtree(subtree, subtreeConfig) {
|
||||
let filteredSubtree;
|
||||
|
||||
if (typeof subtreeConfig === 'object') {
|
||||
// Only a filtered subtree gets persisted as specified by
|
||||
// subtreeConfig.
|
||||
filteredSubtree = {};
|
||||
for (const persistedKey of Object.keys(subtree)) {
|
||||
if (subtreeConfig[persistedKey]) {
|
||||
filteredSubtree[persistedKey] = subtree[persistedKey];
|
||||
}
|
||||
}
|
||||
} else if (subtreeConfig) {
|
||||
// Persist the entire subtree.
|
||||
filteredSubtree = subtree;
|
||||
}
|
||||
|
||||
return filteredSubtree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreives a persisted subtree from the storage.
|
||||
*
|
||||
* @private
|
||||
* @param {string} subtreeName - The name of the subtree.
|
||||
* @param {Object} subtreeConfig - The config of the subtree
|
||||
* from this._elements.
|
||||
* @param {Object} subtreeConfig - The config of the subtree from
|
||||
* {@link #_elements}.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getPersistedSubtree(subtreeName, subtreeConfig) {
|
||||
|
@ -182,6 +218,7 @@ class PersistenceRegistry {
|
|||
if (persistedSubtree) {
|
||||
try {
|
||||
persistedSubtree = JSON.parse(persistedSubtree);
|
||||
|
||||
const filteredSubtree
|
||||
= this._getFilteredSubtree(persistedSubtree, subtreeConfig);
|
||||
|
||||
|
@ -197,58 +234,7 @@ class PersistenceRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a filtered state from the actual or the persisted redux state,
|
||||
* based on this registry.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} state - The actual or persisted redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getFilteredState(state: Object) {
|
||||
const filteredState = {};
|
||||
|
||||
for (const name of Object.keys(this._elements)) {
|
||||
if (state[name]) {
|
||||
filteredState[name] = this._getFilteredSubtree(
|
||||
state[name],
|
||||
this._elements[name]);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a filtered subtree based on the config for persisting or for
|
||||
* retrieval.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} subtree - The redux state subtree.
|
||||
* @param {ElementConfig} subtreeConfig - The related config.
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getFilteredSubtree(subtree, subtreeConfig) {
|
||||
let filteredSubtree;
|
||||
|
||||
if (subtreeConfig === true) {
|
||||
// we persist the entire subtree
|
||||
filteredSubtree = subtree;
|
||||
} else if (typeof subtreeConfig === 'object') {
|
||||
// only a filtered subtree gets persisted, based on the
|
||||
// subtreeConfig object.
|
||||
filteredSubtree = {};
|
||||
for (const persistedKey of Object.keys(subtree)) {
|
||||
if (subtreeConfig[persistedKey]) {
|
||||
filteredSubtree[persistedKey] = subtree[persistedKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredSubtree;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,16 +8,19 @@ import { appNavigate } from '../../app';
|
|||
* The type of the React {@code Component} props of {@link AbstractRecentList}
|
||||
*/
|
||||
type Props = {
|
||||
_defaultURL: string,
|
||||
|
||||
/**
|
||||
* Indicates if the list is disabled or not.
|
||||
*/
|
||||
disabled: boolean,
|
||||
_recentList: Array<Object>,
|
||||
|
||||
/**
|
||||
* The redux store's {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Dispatch<*>
|
||||
dispatch: Dispatch<*>,
|
||||
|
||||
/**
|
||||
* Whether {@code AbstractRecentList} is enabled.
|
||||
*/
|
||||
enabled: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -33,18 +36,20 @@ export default class AbstractRecentList extends Component<Props> {
|
|||
* Joins the selected room.
|
||||
*
|
||||
* @param {string} room - The selected room.
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onJoin(room) {
|
||||
const { disabled, dispatch } = this.props;
|
||||
const { dispatch, enabled } = this.props;
|
||||
|
||||
!disabled && room && dispatch(appNavigate(room));
|
||||
enabled && room && dispatch(appNavigate(room));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bound onPress action for the list item.
|
||||
*
|
||||
* @param {string} room - The selected room.
|
||||
* @protected
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onSelect(room) {
|
||||
|
@ -53,17 +58,18 @@ export default class AbstractRecentList extends Component<Props> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
* Maps (parts of) the redux state into {@code AbstractRecentList}'s React
|
||||
* {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {{
|
||||
* _homeServer: string,
|
||||
* _recentList: Array
|
||||
* _defaultURL: string,
|
||||
* _recentList: Array
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
return {
|
||||
_homeServer: state['features/app'].app._getDefaultURL(),
|
||||
_defaultURL: state['features/app'].app._getDefaultURL(),
|
||||
_recentList: state['features/recent-list']
|
||||
};
|
||||
}
|
||||
|
|
|
@ -47,21 +47,20 @@ class RecentList extends AbstractRecentList {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _recentList, disabled } = this.props;
|
||||
const { enabled, _recentList } = this.props;
|
||||
|
||||
if (!_recentList) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const listViewDataSource
|
||||
= this.dataSource.cloneWithRows(
|
||||
getRecentRooms(_recentList));
|
||||
= this.dataSource.cloneWithRows(getRecentRooms(_recentList));
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
styles.container,
|
||||
disabled ? styles.containerDisabled : null
|
||||
enabled ? null : styles.containerDisabled
|
||||
] }>
|
||||
<ListView
|
||||
dataSource = { listViewDataSource }
|
||||
|
@ -72,19 +71,19 @@ class RecentList extends AbstractRecentList {
|
|||
}
|
||||
|
||||
/**
|
||||
* Assembles the style array of the avatar based on if the conference was a
|
||||
* home or remote server conference (based on current app setting).
|
||||
* Assembles the style array of the avatar based on if the conference was
|
||||
* hosted on the default Jitsi Meet deployment or on a non-default one
|
||||
* (based on current app setting).
|
||||
*
|
||||
* @param {Object} recentListEntry - The recent list entry being rendered.
|
||||
* @private
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
_getAvatarStyle(recentListEntry) {
|
||||
_getAvatarStyle({ baseURL, serverName }) {
|
||||
const avatarStyles = [ styles.avatar ];
|
||||
|
||||
if (recentListEntry.baseURL !== this.props._homeServer) {
|
||||
avatarStyles.push(
|
||||
this._getColorForServerName(recentListEntry.serverName));
|
||||
if (baseURL !== this.props._defaultURL) {
|
||||
avatarStyles.push(this._getColorForServerName(serverName));
|
||||
}
|
||||
|
||||
return avatarStyles;
|
||||
|
@ -115,40 +114,15 @@ class RecentList extends AbstractRecentList {
|
|||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderConfDuration({ conferenceDurationString }) {
|
||||
if (conferenceDurationString) {
|
||||
_renderConfDuration({ durationString }) {
|
||||
if (durationString) {
|
||||
return (
|
||||
<View style = { styles.infoWithIcon } >
|
||||
<Icon
|
||||
name = 'timer'
|
||||
style = { styles.inlineIcon } />
|
||||
<Text style = { styles.confLength }>
|
||||
{ conferenceDurationString }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the server info component based on if the entry was on a
|
||||
* different server or not.
|
||||
*
|
||||
* @param {Object} recentListEntry - The recent list entry being rendered.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderServerInfo(recentListEntry) {
|
||||
if (recentListEntry.baseURL !== this.props._homeServer) {
|
||||
return (
|
||||
<View style = { styles.infoWithIcon } >
|
||||
<Icon
|
||||
name = 'public'
|
||||
style = { styles.inlineIcon } />
|
||||
<Text style = { styles.serverName }>
|
||||
{ recentListEntry.serverName }
|
||||
{ durationString }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
@ -191,17 +165,38 @@ class RecentList extends AbstractRecentList {
|
|||
{ data.dateString }
|
||||
</Text>
|
||||
</View>
|
||||
{
|
||||
this._renderConfDuration(data)
|
||||
}
|
||||
{
|
||||
this._renderServerInfo(data)
|
||||
}
|
||||
{ this._renderConfDuration(data) }
|
||||
{ this._renderServerInfo(data) }
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the server info component based on whether the entry was on a
|
||||
* different server.
|
||||
*
|
||||
* @param {Object} recentListEntry - The recent list entry being rendered.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderServerInfo({ baseURL, serverName }) {
|
||||
if (baseURL !== this.props._defaultURL) {
|
||||
return (
|
||||
<View style = { styles.infoWithIcon } >
|
||||
<Icon
|
||||
name = 'public'
|
||||
style = { styles.inlineIcon } />
|
||||
<Text style = { styles.serverName }>
|
||||
{ serverName }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(RecentList);
|
||||
|
|
|
@ -17,7 +17,6 @@ require('moment/locale/it');
|
|||
require('moment/locale/nb');
|
||||
|
||||
// OC is not available. Please submit OC translation to the MomentJS project.
|
||||
|
||||
require('moment/locale/pl');
|
||||
require('moment/locale/pt');
|
||||
require('moment/locale/pt-br');
|
||||
|
@ -47,22 +46,22 @@ export function getRecentRooms(list: Array<Object>): Array<Object> {
|
|||
const locale = _getSupportedLocale();
|
||||
|
||||
for (const e of list) {
|
||||
const location = parseURIString(e.conference);
|
||||
const uri = parseURIString(e.conference);
|
||||
|
||||
if (uri && uri.room && uri.hostname) {
|
||||
const duration
|
||||
= e.duration || /* legacy */ e.conferenceDuration;
|
||||
|
||||
if (location && location.room && location.hostname) {
|
||||
recentRoomDS.push({
|
||||
baseURL: `${location.protocol}//${location.host}`,
|
||||
baseURL: `${uri.protocol}//${uri.host}`,
|
||||
conference: e.conference,
|
||||
conferenceDuration: e.conferenceDuration,
|
||||
conferenceDurationString:
|
||||
_getDurationString(
|
||||
e.conferenceDuration,
|
||||
locale),
|
||||
dateString: _getDateString(e.date, locale),
|
||||
dateTimeStamp: e.date,
|
||||
initials: _getInitials(location.room),
|
||||
room: location.room,
|
||||
serverName: location.hostname
|
||||
duration,
|
||||
durationString: _getDurationString(duration, locale),
|
||||
initials: _getInitials(uri.room),
|
||||
room: uri.room,
|
||||
serverName: uri.hostname
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +123,7 @@ function _getInitials(room: string) {
|
|||
* or duration ({@code number}).
|
||||
*
|
||||
* @private
|
||||
* @param {Date | number} dateOrDuration - The date or duration to format.
|
||||
* @param {Date|number} dateOrDuration - The date or duration to format.
|
||||
* @param {string} locale - The locale to init the formatter with. Note: The
|
||||
* specified locale must be supported by the formatter so ensure the
|
||||
* prerequisite is met before invoking the function.
|
||||
|
|
|
@ -15,11 +15,11 @@ import { storeCurrentConference, updateConferenceDuration } from './actions';
|
|||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
_updateConferenceDuration(store, next);
|
||||
_updateConferenceDuration(store);
|
||||
break;
|
||||
|
||||
case SET_ROOM:
|
||||
_maybeStoreCurrentConference(store, next, action);
|
||||
_maybeStoreCurrentConference(store, action);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -36,12 +36,11 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeStoreCurrentConference(store, next, action) {
|
||||
const { locationURL } = store.getState()['features/base/connection'];
|
||||
const { room } = action;
|
||||
|
||||
function _maybeStoreCurrentConference({ dispatch, getState }, { room }) {
|
||||
if (room) {
|
||||
next(storeCurrentConference(locationURL));
|
||||
const { locationURL } = getState()['features/base/connection'];
|
||||
|
||||
dispatch(storeCurrentConference(locationURL));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,13 +48,11 @@ function _maybeStoreCurrentConference(store, next, action) {
|
|||
* Updates the duration of the last conference stored in the list.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function.
|
||||
* @param {Action} action - The redux action.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateConferenceDuration(store, next) {
|
||||
const { locationURL } = store.getState()['features/base/connection'];
|
||||
function _updateConferenceDuration({ dispatch, getState }) {
|
||||
const { locationURL } = getState()['features/base/connection'];
|
||||
|
||||
next(updateConferenceDuration(locationURL));
|
||||
dispatch(updateConferenceDuration(locationURL));
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import { APP_WILL_MOUNT } from '../app';
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
import { PersistenceRegistry } from '../base/storage';
|
||||
|
||||
|
@ -10,6 +11,13 @@ import {
|
|||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* The default/initial redux state of the feature {@code recent-list}.
|
||||
*
|
||||
* @type {Array<Object>}
|
||||
*/
|
||||
const DEFAULT_STATE = [];
|
||||
|
||||
/**
|
||||
* The name of the {@code window.localStorage} item where recent rooms are
|
||||
* stored.
|
||||
|
@ -31,17 +39,20 @@ export const MAX_LIST_SIZE = 30;
|
|||
const STORE_NAME = 'features/recent-list';
|
||||
|
||||
/**
|
||||
* Sets up the persistence of the feature recent-list.
|
||||
* Sets up the persistence of the feature {@code recent-list}.
|
||||
*/
|
||||
PersistenceRegistry.register(STORE_NAME);
|
||||
|
||||
/**
|
||||
* Reduces the redux actions of the feature recent-list.
|
||||
* Reduces redux actions for the purposes of the feature {@code recent-list}.
|
||||
*/
|
||||
ReducerRegistry.register(
|
||||
STORE_NAME,
|
||||
(state = _getLegacyRecentRoomList(), action) => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
return _appWillMount(state);
|
||||
|
||||
case STORE_CURRENT_CONFERENCE:
|
||||
return _storeCurrentConference(state, action);
|
||||
|
||||
|
@ -53,18 +64,48 @@ ReducerRegistry.register(
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Reduces the redux action {@link APP_WILL_MOUNT}.
|
||||
*
|
||||
* @param {Object} state - The redux state of the feature {@code recent-list}.
|
||||
* @param {Action} action - The redux action {@code APP_WILL_MOUNT}.
|
||||
* @returns {Array<Object>} The next redux state of the feature
|
||||
* {@code recent-list}.
|
||||
*/
|
||||
function _appWillMount(state) {
|
||||
// XXX APP_WILL_MOUNT is the earliest redux action of ours dispatched in the
|
||||
// store. For the purposes of legacy support, make sure that the
|
||||
// deserialized recent-list's state is in the format deemed current by the
|
||||
// current app revision.
|
||||
if (state && typeof state === 'object') {
|
||||
if (Array.isArray(state)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// In an enterprise/internal build of Jitsi Meet for Android and iOS we
|
||||
// had recent-list's state as an object with property list.
|
||||
const { list } = state;
|
||||
|
||||
if (Array.isArray(list) && list.length) {
|
||||
return list.slice();
|
||||
}
|
||||
}
|
||||
|
||||
// In the weird case that we have previously persisted/serialized null.
|
||||
return DEFAULT_STATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the recent room list that was stored using the legacy way.
|
||||
*
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function _getLegacyRecentRoomList(): Array<Object> {
|
||||
function _getLegacyRecentRoomList(): Array<Object> {
|
||||
try {
|
||||
const list
|
||||
= JSON.parse(window.localStorage.getItem(LEGACY_STORAGE_KEY));
|
||||
const str = window.localStorage.getItem(LEGACY_STORAGE_KEY);
|
||||
|
||||
if (list && list.length) {
|
||||
return list;
|
||||
if (str) {
|
||||
return JSON.parse(str);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Failed to parse legacy recent-room list!');
|
||||
|
@ -74,61 +115,79 @@ export function _getLegacyRecentRoomList(): Array<Object> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds a new list entry to the redux store.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} action - The redux action.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _storeCurrentConference(state, action) {
|
||||
const { locationURL } = action;
|
||||
* Adds a new list entry to the redux store.
|
||||
*
|
||||
* @param {Object} state - The redux state of the feature {@code recent-list}.
|
||||
* @param {Object} action - The redux action.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _storeCurrentConference(state, { locationURL }) {
|
||||
const conference = locationURL.href;
|
||||
|
||||
// If the current conference is already in the list, we remove it to re-add
|
||||
// it to the top.
|
||||
const list = (Array.isArray(state) ? state : [])
|
||||
.filter(e => e.conference !== conference);
|
||||
const nextState
|
||||
= state.filter(e => !_urlStringEquals(e.conference, conference));
|
||||
|
||||
// The list is a reverse-sorted (i.e. the newer elements are at the end).
|
||||
list.push({
|
||||
nextState.push({
|
||||
conference,
|
||||
conferenceDuration: 0, // We don't have this data yet!
|
||||
date: Date.now()
|
||||
date: Date.now(),
|
||||
duration: 0 // We don't have the duration yet!
|
||||
});
|
||||
|
||||
// Ensure the list doesn't exceed a/the maximum size.
|
||||
list.splice(0, list.length - MAX_LIST_SIZE);
|
||||
nextState.splice(0, nextState.length - MAX_LIST_SIZE);
|
||||
|
||||
return list;
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the conference length when left.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} state - The redux state of the feature {@code recent-list}.
|
||||
* @param {Object} action - The redux action.
|
||||
* @returns {Object}
|
||||
* @returns {Object} The next redux state of the feature {@code recent-list}.
|
||||
*/
|
||||
function _updateConferenceDuration(state, action) {
|
||||
const { locationURL } = action;
|
||||
function _updateConferenceDuration(state, { locationURL }) {
|
||||
if (locationURL && locationURL.href && state.length) {
|
||||
const mostRecentIndex = state.length - 1;
|
||||
const mostRecent = state[mostRecentIndex];
|
||||
|
||||
if (locationURL && locationURL.href) {
|
||||
// shallow copy to avoid in-place modification.
|
||||
const list = (Array.isArray(state) ? state : []).slice();
|
||||
if (_urlStringEquals(mostRecent.conference, locationURL.href)) {
|
||||
// The last conference start was stored so we need to update the
|
||||
// length.
|
||||
const nextMostRecent = {
|
||||
...mostRecent,
|
||||
duration: Date.now() - mostRecent.date
|
||||
};
|
||||
|
||||
if (list.length > 0) {
|
||||
const mostRecentURL = list[list.length - 1];
|
||||
delete nextMostRecent.conferenceDuration; // legacy
|
||||
|
||||
if (mostRecentURL.conference === locationURL.href) {
|
||||
// The last conference start was stored so we need to update the
|
||||
// length.
|
||||
mostRecentURL.conferenceDuration
|
||||
= Date.now() - mostRecentURL.date;
|
||||
// Shallow copy to avoid in-place modification.
|
||||
const nextState = state.slice();
|
||||
|
||||
return list;
|
||||
}
|
||||
nextState[mostRecentIndex] = nextMostRecent;
|
||||
|
||||
return nextState;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether two specific URL {@code strings} are equal in the sense
|
||||
* that they identify one and the same conference resource (irrespective of
|
||||
* time) for the purposes of the feature {@code recent-list}.
|
||||
*
|
||||
* @param {string} a - The URL {@code string} to test for equality to {@code b}.
|
||||
* @param {string} b - The URL {@code string} to test for equality to {@code a}.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function _urlStringEquals(a: string, b: string) {
|
||||
// FIXME Case-sensitive comparison is wrong because the room name at least
|
||||
// is case insensitive on the server and elsewhere (where it matters) in the
|
||||
// client. I don't think domain names are case-sensitive either.
|
||||
return a === b;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { Component } from 'react';
|
||||
|
||||
import { getProfile, updateProfile } from '../../base/profile';
|
||||
import { updateProfile } from '../../base/profile';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
|
@ -12,16 +12,22 @@ type Props = {
|
|||
|
||||
/**
|
||||
* The current profile object.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
_profile: Object,
|
||||
|
||||
/**
|
||||
* The default URL for when there is no custom URL set in the profile.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
_serverURL: string,
|
||||
|
||||
/**
|
||||
* Whether {@link AbstractSettingsView} is visible.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
_visible: boolean,
|
||||
|
||||
|
@ -168,7 +174,7 @@ export class AbstractSettingsView extends Component<Props> {
|
|||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
return {
|
||||
_profile: getProfile(state),
|
||||
_profile: state['features/base/profile'],
|
||||
_serverURL: state['features/app'].app._getDefaultURL(),
|
||||
_visible: state['features/settings'].visible
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { Component } from 'react';
|
||||
|
||||
import { createWelcomePageEvent, sendAnalytics } from '../../analytics';
|
||||
|
@ -14,11 +13,6 @@ import { generateRoomWithoutSeparator } from '../functions';
|
|||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Boolean to indicate if the room field is focused or not.
|
||||
*/
|
||||
_fieldFocused: boolean,
|
||||
|
||||
/**
|
||||
* The user's profile.
|
||||
*/
|
||||
|
@ -32,17 +26,7 @@ type Props = {
|
|||
*
|
||||
* @abstract
|
||||
*/
|
||||
export class AbstractWelcomePage extends Component<*, *> {
|
||||
/**
|
||||
* {@code AbstractWelcomePage}'s React {@code Component} prop types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
_room: PropTypes.string,
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
export class AbstractWelcomePage extends Component<Props, *> {
|
||||
_mounted: ?boolean;
|
||||
|
||||
/**
|
||||
|
@ -245,12 +229,13 @@ export class AbstractWelcomePage extends Component<*, *> {
|
|||
* @param {Object} state - The redux state.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* _profile: Object,
|
||||
* _room: string
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
return {
|
||||
_profile: state['features/base/profile'].profile,
|
||||
_profile: state['features/base/profile'],
|
||||
_room: state['features/base/conference'].room
|
||||
};
|
||||
}
|
||||
|
|
|
@ -40,13 +40,6 @@ import WelcomePageSideBar from './WelcomePageSideBar';
|
|||
* @extends AbstractWelcomePage
|
||||
*/
|
||||
class WelcomePage extends AbstractWelcomePage {
|
||||
/**
|
||||
* WelcomePage component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = AbstractWelcomePage.propTypes;
|
||||
|
||||
/**
|
||||
* Constructor of the Component.
|
||||
*
|
||||
|
@ -140,7 +133,7 @@ class WelcomePage extends AbstractWelcomePage {
|
|||
{
|
||||
this._renderHintBox()
|
||||
}
|
||||
<RecentList disabled = { this.state._fieldFocused } />
|
||||
<RecentList enabled = { !this.state._fieldFocused } />
|
||||
</SafeAreaView>
|
||||
<SettingsView />
|
||||
</View>
|
||||
|
|
Loading…
Reference in New Issue