From 45c405de0e56a6fe71d6b66e708ad679f1e22508 Mon Sep 17 00:00:00 2001 From: Zoltan Bettenbuk Date: Wed, 13 Dec 2017 11:35:42 +0100 Subject: [PATCH] [RN] Add recent-list feature --- css/_font.scss | 9 + doc/adding-an-icon.md | 13 +- fonts/jitsi.eot | Bin 8684 -> 9272 bytes fonts/jitsi.svg | 3 + fonts/jitsi.ttf | Bin 8528 -> 9116 bytes fonts/jitsi.woff | Bin 8604 -> 9192 bytes fonts/selection.json | 187 +++++++++++----- package-lock.json | 5 + package.json | 1 + react/features/base/font-icons/jitsi.json | 187 +++++++++++----- .../base/lib-jitsi-meet/native/Storage.js | 20 ++ .../components/AbstractRecentList.js | 101 +++++++++ .../components/RecentList.native.js | 208 ++++++++++++++++++ .../features/recent-list/components/index.js | 1 + .../features/recent-list/components/styles.js | 154 +++++++++++++ react/features/recent-list/constants.js | 11 + react/features/recent-list/functions.js | 132 +++++++++++ react/features/recent-list/index.js | 3 + react/features/recent-list/middleware.js | 106 +++++++++ .../welcome/components/WelcomePage.native.js | 16 +- react/features/welcome/components/styles.js | 5 +- 21 files changed, 1041 insertions(+), 121 deletions(-) mode change 100644 => 100755 react/features/base/font-icons/jitsi.json create mode 100644 react/features/recent-list/components/AbstractRecentList.js create mode 100644 react/features/recent-list/components/RecentList.native.js create mode 100644 react/features/recent-list/components/index.js create mode 100644 react/features/recent-list/components/styles.js create mode 100644 react/features/recent-list/constants.js create mode 100644 react/features/recent-list/functions.js create mode 100644 react/features/recent-list/index.js create mode 100644 react/features/recent-list/middleware.js diff --git a/css/_font.scss b/css/_font.scss index 01f66316bb..2fd9556f09 100644 --- a/css/_font.scss +++ b/css/_font.scss @@ -25,6 +25,15 @@ -moz-osx-font-smoothing: grayscale; } +.icon-public:before { + content: "\e80b"; +} +.icon-event_note:before { + content: "\e616"; +} +.icon-timer:before { + content: "\e425"; +} .icon-thumb-menu:before { content: "\e5d4"; } diff --git a/doc/adding-an-icon.md b/doc/adding-an-icon.md index be19a6b5fb..2ebf6b8095 100644 --- a/doc/adding-an-icon.md +++ b/doc/adding-an-icon.md @@ -2,11 +2,12 @@ 1. Go to https://icomoon.io/app/ 2. Go to "Manage Projects" from the menu on the top left. 3. Use "Import project" and select fonts/selection.json from Jitsi Meet. -4. Import icons (e.g. svg files) using the "import items" button. -5. Go to "generate font" and make sure the identifiers for the new icons are correct. -6. Download the result in a zip file using the "download" button. -7. Copy selection.json and fonts/jitsi.* from the zip file to fonts/ in Jitsi Meet -8. Copy the class for the new icon from style.css in the zip file to css/font.css in Jitsi Meet (do *not* copy the whole file) +4. Click "load". +5. Add the new icons using the "Add icons from library" button... +6. Go to "generate font" and make sure the identifiers for the new icons are correct. +7. Download the result in a zip file using the "download" button. +8. Copy selection.json and fonts/jitsi.* from the zip file to fonts/ in Jitsi Meet +9. Copy the class for the new icon from style.css in the zip file to css/_font.scss in Jitsi Meet (do *not* copy the whole file) +10. Copy the selection.json file to react/features/base/font-icons overwriting jitsi.json Sample commit: https://github.com/jitsi/jitsi-meet/commit/68bc819b89aec12364fcf07b81efa83a1900eed6 - diff --git a/fonts/jitsi.eot b/fonts/jitsi.eot index 38ada4e6042ac710af680a4266c599b1b9a86571..7621636a87961eeaa2850b6da377c1588a67c69f 100755 GIT binary patch delta 942 zcmZWnOK1~O6uoa|GWn;O{1cl=W)c&LwzZSbsExH+TNG@yIt(gO4SxEet(6L`T5V8q zrJxMB5)=`YT36kOxUjfT#9tu<7b=#tp=vjRqKHj=V-+gi$Gm&byXU@pxNqhf?`@)2 zg#oUFF3~CK$jo5-rH0{=eU?280ICFlv5w3@KlWCPpJ1=+=s9-iQ_Z{M7{3BwhdK{t z4p?hSs{w+Wa6Z+E4|E5pyBKFN4tMq*9h!`}IxxP0@#~($`!g@nkFNu8!x;DVW`_D< zJv_pA4&z{7rq_G$?ZuO~0C*E_(${}@;3$wd1)*U@LT9=zsx-VP>=6XtKc8xv8ZB6- zm(JvhWYJM{75%fu50UbGxvWC<|@d%t- zQcVPu_Z&UmLg)yFf-J~nmdwKvsDpLTsLa_}60t~O9L|xr;Bt#Baw8_r*qvOGfO;|2 z=El~R66+Vms5@H4vR%zh+qO10cd^*ixbcLLwz8H|pCp%I)Z=aw|0%*3h;42U;j2Bi zadFu~TobJ~n^~rG8$lU}6|y`oQ4`dNDmeRFFbP@+KnRZuE7LF>g9Mk8anK=-k0hBy zstzUNI1_i0s&cuMBVc}vkoZJAG(Q#=gm6qok{l~ZXjGDNC|6oRjH)S>pweV)rMxTy zey4?=SuiR={mW`URKaH4w;m-E(P|WpM4b+LIa=5_!R1GIaGE3^S&O0;q>zqKnD`l6 zX{}ftT~-qhNj{r|z~+fnR+S2T;u%Hbd5U6}vd!RbK)$NM4awTt9f6?T#tX8|BYSLF z{@nI>DE7=2ajaicx7+V0zHt0NYs;>kt*rw*w>Taz=qDYO?SnH%_Ex07f>HgS0P^yi ztAD-niWN~x(FJ%V67XuWm?10az@6T1bPc^D9+FQfit3?WYZ98Xn!M&C-As4W=jjL9 zw05WVl=i0fyRKDtMK`T)&|lW)41(dlp=h)k1!KGMrfHdJi|Mo}Z<;mzG*_Fu%{j|< b%a4jQ?v3!5f(bCFI)G3nIs5qD;05#xt=jrk delta 435 zcmdnt@y40$jUod>fZ{|pGZyy`d7CFX)YPw%i_c(SV3c8CV3?AgSX=<41%UhwK$;^x zr!q~2XW0rMe+L7DOhiU%VhaDc(_0xB{1#IXwHRo6m14K{eP&8*Wp6sBgG&xOCbaJ!eifA*&5~eWb1m+Vg zDl8=|$5?K$nz2T)*08Q&(_!;q%VV3y_JZAxy@&k@hXF?$#}Q5?&Q+Y(xEQ#Uxcs=L oaZ7O9a2IhOqafDXn4z03vgNSO5S3 diff --git a/fonts/jitsi.svg b/fonts/jitsi.svg index bb75dba1ff..15552e68d5 100755 --- a/fonts/jitsi.svg +++ b/fonts/jitsi.svg @@ -11,11 +11,14 @@ + + + diff --git a/fonts/jitsi.ttf b/fonts/jitsi.ttf index 62aed45c0132342e632227fafbfb97a6327efc0c..e67633eab6244137dff04d36a189af273f9b3b3a 100755 GIT binary patch delta 944 zcmZWnTS!z<6y4|EJ1^(q+<7}W*O@yuBa@H3^SGFkrKJ{>sSOtku?96C_zI=0ET;#3 z^QFJn9dzznvxhFi2nsS*JD5u6XS;|qBU z^%Ua>#<}g?1A`wurZ$WpVm#Z`+Z=u$eDwgpF@$kXcX+T5%Hbu(-!ZoLguAm^KaE^{ z1i-3rlb*ib{sACy3Y=D>MzgwtWh%IWDb}z@1WYUl{C2cFpFRt`P zIxQCc$I$;}piKr!FXr&fG}D;9x@*zux5~2CvR1QxSSWKQZi+Tv*l|ogKxTrbAOZpz zArr6*O5p$;iq`5G5-})xU11#dawap+ATy%9wBG3O6Hvwn>de^c0({xF7In&Hh*`{a{Ej000AFXe5Z2;Lhqsq)#YOHiwVI(*j}fGS z$QOjkDe?t1EDDZn3MN4YIpD;@!^$MIgGQXmNE@V12kY|FzCbDRdvV5VBt_*yDo()0 z1QB9=UgyR{F30711QZfHsSt{VLU9yNKOrL^p^ZXp$!V) zDDGQ^{62Rfa=Y9{gLDttIvkwIhVbAde%7@gxiu)-rgt_I>z=*Z%o2BAk=Gfr>Ou%~ z7Eg9TYLWH5rKEVCJm1k!Q`65n9BhBh@l)GkWG%Q@;V&*enPb=MSWeJc1dA@h-mdpL zJ#Td`L;1ea(>5FNGdicYOjavC8nT-%E}uV>8TZ*`yp-<*AOTZm1Hf71g@BP~EAHYw9(> QGlN)=_$vdG<2f<(2RnrH-v9sr delta 421 zcmbQ^e!*#iLj5Ya_zVUHMi~YMhAHWZ#RWiG0Lb3}q&d=aD$`VWmaPEtcQ7!>L}a8U zrtqITy_JDM4ycB~ECVROk;1YH$oBy9RWfo*DvoN23Ih2#K>nGW{N%*77Mr&)Fvwc~ z`5C#16$K1-42OaI1wg(+USh6f>fH&o%Ygz1fR^MH*L_jrDruMFHQ zAd4BUsX52O=*c+>=8VRZmnbMrKBXW!`MJW1R5QjBrZDCN<`XO`EF~<*SZ=YJu|~1h zu&!a#Ve??iW1GhIg58e2hy4nN0Y@9h5l$t}Rh-wj7`T+U{J5rZOK{t87jYlszQO&5 mM~)|o=LoM8?<+nXpywDEj2R9By$J$LlQ%L7Z{}C(Wds0i-gm$N diff --git a/fonts/jitsi.woff b/fonts/jitsi.woff index b27004bb5a5decf182d27fbe1ddd33df36a60bd3..f110bbb598b1376b6ead542c827db15fee7678fa 100755 GIT binary patch delta 975 zcmZ8gNoW&c6n_6?vNZc-Z)_%+Nlhenv6E%g#=2B1f>x_xs3K}{=|WqT3UxsnMZBm` z9r099JgC%q%0lmu&e`v7#l;sQh{zCeYd0M2<8O7pg*BtJmPBaKDZZo+!OSB6+BN0 zk8kv}9mT@@QVax2SPhSQ+j{n6tQCMDk@UVEJbCNT!M-DSgD00}&UX%$hykEc69~L{ z_IBOd;VJ98h3~U@Ro;_c{E#g7G3-nds= z{%q-DS3=L_JRB2TX7C=3$ghqMD&Z-DYOtiRBS`uQzJezcdt68?QY26llm`)Z&u^UeaNh@nE^roaD413%y;y+1v zfY{>pP~N&@Yv-r^v1`K(CKJmPZWAa1RV7IonjWPGl!nu#hE<>iKLl}H*qNH)7$Ufw zj6)OT_)wgQCF)5$iaSxKO6goGWGR>(p;CM-8k`-e6okr%L{d_ukRrpWRF-55Rn)Mu zr4SUFgi^>!5`b#1W*Q9gOWlhaA5_BztlL21v2YCuhr&*WW-Kn)RHE-J9+T7g7bMx(tTsH2g*wz33@NP@; zI~bJD2_PrEy7pJgX;vf&`5w#5IksG#MusGRX7BWLk?UlVdZ>Cz({wjIp^mA~s&nd( znkG$`=7Q#dHmPmZp3>gb{?s+=hIH@BR+U{X%jyOFeSO|wH3)`XhMUGk#*M}^#+-55 kIAf|Yb(yl}t>#~rB-U1b)&z`#Ug-lU`7~$GSOOQxZ=WCmLI3~& delta 471 zcmaFiKF3+C+~3WOfsp|S6z4E-gXsVU#>p3z#U|>=*G~ZoOj#utpOKzeT)@D<_y@@6 zfMS94oXRu?1|}8;2AK#DR^eH;A|o|1g@HjX2B^jig!#{%-kJdv1d4&oQUPI(6qZ#P zxg`}qu^mAE84y0IB`TPcpA1wd-vZ=kfbd!iqs?1#6Dxog%fA5%D1fmY!{NNd+*BZ! z2k4+Y5WYL1c6mX5aS6~5H531IhXYM!kYW&EU}oR~dQIWM*@vzVS3MSZdgU4OGm&Sq zFV+74{r?}R251I~oG?(15iG~}|FOKe+yS{5neS32Qf!jnCEttlO#Z+qzgd)tgK6_e zmIEx4Rk?0$zRrDAL}qi|2lse>o39Mqz@PwvYiiE1FnaP11#?E@$sZJyCW|SGPBvCt z5pBj;!W71wz { + AsyncStorage.getItem( + `${String(this._keyPrefix)}${key}`, + (error, result) => { + resolve(result ? result : null); + }); + }); + } + /** * Returns the name of the nth key in this storage. * diff --git a/react/features/recent-list/components/AbstractRecentList.js b/react/features/recent-list/components/AbstractRecentList.js new file mode 100644 index 0000000000..43bcf31fd2 --- /dev/null +++ b/react/features/recent-list/components/AbstractRecentList.js @@ -0,0 +1,101 @@ +// @flow + +import { Component } from 'react'; +import { ListView } from 'react-native'; + +import { getRecentRooms } from '../functions'; + +import { appNavigate } from '../../app'; + +/** + * The type of the React {@code Component} state of {@link AbstractRecentList}. + */ +type State = { + + /** + * The {@code ListView.DataSource} to be used for the {@code ListView}. + * Its content comes from the native implementation of + * {@code window.localStorage}. + */ + dataSource: Object +} + +/** + * The type of the React {@code Component} props of {@link AbstractRecentList} + */ +type Props = { + + /** + * Redux store dispatch function. + */ + dispatch: Dispatch<*>, +} + +/** + * Implements a React {@link Component} which represents the list of + * conferences recently joined, similar to how a list of last dialed + * numbers list would do on a mobile + * + * @extends Component + */ +export default class AbstractRecentList extends Component { + + /** + * The datasource that backs the {@code ListView} + */ + listDataSource = new ListView.DataSource({ + rowHasChanged: (r1, r2) => + r1.conference !== r2.conference + && r1.dateTimeStamp !== r2.dateTimeStamp + });; + + /** + * Initializes a new {@code AbstractRecentList} instance. + */ + constructor() { + super(); + + this.state = { + dataSource: this.listDataSource.cloneWithRows([]) + }; + } + + /** + * Implements React's {@link Component#componentWillMount()}. Invoked + * immediately before mounting occurs. + * + * @inheritdoc + */ + componentWillMount() { + // this must be done asynchronously because we don't have the storage + // initiated on app startup immediately. + getRecentRooms().then(rooms => { + this.setState({ + dataSource: this.listDataSource.cloneWithRows(rooms) + }); + }); + } + + /** + * Creates a bound onPress action for the list item. + * + * @param {string} room - The selected room. + * @returns {Function} + */ + _onSelect(room) { + return this._onJoin.bind(this, room); + } + + /** + * Joins the selected room. + * + * @param {string} room - The selected room. + * @returns {void} + */ + _onJoin(room) { + if (room) { + this.props.dispatch(appNavigate(room)); + } + } + +} diff --git a/react/features/recent-list/components/RecentList.native.js b/react/features/recent-list/components/RecentList.native.js new file mode 100644 index 0000000000..5d49334745 --- /dev/null +++ b/react/features/recent-list/components/RecentList.native.js @@ -0,0 +1,208 @@ +import React from 'react'; +import { ListView, Text, TouchableHighlight, View } from 'react-native'; +import { connect } from 'react-redux'; + +import AbstractRecentList from './AbstractRecentList'; +import styles, { UNDERLAY_COLOR } from './styles'; + +import { Icon } from '../../base/font-icons'; + +/** + * The native container rendering the list of the recently joined rooms. + * + * @extends AbstractRecentList + */ +class RecentList extends AbstractRecentList { + /** + * Initializes a new {@code RecentList} instance. + * + */ + constructor() { + super(); + + this._getAvatarStyle = this._getAvatarStyle.bind(this); + this._onSelect = this._onSelect.bind(this); + this._renderConfDuration = this._renderConfDuration.bind(this); + this._renderRow = this._renderRow.bind(this); + this._renderServerInfo = this._renderServerInfo.bind(this); + } + + /** + * Implements React's {@link Component#render()}. Renders a list of + * recently joined rooms. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + if (!this.state.dataSource.getRowCount()) { + return null; + } + + return ( + + + + ); + } + + /** + * Renders the list of recently joined rooms. + * + * @private + * @param {Object} data - The row data to be rendered. + * @returns {ReactElement} + */ + _renderRow(data) { + return ( + + + + + + { data.initials } + + + + + + { data.room } + + + + + { data.dateString } + + + { + this._renderConfDuration(data) + } + { + this._renderServerInfo(data) + } + + + + ); + } + + /** + * Assembles the style array of the avatar based on if the conference + * was a home or remote server conference (based on current app setting). + * + * @private + * @param {Object} recentListEntry - The recent list entry being rendered. + * @returns {Array} + */ + _getAvatarStyle(recentListEntry) { + const avatarStyles = [ styles.avatar ]; + + if (recentListEntry.baseURL !== this.props._homeServer) { + avatarStyles.push( + this._getColorForServerName(recentListEntry.serverName) + ); + } + + return avatarStyles; + } + + /** + * Returns a style (color) based on the server name, so then the + * same server will always be rendered with the same avatar color. + * + * @private + * @param {string} serverName - The recent list entry being rendered. + * @returns {Object} + */ + _getColorForServerName(serverName) { + let nameHash = 0; + + for (let i = 0; i < serverName.length; i++) { + nameHash += serverName.codePointAt(i); + } + + return styles[`avatarRemoteServer${(nameHash % 5) + 1}`]; + } + + /** + * Renders the server info component based on if the entry was + * on a different server or not. + * + * @private + * @param {Object} recentListEntry - The recent list entry being rendered. + * @returns {ReactElement} + */ + _renderServerInfo(recentListEntry) { + if (recentListEntry.baseURL !== this.props._homeServer) { + return ( + + + + { recentListEntry.serverName } + + + ); + } + + return null; + } + + /** + * Renders the conference duration if available. + * + * @private + * @param {Object} recentListEntry - The recent list entry being rendered. + * @returns {ReactElement} + */ + _renderConfDuration(recentListEntry) { + if (recentListEntry.conferenceDurationString) { + return ( + + + + { recentListEntry.conferenceDurationString } + + + ); + } + + return null; + } +} + +/** + * Maps (parts of) the Redux state to the associated RecentList's props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _homeServer: string + * }} + */ +function _mapStateToProps(state) { + return { + /** + * The default server name based on which we determine + * the render method. + * + * @private + * @type {string} + */ + _homeServer: state['features/app'].app._getDefaultURL() + }; +} + +export default connect(_mapStateToProps)(RecentList); diff --git a/react/features/recent-list/components/index.js b/react/features/recent-list/components/index.js new file mode 100644 index 0000000000..03545a486f --- /dev/null +++ b/react/features/recent-list/components/index.js @@ -0,0 +1 @@ +export { default as RecentList } from './RecentList'; diff --git a/react/features/recent-list/components/styles.js b/react/features/recent-list/components/styles.js new file mode 100644 index 0000000000..0b908e8959 --- /dev/null +++ b/react/features/recent-list/components/styles.js @@ -0,0 +1,154 @@ +import { + createStyleSheet, + BoxModel +} from '../../base/styles'; + +const AVATAR_OPACITY = 0.4; +const AVATAR_SIZE = 65; +const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)'; + +export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)'; + +/** + * The styles of the React {@code Components} of the feature: recent list + * {@code RecentList}. + */ +export default createStyleSheet({ + + /** + * The style of the actual avatar + */ + avatar: { + width: AVATAR_SIZE, + height: AVATAR_SIZE, + alignItems: 'center', + backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`, + justifyContent: 'center', + borderRadius: AVATAR_SIZE + }, + + /** + * The style of the avatar container that makes the avatar rounded. + */ + avatarContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + alignItems: 'center', + paddingTop: 5 + }, + + /** + * Simple {@code Text} content of the avatar (the actual initials) + */ + avatarContent: { + color: OVERLAY_FONT_COLOR, + fontSize: 32, + fontWeight: '100', + backgroundColor: 'rgba(0, 0, 0, 0)', + textAlign: 'center' + }, + + /** + * List of styles of the avatar of a remote meeting + * (not the default server). The number of colors are limited + * because they should match nicely. + */ + avatarRemoteServer1: { + backgroundColor: `rgba(232, 105, 156, ${AVATAR_OPACITY})` + }, + + avatarRemoteServer2: { + backgroundColor: `rgba(255, 198, 115, ${AVATAR_OPACITY})` + }, + + avatarRemoteServer3: { + backgroundColor: `rgba(128, 128, 255, ${AVATAR_OPACITY})` + }, + + avatarRemoteServer4: { + backgroundColor: `rgba(105, 232, 194, ${AVATAR_OPACITY})` + }, + + avatarRemoteServer5: { + backgroundColor: `rgba(234, 255, 128, ${AVATAR_OPACITY})` + }, + + /** + * Style of the conference length (if rendered) + */ + confLength: { + color: OVERLAY_FONT_COLOR, + fontWeight: 'normal' + }, + + /** + * This is the top level container style of the list + */ + container: { + flex: 1 + }, + + /** + * Second line of the list (date). + * May be extended with server name later. + */ + date: { + color: OVERLAY_FONT_COLOR + }, + + /** + * The style of the details container (right side) of the list + */ + detailsContainer: { + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + marginLeft: 2 * BoxModel.margin, + alignItems: 'flex-start' + }, + + /** + * The container for an info line with an inline icon. + */ + infoWithIcon: { + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center' + }, + + /** + * Style of an inline icon in an info line. + */ + inlineIcon: { + color: OVERLAY_FONT_COLOR, + marginRight: 5 + }, + + /** + * First line of the list (room name) + */ + roomName: { + fontSize: 18, + fontWeight: 'bold', + color: OVERLAY_FONT_COLOR + }, + + /** + * The style of one single row in the list + */ + row: { + padding: 8, + paddingBottom: 0, + flex: 1, + flexDirection: 'row', + alignItems: 'center' + }, + + /** + * Style of the server name component (if rendered) + */ + serverName: { + color: OVERLAY_FONT_COLOR, + fontWeight: 'normal' + } +}); diff --git a/react/features/recent-list/constants.js b/react/features/recent-list/constants.js new file mode 100644 index 0000000000..00df5f2f0f --- /dev/null +++ b/react/features/recent-list/constants.js @@ -0,0 +1,11 @@ +/** + * The max size of the list. + */ +export const LIST_SIZE = 30; + +/** + * The name of the {@code localStorage} item where recent rooms are stored. + * + * @type {string} + */ +export const RECENT_URL_STORAGE = 'recentURLs'; diff --git a/react/features/recent-list/functions.js b/react/features/recent-list/functions.js new file mode 100644 index 0000000000..9f03c09c16 --- /dev/null +++ b/react/features/recent-list/functions.js @@ -0,0 +1,132 @@ +// @flow + +import moment from 'moment'; + +import { RECENT_URL_STORAGE } from './constants'; + +import { i18next } from '../base/i18n'; +import { parseURIString } from '../base/util'; + +/** +* Retreives the recent room list and generates all the data needed +* to be displayed. +* +* @returns {Promise} The {@code Promise} to be resolved when the list +* is available. +*/ +export function getRecentRooms(): Promise> { + return new Promise(resolve => { + window.localStorage._getItemAsync(RECENT_URL_STORAGE) + .then(recentUrls => { + if (recentUrls) { + const recentUrlsObj = JSON.parse(recentUrls); + const recentRoomDS = []; + + for (const entry of recentUrlsObj) { + const location = parseURIString(entry.conference); + + if (location && location.room && location.hostname) { + recentRoomDS.push({ + baseURL: + `${location.protocol}//${location.host}`, + conference: entry.conference, + dateTimeStamp: entry.date, + conferenceDuration: entry.conferenceDuration, + dateString: _getDateString( + entry.date + ), + conferenceDurationString: _getLengthString( + entry.conferenceDuration + ), + initials: _getInitials(location.room), + room: location.room, + serverName: location.hostname + }); + } + } + + resolve(recentRoomDS.reverse()); + } else { + resolve([]); + } + }); + }); +} + +/** +* Retreives the recent URL list as a list of objects. +* +* @returns {Array} The list of already stored recent URLs. +*/ +export function getRecentUrls() { + let recentUrls = window.localStorage.getItem(RECENT_URL_STORAGE); + + if (recentUrls) { + recentUrls = JSON.parse(recentUrls); + } else { + recentUrls = []; + } + + return recentUrls; +} + +/** +* Updates the recent URL list. +* +* @param {Array} recentUrls - The new URL list. +* @returns {void} +*/ +export function updaterecentUrls(recentUrls: Array) { + window.localStorage.setItem( + RECENT_URL_STORAGE, + JSON.stringify(recentUrls) + ); +} + +/** +* Returns a well formatted date string to be displayed in the list. +* +* @private +* @param {number} dateTimeStamp - The UTC timestamp to be converted to String. +* @returns {string} +*/ +function _getDateString(dateTimeStamp: number) { + const date = new Date(dateTimeStamp); + + if (date.toDateString() === new Date().toDateString()) { + // the date is today, we use fromNow format + + return moment(date) + .locale(i18next.language) + .fromNow(); + } + + return moment(date) + .locale(i18next.language) + .format('lll'); +} + +/** +* Returns a well formatted duration string to be displayed +* as the conference length. +* +* @private +* @param {number} duration - The duration in MS. +* @returns {string} +*/ +function _getLengthString(duration: number) { + return moment.duration(duration) + .locale(i18next.language) + .humanize(); +} + +/** +* Returns the initials supposed to be used based on the room name. +* +* @private +* @param {string} room - The room name. +* @returns {string} +*/ +function _getInitials(room: string) { + return room && room.charAt(0) ? room.charAt(0).toUpperCase() : '?'; +} diff --git a/react/features/recent-list/index.js b/react/features/recent-list/index.js new file mode 100644 index 0000000000..7ce19e0e6b --- /dev/null +++ b/react/features/recent-list/index.js @@ -0,0 +1,3 @@ +export * from './components'; + +import './middleware'; diff --git a/react/features/recent-list/middleware.js b/react/features/recent-list/middleware.js new file mode 100644 index 0000000000..a99ee927c0 --- /dev/null +++ b/react/features/recent-list/middleware.js @@ -0,0 +1,106 @@ +/* @flow */ + +import { LIST_SIZE } from './constants'; +import { getRecentUrls, updaterecentUrls } from './functions'; + +import { CONFERENCE_WILL_LEAVE, SET_ROOM } from '../base/conference'; +import { MiddlewareRegistry } from '../base/redux'; + +/** + * Middleware that captures joined rooms so then it can be saved to + * {@code localStorage} + * + * @param {Store} store - Redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case CONFERENCE_WILL_LEAVE: + return _updateConferenceDuration(store, next, action); + + case SET_ROOM: + return _storeJoinedRoom(store, next, action); + } + + return next(action); +}); + +/** +* Stores the recently joined room in {@code localStorage}. +* +* @param {Store} store - The redux store in which the specified action is being +* dispatched. +* @param {Dispatch} next - The redux dispatch function to dispatch the +* specified action to the specified store. +* @param {Action} action - The redux action CONFERENCE_JOINED which is being +* dispatched in the specified store. +* @returns {Object} The new state that is the result of the reduction of the +* specified action. +*/ +function _storeJoinedRoom(store, next, action) { + const result = next(action); + const { room } = action; + + if (room) { + const { locationURL } = store.getState()['features/base/connection']; + const conferenceLink = locationURL.href; + + // if the current conference is already in the list, + // we remove it to add it + // to the top at the end + const recentUrls = getRecentUrls().filter( + entry => entry.conference !== conferenceLink + ); + + // please note, this is a reverse sorted array + // (newer elements at the end) + recentUrls.push({ + conference: conferenceLink, + date: Date.now(), + conferenceDuration: 0 + }); + + // maximising the size + recentUrls.splice(0, recentUrls.length - LIST_SIZE); + + updaterecentUrls(recentUrls); + } + + return result; +} + +/** +* Updates the conference length when left. +* +* @private +* @param {Store} store - The redux store in which the specified action is being +* dispatched. +* @param {Dispatch} next - The redux dispatch function to dispatch the +* specified action to the specified store. +* @param {Action} action - The redux action CONFERENCE_JOINED which is being +* dispatched in the specified store. +* @returns {Object} The new state that is the result of the reduction of the +* specified action. +*/ +function _updateConferenceDuration(store, next, action) { + const result = next(action); + const { locationURL } = store.getState()['features/base/connection']; + + if (locationURL && locationURL.href) { + const recentUrls = getRecentUrls(); + + if (recentUrls.length > 0 + && recentUrls[recentUrls.length - 1].conference + === locationURL.href) { + // the last conference start was stored + // so we need to update the length + + recentUrls[recentUrls.length - 1].conferenceDuration + = Date.now() - recentUrls[recentUrls.length - 1].date; + + updaterecentUrls(recentUrls); + } + } + + return result; +} diff --git a/react/features/welcome/components/WelcomePage.native.js b/react/features/welcome/components/WelcomePage.native.js index 4a90490cd7..1fa38d0338 100644 --- a/react/features/welcome/components/WelcomePage.native.js +++ b/react/features/welcome/components/WelcomePage.native.js @@ -2,15 +2,16 @@ import React from 'react'; import { TextInput, TouchableHighlight, View } from 'react-native'; import { connect } from 'react-redux'; +import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; +import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay'; +import styles, { PLACEHOLDER_TEXT_COLOR } from './styles'; + import { translate } from '../../base/i18n'; import { MEDIA_TYPE } from '../../base/media'; import { Link, LoadingIndicator, Text } from '../../base/react'; import { ColorPalette } from '../../base/styles'; import { createDesiredLocalTracks } from '../../base/tracks'; - -import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; -import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay'; -import styles from './styles'; +import { RecentList } from '../../recent-list'; /** * The URL at which the privacy policy is available to the user. @@ -67,9 +68,6 @@ class WelcomePage extends AbstractWelcomePage { return ( - - { t('welcomepage.roomname') } - { this._renderJoinButton() } + { this._renderLegalese() diff --git a/react/features/welcome/components/styles.js b/react/features/welcome/components/styles.js index fead989096..5b2f39488b 100644 --- a/react/features/welcome/components/styles.js +++ b/react/features/welcome/components/styles.js @@ -10,6 +10,8 @@ import { */ const TEXT_COLOR = ColorPalette.white; +export const PLACEHOLDER_TEXT_COLOR = 'rgba(255, 255, 255, 0.3)'; + /** * The styles of the React {@code Components} of the feature welcome including * {@code WelcomePage} and {@code BlankPage}. @@ -83,7 +85,8 @@ export default createStyleSheet({ flex: 1, flexDirection: 'column', justifyContent: 'center', - margin: 3 * BoxModel.margin + margin: 3 * BoxModel.margin, + marginBottom: BoxModel.margin }, /**