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