You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CalendarTab.tsx 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. import { Theme } from '@mui/material';
  2. import React, { Component } from 'react';
  3. import { WithTranslation } from 'react-i18next';
  4. import { connect } from 'react-redux';
  5. import { withStyles } from 'tss-react/mui';
  6. import { IReduxState, IStore } from '../../../app/types';
  7. import { translate } from '../../../base/i18n/functions';
  8. import { withPixelLineHeight } from '../../../base/styles/functions.web';
  9. import Button from '../../../base/ui/components/web/Button';
  10. import Spinner from '../../../base/ui/components/web/Spinner';
  11. import { bootstrapCalendarIntegration, clearCalendarIntegration, signIn } from '../../../calendar-sync/actions';
  12. import MicrosoftSignInButton from '../../../calendar-sync/components/MicrosoftSignInButton';
  13. import { CALENDAR_TYPE } from '../../../calendar-sync/constants';
  14. import { isCalendarEnabled } from '../../../calendar-sync/functions';
  15. import GoogleSignInButton from '../../../google-api/components/GoogleSignInButton';
  16. import logger from '../../logger';
  17. /**
  18. * The type of the React {@code Component} props of {@link CalendarTab}.
  19. */
  20. interface IProps extends WithTranslation {
  21. /**
  22. * The name given to this Jitsi Application.
  23. */
  24. _appName: string;
  25. /**
  26. * Whether or not to display a button to sign in to Google.
  27. */
  28. _enableGoogleIntegration: boolean;
  29. /**
  30. * Whether or not to display a button to sign in to Microsoft.
  31. */
  32. _enableMicrosoftIntegration: boolean;
  33. /**
  34. * The current calendar integration in use, if any.
  35. */
  36. _isConnectedToCalendar: boolean;
  37. /**
  38. * The email address associated with the calendar integration in use.
  39. */
  40. _profileEmail?: string;
  41. /**
  42. * CSS classes object.
  43. */
  44. classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
  45. /**
  46. * Invoked to change the configured calendar integration.
  47. */
  48. dispatch: IStore['dispatch'];
  49. }
  50. /**
  51. * The type of the React {@code Component} state of {@link CalendarTab}.
  52. */
  53. interface IState {
  54. /**
  55. * Whether or not any third party APIs are being loaded.
  56. */
  57. loading: boolean;
  58. }
  59. const styles = (theme: Theme) => {
  60. return {
  61. container: {
  62. width: '100%',
  63. display: 'flex',
  64. flexDirection: 'column' as const,
  65. alignItems: 'center',
  66. justifyContent: 'center',
  67. textAlign: 'center',
  68. minHeight: '100px',
  69. color: theme.palette.text01,
  70. ...withPixelLineHeight(theme.typography.bodyShortRegular)
  71. },
  72. button: {
  73. marginTop: theme.spacing(4)
  74. }
  75. };
  76. };
  77. /**
  78. * React {@code Component} for modifying calendar integration.
  79. *
  80. * @augments Component
  81. */
  82. class CalendarTab extends Component<IProps, IState> {
  83. /**
  84. * Initializes a new {@code CalendarTab} instance.
  85. *
  86. * @inheritdoc
  87. */
  88. constructor(props: IProps) {
  89. super(props);
  90. this.state = {
  91. loading: true
  92. };
  93. // Bind event handlers so they are only bound once for every instance.
  94. this._onClickDisconnect = this._onClickDisconnect.bind(this);
  95. this._onClickGoogle = this._onClickGoogle.bind(this);
  96. this._onClickMicrosoft = this._onClickMicrosoft.bind(this);
  97. }
  98. /**
  99. * Loads third party APIs as needed and bootstraps the initial calendar
  100. * state if not already set.
  101. *
  102. * @inheritdoc
  103. */
  104. componentDidMount() {
  105. this.props.dispatch(bootstrapCalendarIntegration())
  106. .catch((err: any) => logger.error('CalendarTab bootstrap failed', err))
  107. .then(() => this.setState({ loading: false }));
  108. }
  109. /**
  110. * Implements React's {@link Component#render()}.
  111. *
  112. * @inheritdoc
  113. * @returns {ReactElement}
  114. */
  115. render() {
  116. const classes = withStyles.getClasses(this.props);
  117. let view;
  118. if (this.state.loading) {
  119. view = this._renderLoadingState();
  120. } else if (this.props._isConnectedToCalendar) {
  121. view = this._renderSignOutState();
  122. } else {
  123. view = this._renderSignInState();
  124. }
  125. return (
  126. <div className = { classes.container }>
  127. { view }
  128. </div>
  129. );
  130. }
  131. /**
  132. * Dispatches the action to start the sign in flow for a given calendar
  133. * integration type.
  134. *
  135. * @param {string} type - The calendar type to try integrating with.
  136. * @private
  137. * @returns {void}
  138. */
  139. _attemptSignIn(type: string) {
  140. this.props.dispatch(signIn(type));
  141. }
  142. /**
  143. * Dispatches an action to sign out of the currently connected third party
  144. * used for calendar integration.
  145. *
  146. * @private
  147. * @returns {void}
  148. */
  149. _onClickDisconnect() {
  150. // We clear the integration state instead of actually signing out. This
  151. // is for two primary reasons. Microsoft does not support a sign out and
  152. // instead relies on clearing of local auth data. Google signout can
  153. // also sign the user out of YouTube. So for now we've decided not to
  154. // do an actual sign out.
  155. this.props.dispatch(clearCalendarIntegration());
  156. }
  157. /**
  158. * Starts the sign in flow for Google calendar integration.
  159. *
  160. * @private
  161. * @returns {void}
  162. */
  163. _onClickGoogle() {
  164. this._attemptSignIn(CALENDAR_TYPE.GOOGLE);
  165. }
  166. /**
  167. * Starts the sign in flow for Microsoft calendar integration.
  168. *
  169. * @private
  170. * @returns {void}
  171. */
  172. _onClickMicrosoft() {
  173. this._attemptSignIn(CALENDAR_TYPE.MICROSOFT);
  174. }
  175. /**
  176. * Render a React Element to indicate third party APIs are being loaded.
  177. *
  178. * @private
  179. * @returns {ReactElement}
  180. */
  181. _renderLoadingState() {
  182. return (
  183. <Spinner />
  184. );
  185. }
  186. /**
  187. * Render a React Element to sign into a third party for calendar
  188. * integration.
  189. *
  190. * @private
  191. * @returns {ReactElement}
  192. */
  193. _renderSignInState() {
  194. const {
  195. _appName,
  196. _enableGoogleIntegration,
  197. _enableMicrosoftIntegration,
  198. t
  199. } = this.props;
  200. const classes = withStyles.getClasses(this.props);
  201. return (
  202. <>
  203. <p>
  204. { t('settings.calendar.about',
  205. { appName: _appName || '' }) }
  206. </p>
  207. { _enableGoogleIntegration
  208. && <div className = { classes.button }>
  209. <GoogleSignInButton
  210. onClick = { this._onClickGoogle }
  211. text = { t('liveStreaming.signIn') } />
  212. </div> }
  213. { _enableMicrosoftIntegration
  214. && <div className = { classes.button }>
  215. <MicrosoftSignInButton
  216. onClick = { this._onClickMicrosoft }
  217. text = { t('settings.calendar.microsoftSignIn') } />
  218. </div> }
  219. </>
  220. );
  221. }
  222. /**
  223. * Render a React Element to sign out of the currently connected third
  224. * party used for calendar integration.
  225. *
  226. * @private
  227. * @returns {ReactElement}
  228. */
  229. _renderSignOutState() {
  230. const { _profileEmail, t } = this.props;
  231. const classes = withStyles.getClasses(this.props);
  232. return (
  233. <>
  234. { t('settings.calendar.signedIn',
  235. { email: _profileEmail }) }
  236. <Button
  237. className = { classes.button }
  238. id = 'calendar_logout'
  239. label = { t('settings.calendar.disconnect') }
  240. onClick = { this._onClickDisconnect } />
  241. </>
  242. );
  243. }
  244. }
  245. /**
  246. * Maps (parts of) the Redux state to the associated props for the
  247. * {@code CalendarTab} component.
  248. *
  249. * @param {Object} state - The Redux state.
  250. * @private
  251. * @returns {{
  252. * _appName: string,
  253. * _enableGoogleIntegration: boolean,
  254. * _enableMicrosoftIntegration: boolean,
  255. * _isConnectedToCalendar: boolean,
  256. * _profileEmail: string
  257. * }}
  258. */
  259. function _mapStateToProps(state: IReduxState) {
  260. const calendarState = state['features/calendar-sync'] || {};
  261. const {
  262. googleApiApplicationClientID,
  263. microsoftApiApplicationClientID
  264. } = state['features/base/config'];
  265. const calendarEnabled = isCalendarEnabled(state);
  266. return {
  267. _appName: interfaceConfig.APP_NAME,
  268. _enableGoogleIntegration: Boolean(
  269. calendarEnabled && googleApiApplicationClientID),
  270. _enableMicrosoftIntegration: Boolean(
  271. calendarEnabled && microsoftApiApplicationClientID),
  272. _isConnectedToCalendar: calendarState.integrationReady,
  273. _profileEmail: calendarState.profileEmail
  274. };
  275. }
  276. export default withStyles(translate(connect(_mapStateToProps)(CalendarTab)), styles);