diff --git a/web/SWMansionIcon.react.js b/lib/components/SWMansionIcon.react.js similarity index 98% rename from web/SWMansionIcon.react.js rename to lib/components/SWMansionIcon.react.js index e0ce0406a..e68d973cb 100644 --- a/web/SWMansionIcon.react.js +++ b/lib/components/SWMansionIcon.react.js @@ -1,315 +1,315 @@ // @flow import * as React from 'react'; import IcomoonReact from 'react-icomoon'; -import iconSet from 'lib/shared/swmansion-icon-config.json'; +import iconSet from '../shared/swmansion-icon-config.json'; /* To see all of the icons the application uses and what their names are: - Go to: https://icomoon.io/app/#/select - Click the import project button, upload the lib/shared/swmansion-icon-config.json file and click the load button. - All of the icons in the selected icons section are used in the app - To see the icon image mapped to the name go to https://icomoon.io/app/#/select/image after going through the steps above */ export type Icon = | 'air' | 'alarm' | 'arrow-circle-down' | 'arrow-circle-left' | 'arrow-circle-right' | 'arrow-circle-up' | 'arrow-down' | 'arrow-left' | 'arrow-right' | 'arrow-small-down' | 'arrow-small-left' | 'arrow-small-right' | 'arrow-small-up' | 'arrow-up' | 'at-email' | 'attachment' | 'basket' | 'basketball' | 'bell-disabled' | 'bell' | 'block-1' | 'block-2' | 'bolt' | 'bone-broken' | 'bone' | 'bookmark' | 'calendar-check' | 'calendar-clock' | 'calendar-cross' | 'calendar-edit' | 'calendar-link' | 'calendar-lock' | 'calendar-minus' | 'calendar-plus' | 'calendar-user' | 'calendar-warning' | 'calendar' | 'cam-disabled' | 'cam' | 'camera-disabled' | 'camera' | 'capsule' | 'cardiology' | 'cart-1' | 'cart-2' | 'cart-3' | 'cart-4' | 'cast' | 'chart-vertical' | 'chart' | 'check-circle' | 'check-small' | 'check' | 'chevron-circle-down' | 'chevron-circle-left' | 'chevron-circle-right' | 'chevron-circle-up' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-small-down' | 'chevron-small-left' | 'chevron-small-right' | 'chevron-small-up' | 'chevron-up' | 'circle' | 'clock' | 'cloud' | 'coin' | 'command' | 'copy' | 'creditcard' | 'cross-circle' | 'cross-small' | 'cross' | 'crown-1' | 'crown-2' | 'cut' | 'delete' | 'dislike' | 'dna' | 'document-check' | 'document-clean' | 'document-cross' | 'document-filled' | 'document-minus' | 'document-plus' | 'download' | 'edit-1' | 'edit-2' | 'edit-3' | 'edit-4' | 'emote-normal' | 'emote-sad' | 'emote-smile' | 'explore' | 'eye-closed' | 'eye-open' | 'faceid' | 'female' | 'filters-1' | 'filters-2' | 'filters-3' | 'flag-1' | 'flag-2' | 'flag-3' | 'forward' | 'fullscreen' | 'gift' | 'globe-1' | 'grid-2-horizontal' | 'grid-2-vertical' | 'grid-4' | 'headphones' | 'heart' | 'home-1' | 'home-2' | 'home-hospital' | 'horizontal' | 'hourglass' | 'ice' | 'image-1' | 'inbox' | 'info-circle' | 'info-small' | 'info' | 'key' | 'laptop' | 'like' | 'link' | 'list-center' | 'list-left' | 'list-pointers' | 'list-right' | 'location-1' | 'location-2' | 'location-med-1' | 'location-med-2' | 'lock-off' | 'lock-on' | 'login' | 'logout' | 'mail' | 'male' | 'map' | 'medkit' | 'meds' | 'menu-hamburger' | 'menu-horizontal' | 'menu-vertical' | 'message-circle-dots' | 'message-circle-lines' | 'message-circle' | 'message-square-dots' | 'message-square-lines' | 'message-square' | 'microphone-disabled' | 'microphone' | 'minpaper-plus' | 'minus-circle' | 'minus-small' | 'minus' | 'money' | 'moon' | 'music' | 'navigation' | 'newscreen' | 'next' | 'offer' | 'package' | 'pause' | 'phone-call' | 'phone-cross' | 'phone-down' | 'phone' | 'pin-1' | 'pin-2' | 'pinpaper-check' | 'pinpaper-cross' | 'pinpaper-filled' | 'pinpaper-minus' | 'play' | 'plus-circle' | 'plus-small' | 'plus' | 'power' | 'previous' | 'print' | 'question-circle' | 'question-small' | 'question' | 'quote' | 'redo-circle' | 'redo-small' | 'redo' | 'refresh-circle' | 'refresh-small' | 'refresh' | 'resize-circle-horizontal' | 'resize-circle-vertical' | 'resize-small-horizontal' | 'resize-small-vertical' | 'rewind' | 'rotate-circle-left' | 'rotate-circle-right' | 'rotate-left' | 'rotate-right' | 'rotate-small-left' | 'rotate-small-right' | 'save' | 'screen-disabled' | 'screen-share' | 'screen' | 'search' | 'send-1' | 'send-2' | 'settings' | 'share-1' | 'share-2' | 'shield-check' | 'shield-cross' | 'shield-empty' | 'shirt' | 'smartphone' | 'sound-0' | 'sound-1' | 'sound-2' | 'speaker-0' | 'speaker-1' | 'speaker-2' | 'speaker-cross' | 'speaker-disabled' | 'star-1' | 'star-2' | 'stop' | 'stopwatch' | 'suitcase' | 'sun' | 'syringe' | 'tag' | 'test-tube' | 'tooth' | 'trash-1' | 'trash-2' | 'trending-down' | 'trending-up' | 'trophy' | 'umbrella-1' | 'umbrella-2' | 'undo-circle' | 'undo-small' | 'undo' | 'upload' | 'user-1' | 'user-2' | 'user-check' | 'user-cross' | 'user-info' | 'user-minus' | 'user-plus' | 'user-question' | 'user-warning' | 'users-more' | 'users' | 'vertical' | 'virus' | 'wallet' | 'wand' | 'warning-circle' | 'warning-small' | 'warning' | 'waterdrop' | 'windows' | 'zoom-in' | 'zoom-out'; type SWMansionIconProps = { +icon: Icon, +size: number | string, +color?: string, +title?: string, +className?: string, +disableFill?: boolean, +removeInlineStyle?: boolean, }; const iconStyle = { stroke: 'none', }; function SWMansionIcon(props: SWMansionIconProps): React.Node { return ; } export default SWMansionIcon; diff --git a/lib/package.json b/lib/package.json index 7d2704118..eff2f4263 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,60 +1,61 @@ { "name": "lib", "version": "0.0.1", "type": "module", "private": true, "license": "BSD-3-Clause", "scripts": { "clean": "rm -rf node_modules/", "test": "jest" }, "devDependencies": { "@babel/core": "^7.13.14", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-object-rest-spread": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-transform-runtime": "^7.13.10", "@babel/preset-env": "^7.13.12", "@babel/preset-flow": "^7.13.13", "@babel/preset-react": "^7.13.13", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "babel-jest": "^26.6.3", "flow-bin": "^0.182.0", "flow-typed": "^3.2.1", "react-refresh": "^0.14.0" }, "dependencies": { "dateformat": "^3.0.3", "emoji-regex": "^10.2.1", "eth-ens-namehash": "^2.0.8", "ethers": "^5.7.2", "fast-json-stable-stringify": "^2.0.0", "file-type": "^12.3.0", "invariant": "^2.2.4", "just-clone": "^3.2.1", "lodash": "^4.17.21", "react": "18.1.0", + "react-icomoon": "^2.4.1", "react-redux": "^7.1.1", "reselect": "^4.0.0", "reselect-map": "^1.0.5", "simple-markdown": "^0.7.2", "string-hash": "^1.1.3", "tcomb": "^3.2.29", "siwe": "^1.1.6", "tinycolor2": "^1.4.1", "tokenize-text": "^1.1.3", "url-parse-lax": "^3.0.0", "util-inspect": "^0.1.8", "utils-copy-error": "^1.0.1", "wagmi": "^0.6.0" }, "jest": { "transform": { "\\.js$": "babel-jest" }, "transformIgnorePatterns": [ "/node_modules/(?!@babel/runtime)" ] } } diff --git a/web/account/connected-wallet-info.react.js b/web/account/connected-wallet-info.react.js index 5d2d6e9aa..ecc2a864a 100644 --- a/web/account/connected-wallet-info.react.js +++ b/web/account/connected-wallet-info.react.js @@ -1,70 +1,70 @@ // @flow import { emojiAvatarForAddress, useAccountModal } from '@rainbow-me/rainbowkit'; import * as React from 'react'; import { useAccount, useEnsAvatar } from 'wagmi'; +import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { useENSName } from 'lib/hooks/ens-cache.js'; -import SWMansionIcon from '../SWMansionIcon.react.js'; import css from './connected-wallet-info.css'; function shortenAddressToFitWidth(address: string): string { if (address.length < 22) { return address; } return `${address.slice(0, 10)}…${address.slice(-10)}`; } type RainbowKitEmojiAvatar = { +color: string, +emoji: string, }; function ConnectedWalletInfo(): React.Node { const { address } = useAccount(); const { openAccountModal } = useAccountModal(); const potentiallyENSName = useENSName(address); const emojiAvatar: RainbowKitEmojiAvatar = React.useMemo( () => emojiAvatarForAddress(address), [address], ); const emojiAvatarStyle = React.useMemo( () => ({ backgroundColor: emojiAvatar.color }), [emojiAvatar.color], ); const emojiAvatarView = React.useMemo(() =>

