This commit is contained in:
34
react/features/recent-list/actionTypes.ts
Normal file
34
react/features/recent-list/actionTypes.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Action type to signal the deletion of a list entry.
|
||||
*
|
||||
* {
|
||||
* type: DELETE_RECENT_LIST_ENTRY,
|
||||
* entryId: Object
|
||||
* }
|
||||
*/
|
||||
export const DELETE_RECENT_LIST_ENTRY = 'DELETE_RECENT_LIST_ENTRY';
|
||||
|
||||
/**
|
||||
* Action type to signal a new addition to the list.
|
||||
*
|
||||
* {
|
||||
* type: _STORE_CURRENT_CONFERENCE,
|
||||
* locationURL: Object
|
||||
* }
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export const _STORE_CURRENT_CONFERENCE = '_STORE_CURRENT_CONFERENCE';
|
||||
|
||||
/**
|
||||
* Action type to signal that a new conference duration info is available.
|
||||
*
|
||||
* {
|
||||
* type: _UPDATE_CONFERENCE_DURATION,
|
||||
* locationURL: Object
|
||||
* }
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export const _UPDATE_CONFERENCE_DURATION
|
||||
= '_UPDATE_CONFERENCE_DURATION';
|
||||
56
react/features/recent-list/actions.ts
Normal file
56
react/features/recent-list/actions.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
DELETE_RECENT_LIST_ENTRY,
|
||||
_STORE_CURRENT_CONFERENCE,
|
||||
_UPDATE_CONFERENCE_DURATION
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Deletes a recent list entry based on url and date.
|
||||
*
|
||||
* @param {Object} entryId - An object constructed of the url and the date of
|
||||
* the entry for easy identification.
|
||||
* @returns {{
|
||||
* type: DELETE_RECENT_LIST_ENTRY,
|
||||
* entryId: Object
|
||||
* }}
|
||||
*/
|
||||
export function deleteRecentListEntry(entryId: Object) {
|
||||
return {
|
||||
type: DELETE_RECENT_LIST_ENTRY,
|
||||
entryId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to initiate a new addition to the list.
|
||||
*
|
||||
* @param {Object} locationURL - The current location URL.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* type: _STORE_CURRENT_CONFERENCE,
|
||||
* locationURL: Object
|
||||
* }}
|
||||
*/
|
||||
export function _storeCurrentConference(locationURL: Object) {
|
||||
return {
|
||||
type: _STORE_CURRENT_CONFERENCE,
|
||||
locationURL
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to initiate the update of the duration of the last conference.
|
||||
*
|
||||
* @param {Object} locationURL - The current location URL.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* type: _UPDATE_CONFERENCE_DURATION,
|
||||
* locationURL: Object
|
||||
* }}
|
||||
*/
|
||||
export function _updateConferenceDuration(locationURL: Object) {
|
||||
return {
|
||||
type: _UPDATE_CONFERENCE_DURATION,
|
||||
locationURL
|
||||
};
|
||||
}
|
||||
97
react/features/recent-list/components/AbstractRecentList.tsx
Normal file
97
react/features/recent-list/components/AbstractRecentList.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
|
||||
import { createRecentClickedEvent, createRecentSelectedEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { appNavigate } from '../../app/actions';
|
||||
import { IStore } from '../../app/types';
|
||||
import AbstractPage from '../../base/react/components/AbstractPage';
|
||||
import { Container, Text } from '../../base/react/components/index';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AbstractRecentList}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The redux store's {@code dispatch} function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract component for the recent list.
|
||||
*
|
||||
*/
|
||||
export default class AbstractRecentList<P extends IProps> extends AbstractPage<P> {
|
||||
/**
|
||||
* Initializes a new {@code RecentList} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
this._onPress = this._onPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
||||
* immediately after this component is mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
sendAnalytics(createRecentSelectedEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list empty component if a custom one has to be rendered instead
|
||||
* of the default one in the {@link NavigateSectionList}.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Component}
|
||||
*/
|
||||
_getRenderListEmptyComponent() {
|
||||
const { t } = this.props;
|
||||
const descriptionId = 'meetings-list-empty-description';
|
||||
|
||||
return (
|
||||
<Container
|
||||
aria-describedby = { descriptionId }
|
||||
aria-label = { t('welcomepage.recentList') }
|
||||
className = 'meetings-list-empty'
|
||||
role = 'region'
|
||||
style = { styles.emptyListContainer as any }>
|
||||
<Text // @ts-ignore
|
||||
className = 'description'
|
||||
id = { descriptionId }
|
||||
style = { styles.emptyListText as any }>
|
||||
{ t('welcomepage.recentListEmpty') }
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the list's navigate action.
|
||||
*
|
||||
* @private
|
||||
* @param {string} url - The url string to navigate to.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPress(url: string) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createRecentClickedEvent('meeting.tile'));
|
||||
|
||||
dispatch(appNavigate(url));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { IconTrash } from '../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
import { deleteRecentListEntry } from '../actions';
|
||||
|
||||
export interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* The ID of the entry to be deleted.
|
||||
*/
|
||||
itemId: Object;
|
||||
}
|
||||
|
||||
/**
|
||||
* A recent list menu button which deletes the selected entry.
|
||||
*/
|
||||
class DeleteItemButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'welcomepage.recentListDelete';
|
||||
override icon = IconTrash;
|
||||
override label = 'welcomepage.recentListDelete';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch, itemId } = this.props;
|
||||
|
||||
dispatch(deleteRecentListEntry(itemId));
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(DeleteItemButton));
|
||||
128
react/features/recent-list/components/RecentList.native.tsx
Normal file
128
react/features/recent-list/components/RecentList.native.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { GestureResponderEvent, TouchableWithoutFeedback, View, ViewStyle } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getDefaultURL } from '../../app/functions.native';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { openSheet } from '../../base/dialog/actions';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import NavigateSectionList from '../../base/react/components/native/NavigateSectionList';
|
||||
import { Item, Section } from '../../base/react/types';
|
||||
import styles from '../../welcome/components/styles';
|
||||
import { isRecentListEnabled, toDisplayableList } from '../functions.native';
|
||||
|
||||
import AbstractRecentList from './AbstractRecentList';
|
||||
import RecentListItemMenu from './RecentListItemMenu.native';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link RecentList}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The default server URL.
|
||||
*/
|
||||
_defaultServerURL: string;
|
||||
|
||||
/**
|
||||
* The recent list from the Redux store.
|
||||
*/
|
||||
_recentList: Array<Section>;
|
||||
|
||||
/**
|
||||
* Renders the list disabled.
|
||||
*/
|
||||
disabled: boolean;
|
||||
|
||||
/**
|
||||
* The redux store's {@code dispatch} function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Callback to be invoked when pressing the list container.
|
||||
*/
|
||||
onListContainerPress?: (e?: GestureResponderEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that renders the list of the recently joined rooms.
|
||||
*
|
||||
*/
|
||||
class RecentList extends AbstractRecentList<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code RecentList} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onLongPress = this._onLongPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the React Components's render method.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
if (!isRecentListEnabled()) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
disabled,
|
||||
onListContainerPress,
|
||||
t,
|
||||
_defaultServerURL,
|
||||
_recentList
|
||||
} = this.props; // @ts-ignore
|
||||
const recentList = toDisplayableList(_recentList, t, _defaultServerURL);
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress = { onListContainerPress }>
|
||||
<View style = { (disabled ? styles.recentListDisabled : styles.recentList) as ViewStyle }>
|
||||
<NavigateSectionList
|
||||
disabled = { disabled }
|
||||
onLongPress = { this._onLongPress }
|
||||
onPress = { this._onPress }
|
||||
renderListEmptyComponent
|
||||
= { this._getRenderListEmptyComponent() }
|
||||
|
||||
// @ts-ignore
|
||||
sections = { recentList } />
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the list's navigate action.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} item - The item which was long pressed.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLongPress(item: Item) {
|
||||
this.props.dispatch(openSheet(RecentListItemMenu, { item }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_defaultServerURL: getDefaultURL(state),
|
||||
_recentList: state['features/recent-list']
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export default translate(connect(_mapStateToProps)(RecentList));
|
||||
105
react/features/recent-list/components/RecentList.web.tsx
Normal file
105
react/features/recent-list/components/RecentList.web.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import MeetingsList from '../../base/react/components/web/MeetingsList';
|
||||
import { deleteRecentListEntry } from '../actions';
|
||||
import { isRecentListEnabled, toDisplayableList } from '../functions.web';
|
||||
|
||||
import AbstractRecentList from './AbstractRecentList';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link RecentList}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The recent list from the Redux store.
|
||||
*/
|
||||
_recentList: Array<any>;
|
||||
|
||||
/**
|
||||
* Renders the list disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* The redux store's {@code dispatch} function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* The cross platform container rendering the list of the recently joined rooms.
|
||||
*
|
||||
*/
|
||||
class RecentList extends AbstractRecentList<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code RecentList} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._getRenderListEmptyComponent
|
||||
= this._getRenderListEmptyComponent.bind(this);
|
||||
this._onPress = this._onPress.bind(this);
|
||||
this._onItemDelete = this._onItemDelete.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a recent entry.
|
||||
*
|
||||
* @param {Object} entry - The entry to be deleted.
|
||||
* @inheritdoc
|
||||
*/
|
||||
_onItemDelete(entry: Object) {
|
||||
this.props.dispatch(deleteRecentListEntry(entry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the React Components's render method.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
if (!isRecentListEnabled()) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
disabled,
|
||||
_recentList
|
||||
} = this.props;
|
||||
const recentList = toDisplayableList(_recentList);
|
||||
|
||||
return (
|
||||
<MeetingsList
|
||||
disabled = { Boolean(disabled) }
|
||||
hideURL = { true }
|
||||
listEmptyComponent = { this._getRenderListEmptyComponent() }
|
||||
meetings = { recentList }
|
||||
onItemDelete = { this._onItemDelete }
|
||||
onPress = { this._onPress } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {{
|
||||
* _defaultServerURL: string,
|
||||
* _recentList: Array
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_recentList: state['features/recent-list']
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(RecentList));
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { hideSheet } from '../../base/dialog/actions';
|
||||
import BottomSheet from '../../base/dialog/components/native/BottomSheet';
|
||||
import { bottomSheetStyles } from '../../base/dialog/components/native/styles';
|
||||
import { Item } from '../../base/react/types';
|
||||
|
||||
import DeleteItemButton from './DeleteItemButton.native';
|
||||
import ShowDialInInfoButton from './ShowDialInInfoButton.native';
|
||||
import styles from './styles.native';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Item being rendered in this menu.
|
||||
*/
|
||||
item: Item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to implement a popup menu that opens upon long pressing a recent list item.
|
||||
*/
|
||||
class RecentListItemMenu extends PureComponent<IProps> {
|
||||
/**
|
||||
* Constructor of the component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._renderMenuHeader = this._renderMenuHeader.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { item } = this.props;
|
||||
const buttonProps = {
|
||||
afterClick: this._onCancel,
|
||||
itemId: item.id,
|
||||
showLabel: true,
|
||||
styles: bottomSheetStyles.buttons
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
renderHeader = { this._renderMenuHeader }>
|
||||
<DeleteItemButton { ...buttonProps } />
|
||||
<ShowDialInInfoButton { ...buttonProps } />
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to hide this menu.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onCancel() {
|
||||
this.props.dispatch(hideSheet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to render the menu's header.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderMenuHeader() {
|
||||
const { item } = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
bottomSheetStyles.sheet,
|
||||
styles.entryNameContainer as ViewStyle
|
||||
] }>
|
||||
<Text
|
||||
ellipsizeMode = { 'middle' }
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.entryNameLabel as TextStyle }>
|
||||
{ item.title }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(RecentListItemMenu);
|
||||
@@ -0,0 +1,40 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { IconInfoCircle } from '../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../../mobile/navigation/routes';
|
||||
|
||||
export interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* The ID of the entry to be deleted.
|
||||
*/
|
||||
itemId: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* A recent list menu button which opens the dial-in info dialog.
|
||||
*/
|
||||
class ShowDialInInfoButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'welcomepage.info';
|
||||
override icon = IconInfoCircle;
|
||||
override label = 'welcomepage.info';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { itemId } = this.props;
|
||||
|
||||
navigateRoot(screen.dialInSummary, {
|
||||
summaryUrl: itemId.url
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(ShowDialInInfoButton));
|
||||
45
react/features/recent-list/components/styles.native.ts
Normal file
45
react/features/recent-list/components/styles.native.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ColorPalette } from '../../base/styles/components/styles/ColorPalette';
|
||||
import { createStyleSheet } from '../../base/styles/functions.native';
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Component}s of the feature recent-list i.e.
|
||||
* {@code CalendarList}.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
|
||||
/**
|
||||
* Text style of the empty recent list message.
|
||||
*/
|
||||
emptyListText: {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* The style of the empty recent list container.
|
||||
*/
|
||||
emptyListContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20
|
||||
},
|
||||
|
||||
entryNameContainer: {
|
||||
alignItems: 'center',
|
||||
borderBottomColor: ColorPalette.lightGrey,
|
||||
borderBottomWidth: 1,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
height: 48
|
||||
},
|
||||
|
||||
entryNameLabel: {
|
||||
color: ColorPalette.lightGrey,
|
||||
flexShrink: 1,
|
||||
fontSize: 16,
|
||||
opacity: 0.90
|
||||
}
|
||||
});
|
||||
4
react/features/recent-list/components/styles.web.ts
Normal file
4
react/features/recent-list/components/styles.web.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
emptyListContainer: {},
|
||||
emptyListText: {}
|
||||
};
|
||||
157
react/features/recent-list/functions.native.ts
Normal file
157
react/features/recent-list/functions.native.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
getLocalizedDateFormatter,
|
||||
getLocalizedDurationFormatter
|
||||
} from '../base/i18n/dateUtil';
|
||||
import NavigateSectionList from '../base/react/components/native/NavigateSectionList';
|
||||
import { parseURIString, safeDecodeURIComponent } from '../base/util/uri';
|
||||
|
||||
import { IRecentItem } from './types';
|
||||
|
||||
/**
|
||||
* Creates a displayable list item of a recent list entry.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} item - The recent list entry.
|
||||
* @param {string} defaultServerURL - The default server URL.
|
||||
* @param {Function} t - The translate function.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function toDisplayableItem(item: IRecentItem,
|
||||
defaultServerURL: string, t: Function) {
|
||||
const location = parseURIString(item.conference);
|
||||
const baseURL = `${location.protocol}//${location.host}`;
|
||||
const serverName = baseURL === defaultServerURL ? null : location.host;
|
||||
|
||||
return {
|
||||
colorBase: serverName,
|
||||
id: {
|
||||
date: item.date,
|
||||
url: item.conference
|
||||
},
|
||||
key: `key-${item.conference}-${item.date}`,
|
||||
lines: [
|
||||
_toDateString(item.date, t),
|
||||
_toDurationString(item.duration),
|
||||
serverName
|
||||
],
|
||||
title: safeDecodeURIComponent(location.room),
|
||||
url: item.conference
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a duration string for the item.
|
||||
*
|
||||
* @private
|
||||
* @param {number} duration - The item's duration.
|
||||
* @returns {string}
|
||||
*/
|
||||
function _toDurationString(duration: number) {
|
||||
if (duration) {
|
||||
return getLocalizedDurationFormatter(duration);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a date string for the item.
|
||||
*
|
||||
* @private
|
||||
* @param {number} itemDate - The item's timestamp.
|
||||
* @param {Function} t - The translate function.
|
||||
* @returns {string}
|
||||
*/
|
||||
function _toDateString(itemDate: number, t: Function) {
|
||||
const m = getLocalizedDateFormatter(itemDate);
|
||||
const date = new Date(itemDate);
|
||||
const dateInMs = date.getTime();
|
||||
const now = new Date();
|
||||
const todayInMs = new Date().setHours(0, 0, 0, 0);
|
||||
const yesterday = new Date();
|
||||
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
yesterday.setHours(0, 0, 0, 0);
|
||||
const yesterdayInMs = yesterday.getTime();
|
||||
|
||||
if (dateInMs >= todayInMs) {
|
||||
return m.fromNow();
|
||||
} else if (dateInMs >= yesterdayInMs) {
|
||||
return `${t('dateUtils.yesterday')}, ${m.format('h:mm A')}`;
|
||||
} else if (date.getFullYear() !== now.getFullYear()) {
|
||||
// We only want to include the year in the date if its not the current
|
||||
// year.
|
||||
return m.format('ddd, MMMM DD h:mm A, gggg');
|
||||
}
|
||||
|
||||
return m.format('ddd, MMMM DD h:mm A');
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the history list to a displayable list
|
||||
* with sections.
|
||||
*
|
||||
* @private
|
||||
* @param {Array<Object>} recentList - The recent list form the redux store.
|
||||
* @param {Function} t - The translate function.
|
||||
* @param {string} defaultServerURL - The default server URL.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function toDisplayableList(recentList: IRecentItem[],
|
||||
t: Function, defaultServerURL: string) {
|
||||
const { createSection } = NavigateSectionList;
|
||||
const todaySection = createSection(t('dateUtils.today'), 'today');
|
||||
const yesterdaySection
|
||||
= createSection(t('dateUtils.yesterday'), 'yesterday');
|
||||
const earlierSection
|
||||
= createSection(t('dateUtils.earlier'), 'earlier');
|
||||
const today = new Date();
|
||||
const todayString = today.toDateString();
|
||||
const yesterday = new Date();
|
||||
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterdayString = yesterday.toDateString();
|
||||
|
||||
for (const item of recentList) {
|
||||
const itemDateString = new Date(item.date).toDateString();
|
||||
const displayableItem = toDisplayableItem(item, defaultServerURL, t);
|
||||
|
||||
if (itemDateString === todayString) {
|
||||
todaySection.data.push(displayableItem);
|
||||
} else if (itemDateString === yesterdayString) {
|
||||
yesterdaySection.data.push(displayableItem);
|
||||
} else {
|
||||
earlierSection.data.push(displayableItem);
|
||||
}
|
||||
}
|
||||
const displayableList = [];
|
||||
|
||||
// the recent list in the redux store has the latest date in the last index
|
||||
// therefore all the sectionLists' data that was created by parsing through
|
||||
// the recent list is in reverse order and must be reversed for the most
|
||||
// item to show first
|
||||
if (todaySection.data.length) {
|
||||
todaySection.data.reverse();
|
||||
displayableList.push(todaySection);
|
||||
}
|
||||
if (yesterdaySection.data.length) {
|
||||
yesterdaySection.data.reverse();
|
||||
displayableList.push(yesterdaySection);
|
||||
}
|
||||
if (earlierSection.data.length) {
|
||||
earlierSection.data.reverse();
|
||||
displayableList.push(earlierSection);
|
||||
}
|
||||
|
||||
return displayableList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <tt>true</tt> if recent list is enabled and <tt>false</tt> otherwise.
|
||||
*
|
||||
* @returns {boolean} <tt>true</tt> if recent list is enabled and <tt>false</tt>
|
||||
* otherwise.
|
||||
*/
|
||||
export function isRecentListEnabled() {
|
||||
return true;
|
||||
}
|
||||
33
react/features/recent-list/functions.web.ts
Normal file
33
react/features/recent-list/functions.web.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { parseURIString, safeDecodeURIComponent } from '../base/util/uri';
|
||||
|
||||
|
||||
/**
|
||||
* Transforms the history list to a displayable list.
|
||||
*
|
||||
* @private
|
||||
* @param {Array<Object>} recentList - The recent list form the redux store.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function toDisplayableList(recentList: Array<{ conference: string; date: Date; duration: number; }>) {
|
||||
return (
|
||||
[ ...recentList ].reverse()
|
||||
.map(item => {
|
||||
return {
|
||||
date: item.date,
|
||||
duration: item.duration,
|
||||
time: [ item.date ],
|
||||
title: safeDecodeURIComponent(parseURIString(item.conference).room),
|
||||
url: item.conference
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <tt>true</tt> if recent list is enabled and <tt>false</tt> otherwise.
|
||||
*
|
||||
* @returns {boolean} <tt>true</tt> if recent list is enabled and <tt>false</tt>
|
||||
* otherwise.
|
||||
*/
|
||||
export function isRecentListEnabled() {
|
||||
return interfaceConfig.RECENT_LIST_ENABLED;
|
||||
}
|
||||
3
react/features/recent-list/logger.ts
Normal file
3
react/features/recent-list/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/recent-list');
|
||||
150
react/features/recent-list/middleware.ts
Normal file
150
react/features/recent-list/middleware.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { APP_WILL_MOUNT } from '../base/app/actionTypes';
|
||||
import { CONFERENCE_WILL_LEAVE, SET_ROOM } from '../base/conference/actionTypes';
|
||||
import { JITSI_CONFERENCE_URL_KEY } from '../base/conference/constants';
|
||||
import { addKnownDomains } from '../base/known-domains/actions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { isEmbedded } from '../base/util/embedUtils';
|
||||
import { parseURIString } from '../base/util/uri';
|
||||
|
||||
import { _storeCurrentConference, _updateConferenceDuration } from './actions';
|
||||
import { isRecentListEnabled } from './functions';
|
||||
|
||||
/**
|
||||
* Middleware that captures joined rooms so they can be saved into
|
||||
* {@code window.localStorage}.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
if (isRecentListEnabled()) {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
return _appWillMount(store, next, action);
|
||||
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
return _conferenceWillLeave(store, next, action);
|
||||
|
||||
case SET_ROOM:
|
||||
return _setRoom(store, next, action);
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Notifies the feature recent-list that the redux action {@link APP_WILL_MOUNT}
|
||||
* is being dispatched in a specific redux store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified action is being
|
||||
* dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified action to the specified store.
|
||||
* @param {Action} action - The redux action {@code APP_WILL_MOUNT} which is
|
||||
* being dispatched in the specified redux store.
|
||||
* @private
|
||||
* @returns {*} The result returned by {@code next(action)}.
|
||||
*/
|
||||
function _appWillMount({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
|
||||
// It's an opportune time to transfer the feature recent-list's knowledge
|
||||
// about "known domains" (which is local to the feature) to the feature
|
||||
// base/known-domains (which is global to the app).
|
||||
//
|
||||
// XXX Since the feature recent-list predates the feature calendar-sync and,
|
||||
// consequently, the feature known-domains, it's possible for the feature
|
||||
// known-list to know of domains which the feature known-domains is yet to
|
||||
// discover.
|
||||
const knownDomains = [];
|
||||
|
||||
for (const { conference } of getState()['features/recent-list']) {
|
||||
const uri = parseURIString(conference);
|
||||
let host;
|
||||
|
||||
uri && (host = uri.host) && knownDomains.push(host);
|
||||
}
|
||||
knownDomains.length && dispatch(addKnownDomains(knownDomains));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {@link CONFERENCE_WILL_LEAVE}.
|
||||
* @private
|
||||
* @returns {*} The result returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceWillLeave({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const state = getState();
|
||||
const { doNotStoreRoom } = state['features/base/config'];
|
||||
|
||||
if (!doNotStoreRoom && !isEmbedded()) {
|
||||
let locationURL;
|
||||
|
||||
/**
|
||||
* FIXME:
|
||||
* It is better to use action.conference[JITSI_CONFERENCE_URL_KEY]
|
||||
* in order to make sure we get the url the conference is leaving
|
||||
* from (i.e. The room we are leaving from) because if the order of events
|
||||
* is different, we cannot be guaranteed that the location URL in base
|
||||
* connection is the url we are leaving from... Not the one we are going to
|
||||
* (the latter happens on mobile -- if we use the web implementation);
|
||||
* however, the conference object on web does not have
|
||||
* JITSI_CONFERENCE_URL_KEY so we cannot call it and must use the other way.
|
||||
*/
|
||||
if (typeof APP === 'undefined') {
|
||||
const { conference } = action;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
||||
locationURL = conference && conference[JITSI_CONFERENCE_URL_KEY];
|
||||
} else {
|
||||
locationURL = state['features/base/connection'].locationURL;
|
||||
}
|
||||
dispatch(
|
||||
_updateConferenceDuration(
|
||||
locationURL
|
||||
));
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is a current conference (upon SET_ROOM action), and saves it
|
||||
* if necessary.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function.
|
||||
* @param {Action} action - The redux action {@link SET_ROOM}.
|
||||
* @private
|
||||
* @returns {*} The result returned by {@code next(action)}.
|
||||
*/
|
||||
function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const { doNotStoreRoom } = getState()['features/base/config'];
|
||||
|
||||
if (!doNotStoreRoom && !isEmbedded() && action.room) {
|
||||
const { locationURL } = getState()['features/base/connection'];
|
||||
|
||||
if (locationURL) {
|
||||
dispatch(_storeCurrentConference(locationURL));
|
||||
|
||||
// Whatever domain the feature recent-list knows about, the app as a
|
||||
// whole should know about.
|
||||
//
|
||||
// XXX Technically, _storeCurrentConference could be turned into an
|
||||
// asynchronous action creator which dispatches both
|
||||
// _STORE_CURRENT_CONFERENCE and addKnownDomains but...
|
||||
dispatch(addKnownDomains(locationURL.host));
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
151
react/features/recent-list/reducer.ts
Normal file
151
react/features/recent-list/reducer.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { getURLWithoutParamsNormalized } from '../base/connection/utils';
|
||||
import PersistenceRegistry from '../base/redux/PersistenceRegistry';
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
DELETE_RECENT_LIST_ENTRY,
|
||||
_STORE_CURRENT_CONFERENCE,
|
||||
_UPDATE_CONFERENCE_DURATION
|
||||
} from './actionTypes';
|
||||
import { isRecentListEnabled } from './functions';
|
||||
|
||||
interface IRecent {
|
||||
conference: string;
|
||||
date: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export type IRecentListState = IRecent[];
|
||||
|
||||
/**
|
||||
* The default/initial redux state of the feature {@code recent-list}.
|
||||
*
|
||||
* @type {IRecentListState}
|
||||
*/
|
||||
const DEFAULT_STATE: IRecentListState = [];
|
||||
|
||||
/**
|
||||
* The max size of the list.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const MAX_LIST_SIZE = 30;
|
||||
|
||||
/**
|
||||
* The redux subtree of this feature.
|
||||
*/
|
||||
const STORE_NAME = 'features/recent-list';
|
||||
|
||||
/**
|
||||
* Sets up the persistence of the feature {@code recent-list}.
|
||||
*/
|
||||
PersistenceRegistry.register(STORE_NAME);
|
||||
|
||||
/**
|
||||
* Reduces redux actions for the purposes of the feature {@code recent-list}.
|
||||
*/
|
||||
ReducerRegistry.register<IRecentListState>(STORE_NAME, (state = DEFAULT_STATE, action): IRecentListState => {
|
||||
if (isRecentListEnabled()) {
|
||||
switch (action.type) {
|
||||
case DELETE_RECENT_LIST_ENTRY:
|
||||
return _deleteRecentListEntry(state, action.entryId);
|
||||
case _STORE_CURRENT_CONFERENCE:
|
||||
return _storeCurrentConference(state, action);
|
||||
case _UPDATE_CONFERENCE_DURATION:
|
||||
return _updateConferenceDuration(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a recent list entry based on the url and date of the item.
|
||||
*
|
||||
* @param {IRecentListState} state - The Redux state.
|
||||
* @param {Object} entryId - The ID object of the entry.
|
||||
* @returns {IRecentListState}
|
||||
*/
|
||||
function _deleteRecentListEntry(
|
||||
state: Array<IRecent>, entryId: { date: number; url: string; }): Array<IRecent> {
|
||||
return state.filter(entry =>
|
||||
entry.conference !== entryId.url || entry.date !== entryId.date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new list entry to the redux store.
|
||||
*
|
||||
* @param {IRecentListState} state - The redux state of the feature {@code recent-list}.
|
||||
* @param {Object} action - The redux action.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _storeCurrentConference(state: IRecentListState, { locationURL }: { locationURL: { href: string; }; }) {
|
||||
const conference = getURLWithoutParamsNormalized(new URL(locationURL.href));
|
||||
|
||||
// If the current conference is already in the list, we remove it to re-add
|
||||
// it to the top.
|
||||
const nextState
|
||||
= state.filter(e => !_urlStringEquals(e.conference, conference));
|
||||
|
||||
// The list is a reverse-sorted (i.e. the newer elements are at the end).
|
||||
nextState.push({
|
||||
conference,
|
||||
date: Date.now(),
|
||||
duration: 0 // We don't have the duration yet!
|
||||
});
|
||||
|
||||
// Ensure the list doesn't exceed a/the maximum size.
|
||||
nextState.splice(0, nextState.length - MAX_LIST_SIZE);
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the conference length when left.
|
||||
*
|
||||
* @param {IRecentListState} state - The redux state of the feature {@code recent-list}.
|
||||
* @param {Object} action - The redux action.
|
||||
* @returns {Object} The next redux state of the feature {@code recent-list}.
|
||||
*/
|
||||
function _updateConferenceDuration(state: IRecentListState, { locationURL }: { locationURL: { href: string; }; }) {
|
||||
if (locationURL?.href && state.length) {
|
||||
const mostRecentIndex = state.length - 1;
|
||||
const mostRecent = state[mostRecentIndex];
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
// Shallow copy to avoid in-place modification.
|
||||
const nextState = state.slice();
|
||||
|
||||
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) {
|
||||
const aHref = getURLWithoutParamsNormalized(new URL(a));
|
||||
const bHref = getURLWithoutParamsNormalized(new URL(b));
|
||||
|
||||
return aHref === bHref;
|
||||
}
|
||||
5
react/features/recent-list/types.ts
Normal file
5
react/features/recent-list/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface IRecentItem {
|
||||
conference: string;
|
||||
date: number;
|
||||
duration: number;
|
||||
}
|
||||
Reference in New Issue
Block a user