{emojiAvatar.emoji}

, [ emojiAvatar.emoji, ]); const { data: ensAvatarURI } = useEnsAvatar({ addressOrName: potentiallyENSName, }); const potentiallyENSAvatar = React.useMemo( () => , [ensAvatarURI], ); const onClick = React.useCallback(() => { openAccountModal && openAccountModal(); }, [openAccountModal]); return (
{ensAvatarURI ? potentiallyENSAvatar : emojiAvatarView}

{shortenAddressToFitWidth(potentiallyENSName)}

); } export default ConnectedWalletInfo; diff --git a/web/account/password-input.react.js b/web/account/password-input.react.js index 8c73db43e..f88b9b33f 100644 --- a/web/account/password-input.react.js +++ b/web/account/password-input.react.js @@ -1,44 +1,47 @@ // @flow import * as React from 'react'; +import SWMansionIcon, { + type Icon, +} from 'lib/components/SWMansionIcon.react.js'; + import Button from '../components/button.react'; import Input, { type BaseInputProps } from '../modals/input.react'; -import SWMansionIcon, { type Icon } from '../SWMansionIcon.react'; import css from './password-input.css'; type PasswordInputProps = BaseInputProps; function PasswordInput(props: PasswordInputProps, ref): React.Node { const [htmlInputType, setHtmlInputType] = React.useState<'password' | 'text'>( 'password', ); const onToggleShowPassword = React.useCallback(() => { setHtmlInputType(oldType => (oldType === 'password' ? 'text' : 'password')); }, []); const icon: Icon = htmlInputType === 'password' ? 'eye-open' : 'eye-closed'; return (
); } const ForwardedPasswordInput: React.AbstractComponent< PasswordInputProps, HTMLInputElement, > = React.forwardRef(PasswordInput); export default ForwardedPasswordInput; diff --git a/web/account/siwe-login-form.react.js b/web/account/siwe-login-form.react.js index e8af31ff3..2675e4ef6 100644 --- a/web/account/siwe-login-form.react.js +++ b/web/account/siwe-login-form.react.js @@ -1,178 +1,178 @@ // @flow import '@rainbow-me/rainbowkit/dist/index.css'; import olm from '@matrix-org/olm'; import invariant from 'invariant'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { useAccount, useSigner } from 'wagmi'; import { getSIWENonce, getSIWENonceActionTypes, siweAuth, siweAuthActionTypes, } from 'lib/actions/siwe-actions'; +import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import type { LogInStartingPayload } from 'lib/types/account-types.js'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import { createSIWEMessage, getSIWEStatementForPublicKey, siweMessageSigningExplanationStatements, } from 'lib/utils/siwe-utils.js'; import Button from '../components/button.react'; import OrBreak from '../components/or-break.react.js'; import LoadingIndicator from '../loading-indicator.react'; import { setPrimaryIdentityPublicKey } from '../redux/primary-identity-public-key-reducer'; import { useSelector } from '../redux/redux-utils'; import { webLogInExtraInfoSelector } from '../selectors/account-selectors.js'; -import SWMansionIcon from '../SWMansionIcon.react.js'; import ConnectedWalletInfo from './connected-wallet-info.react.js'; import HeaderSeparator from './header-separator.react.js'; import css from './siwe.css'; type SIWELoginFormProps = { +cancelSIWEAuthFlow: () => void, }; const getSIWENonceLoadingStatusSelector = createLoadingStatusSelector( getSIWENonceActionTypes, ); function SIWELoginForm(props: SIWELoginFormProps): React.Node { const { address } = useAccount(); const { data: signer } = useSigner(); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const getSIWENonceCall = useServerCall(getSIWENonce); const getSIWENonceCallLoadingStatus = useSelector( getSIWENonceLoadingStatusSelector, ); const siweAuthCall = useServerCall(siweAuth); const logInExtraInfo = useSelector(webLogInExtraInfoSelector); const [siweNonce, setSIWENonce] = React.useState(null); const siweNonceShouldBeFetched = !siweNonce && getSIWENonceCallLoadingStatus !== 'loading'; React.useEffect(() => { if (!siweNonceShouldBeFetched) { return; } dispatchActionPromise( getSIWENonceActionTypes, (async () => { const response = await getSIWENonceCall(); setSIWENonce(response); })(), ); }, [dispatchActionPromise, getSIWENonceCall, siweNonceShouldBeFetched]); const primaryIdentityPublicKey = useSelector( state => state.primaryIdentityPublicKey, ); React.useEffect(() => { (async () => { await olm.init(); const account = new olm.Account(); account.create(); const { ed25519 } = JSON.parse(account.identity_keys()); dispatch({ type: setPrimaryIdentityPublicKey, payload: ed25519, }); })(); }, [dispatch]); const callSIWEAuthEndpoint = React.useCallback( (message: string, signature: string, extraInfo) => siweAuthCall({ message, signature, ...extraInfo, }), [siweAuthCall], ); const attemptSIWEAuth = React.useCallback( (message: string, signature: string) => { const extraInfo = logInExtraInfo(); dispatchActionPromise( siweAuthActionTypes, callSIWEAuthEndpoint(message, signature, extraInfo), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); }, [callSIWEAuthEndpoint, dispatchActionPromise, logInExtraInfo], ); const onSignInButtonClick = React.useCallback(async () => { invariant(signer, 'signer must be present during SIWE attempt'); invariant(siweNonce, 'nonce must be present during SIWE attempt'); invariant( primaryIdentityPublicKey, 'primaryIdentityPublicKey must be present during SIWE attempt', ); const statement = getSIWEStatementForPublicKey(primaryIdentityPublicKey); const message = createSIWEMessage(address, statement, siweNonce); const signature = await signer.signMessage(message); attemptSIWEAuth(message, signature); }, [address, attemptSIWEAuth, primaryIdentityPublicKey, signer, siweNonce]); const { cancelSIWEAuthFlow } = props; const backButtonColor = React.useMemo( () => ({ backgroundColor: '#211E2D' }), [], ); if (!siweNonce || !primaryIdentityPublicKey) { return (
); } return (

Sign in with Ethereum

Wallet Connected

{siweMessageSigningExplanationStatements}

By signing up, you agree to our{' '} Terms of Use &{' '} Privacy Policy.

); } export default SIWELoginForm; diff --git a/web/apps/app-listing.react.js b/web/apps/app-listing.react.js index 44eb6b49b..e165d6b7e 100644 --- a/web/apps/app-listing.react.js +++ b/web/apps/app-listing.react.js @@ -1,81 +1,81 @@ // @flow import { faCheckCircle } from '@fortawesome/free-regular-svg-icons'; import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classnames from 'classnames'; import * as React from 'react'; import { useDispatch } from 'react-redux'; +import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { disableAppActionType, enableAppActionType, } from 'lib/reducers/enabled-apps-reducer'; import type { SupportedApps } from 'lib/types/enabled-apps'; import Button from '../components/button.react'; -import SWMansionIcon from '../SWMansionIcon.react'; import css from './apps.css'; type Props = { +id: SupportedApps | 'chat', +readOnly: boolean, +enabled: boolean, +name: string, +icon: 'message-square' | 'calendar', +copy: string, }; function AppListing(props: Props): React.Node { const { id, readOnly, enabled, name, icon, copy } = props; const dispatch = useDispatch(); const switchAppState = React.useCallback( () => dispatch({ type: enabled ? disableAppActionType : enableAppActionType, payload: id, }), [dispatch, enabled, id], ); const actionButton = React.useMemo(() => { const switchIcon = enabled ? faCheckCircle : faPlusCircle; if (readOnly) { const readOnlyIconClasses = classnames( css.appListingIcon, css.appListingIconState, css.iconReadOnly, ); return (
); } const iconClasses = classnames({ [css.appListingIconState]: true, [css.iconEnabled]: enabled, [css.iconDisabled]: !enabled, }); return ( ); }, [enabled, readOnly, switchAppState]); return (
{name}
{copy}
{actionButton}
); } export default AppListing; diff --git a/web/calendar/calendar.react.js b/web/calendar/calendar.react.js index 52fdcc144..5369db118 100644 --- a/web/calendar/calendar.react.js +++ b/web/calendar/calendar.react.js @@ -1,288 +1,288 @@ // @flow import { faFilter } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import dateFormat from 'dateformat'; import invariant from 'invariant'; import * as React from 'react'; import { updateCalendarQueryActionTypes, updateCalendarQuery, } from 'lib/actions/entry-actions'; +import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { currentDaysToEntries } from 'lib/selectors/thread-selectors'; import { isLoggedIn } from 'lib/selectors/user-selectors'; import { type EntryInfo, type CalendarQuery, type CalendarQueryUpdateResult, type CalendarQueryUpdateStartingPayload, } from 'lib/types/entry-types'; import { type DispatchActionPromise, useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import { getDate, dateString, startDateForYearAndMonth, endDateForYearAndMonth, } from 'lib/utils/date-utils'; import { useSelector } from '../redux/redux-utils'; import { yearAssertingSelector, monthAssertingSelector, webCalendarQuery, } from '../selectors/nav-selectors'; -import SWMansionIcon from '../SWMansionIcon.react'; import type { NavInfo } from '../types/nav-types'; import { canonicalURLFromReduxState } from '../url-utils'; import css from './calendar.css'; import Day from './day.react'; import FilterPanel from './filter-panel.react'; type BaseProps = { +url: string, }; type Props = { ...BaseProps, +year: number, +month: number, +daysToEntries: { +[dayString: string]: EntryInfo[] }, +navInfo: NavInfo, +currentCalendarQuery: () => CalendarQuery, +loggedIn: boolean, +dispatchActionPromise: DispatchActionPromise, +updateCalendarQuery: ( calendarQuery: CalendarQuery, reduxAlreadyUpdated?: boolean, ) => Promise, }; type State = { +filterPanelOpen: boolean, }; class Calendar extends React.PureComponent { state: State = { filterPanelOpen: false, }; getDate( dayOfMonth: number, monthInput: ?number = undefined, yearInput: ?number = undefined, ) { return getDate( yearInput ? yearInput : this.props.year, monthInput ? monthInput : this.props.month, dayOfMonth, ); } prevMonthDates() { const { year, month } = this.props; const lastMonthDate = getDate(year, month - 1, 1); const prevYear = lastMonthDate.getFullYear(); const prevMonth = lastMonthDate.getMonth() + 1; return { startDate: startDateForYearAndMonth(prevYear, prevMonth), endDate: endDateForYearAndMonth(prevYear, prevMonth), }; } nextMonthDates() { const { year, month } = this.props; const nextMonthDate = getDate(year, month + 1, 1); const nextYear = nextMonthDate.getFullYear(); const nextMonth = nextMonthDate.getMonth() + 1; return { startDate: startDateForYearAndMonth(nextYear, nextMonth), endDate: endDateForYearAndMonth(nextYear, nextMonth), }; } render() { const { year, month } = this.props; const monthName = dateFormat(getDate(year, month, 1), 'mmmm'); const prevURL = canonicalURLFromReduxState( { ...this.props.navInfo, ...this.prevMonthDates() }, this.props.url, this.props.loggedIn, ); const nextURL = canonicalURLFromReduxState( { ...this.props.navInfo, ...this.nextMonthDates() }, this.props.url, this.props.loggedIn, ); const lastDayOfMonth = this.getDate(0, this.props.month + 1); const totalDaysInMonth = lastDayOfMonth.getDate(); const firstDayToPrint = 1 - this.getDate(1).getDay(); const lastDayToPrint = totalDaysInMonth + 6 - lastDayOfMonth.getDay(); const rows = []; let columns = []; let week = 1; let tabIndex = 1; for ( let curDayOfMonth = firstDayToPrint; curDayOfMonth <= lastDayToPrint; curDayOfMonth++ ) { if (curDayOfMonth < 1 || curDayOfMonth > totalDaysInMonth) { columns.push(); } else { const dayString = dateString( this.props.year, this.props.month, curDayOfMonth, ); const entries = this.props.daysToEntries[dayString]; invariant( entries, 'the currentDaysToEntries selector should make sure all dayStrings ' + `in the current range have entries, but ${dayString} did not`, ); columns.push( , ); tabIndex += entries.length; } if (columns.length === 7) { rows.push({columns}); columns = []; } } let filterPanel = null; let calendarContentStyle = null; let filterButtonStyle = null; if (this.state.filterPanelOpen) { filterPanel = ; calendarContentStyle = { marginLeft: '300px' }; filterButtonStyle = { backgroundColor: 'rgba(0,0,0,0.67)' }; } return (
{filterPanel}
Filters
{rows}
Sunday Monday Tuesday Wednesday Thursday Friday Saturday
); } toggleFilters = (event: SyntheticEvent) => { event.preventDefault(); this.setState({ filterPanelOpen: !this.state.filterPanelOpen }); }; onClickPrevURL = (event: SyntheticEvent) => { event.preventDefault(); const currentCalendarQuery = this.props.currentCalendarQuery(); const newCalendarQuery = { ...currentCalendarQuery, ...this.prevMonthDates(), }; this.props.dispatchActionPromise( updateCalendarQueryActionTypes, this.props.updateCalendarQuery(newCalendarQuery, true), undefined, ({ calendarQuery: newCalendarQuery }: CalendarQueryUpdateStartingPayload), ); }; onClickNextURL = (event: SyntheticEvent) => { event.preventDefault(); const currentCalendarQuery = this.props.currentCalendarQuery(); const newCalendarQuery = { ...currentCalendarQuery, ...this.nextMonthDates(), }; this.props.dispatchActionPromise( updateCalendarQueryActionTypes, this.props.updateCalendarQuery(newCalendarQuery, true), undefined, ({ calendarQuery: newCalendarQuery }: CalendarQueryUpdateStartingPayload), ); }; } const ConnectedCalendar: React.ComponentType = React.memo( function ConnectedCalendar(props) { const year = useSelector(yearAssertingSelector); const month = useSelector(monthAssertingSelector); const daysToEntries = useSelector(currentDaysToEntries); const navInfo = useSelector(state => state.navInfo); const currentCalendarQuery = useSelector(webCalendarQuery); const loggedIn = useSelector(isLoggedIn); const callUpdateCalendarQuery = useServerCall(updateCalendarQuery); const dispatchActionPromise = useDispatchActionPromise(); return ( ); }, ); export default ConnectedCalendar; diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js index f39a788b8..4a71b61b2 100644 --- a/web/chat/chat-input-bar.react.js +++ b/web/chat/chat-input-bar.react.js @@ -1,640 +1,640 @@ // @flow import invariant from 'invariant'; import _difference from 'lodash/fp/difference'; import * as React from 'react'; import { joinThreadActionTypes, joinThread, newThreadActionTypes, } from 'lib/actions/thread-actions'; +import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { userStoreSearchIndex, relativeMemberInfoSelectorForMembersOfThread, } from 'lib/selectors/user-selectors'; import { localIDPrefix, trimMessage } from 'lib/shared/message-utils'; import { threadHasPermission, viewerIsMember, threadFrozenDueToViewerBlock, threadActualMembers, checkIfDefaultMembersAreVoiced, } from 'lib/shared/thread-utils'; import { getTypeaheadUserSuggestions } from 'lib/shared/typeahead-utils'; import type { CalendarQuery } from 'lib/types/entry-types'; import type { LoadingStatus } from 'lib/types/loading-types'; import { messageTypes } from 'lib/types/message-types'; import { type ThreadInfo, threadPermissions, type ClientThreadJoinRequest, type ThreadJoinPayload, } from 'lib/types/thread-types'; import type { RelativeMemberInfo } from 'lib/types/thread-types'; import { type UserInfos } from 'lib/types/user-types'; import { type DispatchActionPromise, useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import Button from '../components/button.react'; import { type InputState, type PendingMultimediaUpload, } from '../input/input-state'; import LoadingIndicator from '../loading-indicator.react'; import { allowedMimeTypeString } from '../media/file-utils'; import Multimedia from '../media/multimedia.react'; import { useSelector } from '../redux/redux-utils'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors'; -import SWMansionIcon from '../SWMansionIcon.react'; import { webTypeaheadRegex } from '../utils/typeahead-utils'; import css from './chat-input-bar.css'; import TypeaheadTooltip from './typeahead-tooltip.react'; type BaseProps = { +threadInfo: ThreadInfo, +inputState: InputState, }; type Props = { ...BaseProps, // Redux state +viewerID: ?string, +joinThreadLoadingStatus: LoadingStatus, +threadCreationInProgress: boolean, +calendarQuery: () => CalendarQuery, +nextLocalID: number, +isThreadActive: boolean, +userInfos: UserInfos, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +joinThread: (request: ClientThreadJoinRequest) => Promise, +typeaheadMatchedStrings: ?TypeaheadMatchedStrings, +suggestedUsers: $ReadOnlyArray, }; export type TypeaheadMatchedStrings = { +textBeforeAtSymbol: string, +usernamePrefix: string, }; class ChatInputBar extends React.PureComponent { textarea: ?HTMLTextAreaElement; multimediaInput: ?HTMLInputElement; componentDidMount() { this.updateHeight(); if (this.props.isThreadActive) { this.addReplyListener(); } } componentWillUnmount() { if (this.props.isThreadActive) { this.removeReplyListener(); } } componentDidUpdate(prevProps: Props) { if (this.props.isThreadActive && !prevProps.isThreadActive) { this.addReplyListener(); } else if (!this.props.isThreadActive && prevProps.isThreadActive) { this.removeReplyListener(); } const { inputState } = this.props; const prevInputState = prevProps.inputState; if (inputState.draft !== prevInputState.draft) { this.updateHeight(); } if ( inputState.draft !== prevInputState.draft || inputState.textCursorPosition !== prevInputState.textCursorPosition ) { inputState.setTypeaheadState({ canBeVisible: true, }); } const curUploadIDs = ChatInputBar.unassignedUploadIDs( inputState.pendingUploads, ); const prevUploadIDs = ChatInputBar.unassignedUploadIDs( prevInputState.pendingUploads, ); if ( this.multimediaInput && _difference(prevUploadIDs)(curUploadIDs).length > 0 ) { // Whenever a pending upload is removed, we reset the file // HTMLInputElement's value field, so that if the same upload occurs again // the onChange call doesn't get filtered this.multimediaInput.value = ''; } else if ( this.textarea && _difference(curUploadIDs)(prevUploadIDs).length > 0 ) { // Whenever a pending upload is added, we focus the textarea this.textarea.focus(); return; } if ( (this.props.threadInfo.id !== prevProps.threadInfo.id || (inputState.textCursorPosition !== prevInputState.textCursorPosition && this.textarea?.selectionStart === this.textarea?.selectionEnd)) && this.textarea ) { this.textarea.focus(); this.textarea?.setSelectionRange( inputState.textCursorPosition, inputState.textCursorPosition, 'none', ); } } static unassignedUploadIDs( pendingUploads: $ReadOnlyArray, ) { return pendingUploads .filter( (pendingUpload: PendingMultimediaUpload) => !pendingUpload.messageID, ) .map((pendingUpload: PendingMultimediaUpload) => pendingUpload.localID); } updateHeight() { const textarea = this.textarea; if (textarea) { textarea.style.height = 'auto'; const newHeight = Math.min(textarea.scrollHeight, 150); textarea.style.height = `${newHeight}px`; } } addReplyListener() { invariant( this.props.inputState, 'inputState should be set in addReplyListener', ); this.props.inputState.addReplyListener(this.focusAndUpdateText); } removeReplyListener() { invariant( this.props.inputState, 'inputState should be set in removeReplyListener', ); this.props.inputState.removeReplyListener(this.focusAndUpdateText); } render() { const isMember = viewerIsMember(this.props.threadInfo); const canJoin = threadHasPermission( this.props.threadInfo, threadPermissions.JOIN_THREAD, ); let joinButton = null; if (!isMember && canJoin && !this.props.threadCreationInProgress) { let buttonContent; if (this.props.joinThreadLoadingStatus === 'loading') { buttonContent = ( ); } else { buttonContent = ( <>

Join Chat

); } joinButton = (
); } const { pendingUploads, cancelPendingUpload } = this.props.inputState; const multimediaPreviews = pendingUploads.map(pendingUpload => ( )); const previews = multimediaPreviews.length > 0 ? (
{multimediaPreviews}
) : null; let content; // If the thread is created by somebody else while the viewer is attempting // to create it, the threadInfo might be modified in-place and won't // list the viewer as a member, which will end up hiding the input. In // this case, we will assume that our creation action will get translated, // into a join and as long as members are voiced, we can show the input. const defaultMembersAreVoiced = checkIfDefaultMembersAreVoiced( this.props.threadInfo, ); let sendButton; if (this.props.inputState.draft.length) { sendButton = ( ); } if ( threadHasPermission(this.props.threadInfo, threadPermissions.VOICED) || (this.props.threadCreationInProgress && defaultMembersAreVoiced) ) { content = (