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
Sign in using this wallet
Back to sign in with username
);
}
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
{' '}
{monthName} {year}{' '}
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
{rows}
);
}
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 = (
{buttonContent}
);
}
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 = (
);
} else if (
threadFrozenDueToViewerBlock(
this.props.threadInfo,
this.props.viewerID,
this.props.userInfos,
) &&
threadActualMembers(this.props.threadInfo.members).length === 2
) {
content = (
You can't send messages to a user that you've blocked.
);
} else if (isMember) {
content = (
You don't have permission to send messages.
);
} else if (defaultMembersAreVoiced && canJoin) {
content = null;
} else {
content = (
You don't have permission to send messages.
);
}
let typeaheadTooltip;
if (
this.props.inputState.typeaheadState.canBeVisible &&
this.props.suggestedUsers.length > 0 &&
this.props.typeaheadMatchedStrings &&
this.textarea
) {
typeaheadTooltip = (
);
}
return (
{joinButton}
{previews}
{content}
{typeaheadTooltip}
);
}
textareaRef = (textarea: ?HTMLTextAreaElement) => {
this.textarea = textarea;
if (textarea) {
textarea.focus();
}
};
onChangeMessageText = (event: SyntheticEvent) => {
this.props.inputState.setDraft(event.currentTarget.value);
this.props.inputState.setTextCursorPosition(
event.currentTarget.selectionStart,
);
};
onClickTextarea = (event: SyntheticEvent) => {
this.props.inputState.setTextCursorPosition(
event.currentTarget.selectionStart,
);
};
onSelectTextarea = (event: SyntheticEvent) => {
this.props.inputState.setTextCursorPosition(
event.currentTarget.selectionStart,
);
};
focusAndUpdateText = (text: string) => {
// We need to call focus() first on Safari, otherwise the cursor
// ends up at the start instead of the end for some reason
const { textarea } = this;
invariant(textarea, 'textarea should be set');
textarea.focus();
// We reset the textarea to an empty string at the start so that the cursor
// always ends up at the end, even if the text doesn't actually change
textarea.value = '';
const currentText = this.props.inputState.draft;
if (!currentText.startsWith(text)) {
const prependedText = text.concat(currentText);
this.props.inputState.setDraft(prependedText);
textarea.value = prependedText;
} else {
textarea.value = currentText;
}
// The above strategies make sure the cursor is at the end,
// but we also need to make sure that we're scrolled to the bottom
textarea.scrollTop = textarea.scrollHeight;
};
onKeyDown = (event: SyntheticKeyboardEvent) => {
const {
accept,
close,
moveChoiceUp,
moveChoiceDown,
} = this.props.inputState.typeaheadState;
const actions = {
Enter: accept,
Tab: accept,
ArrowDown: moveChoiceDown,
ArrowUp: moveChoiceUp,
Escape: close,
};
if (
this.props.inputState.typeaheadState.canBeVisible &&
actions[event.key]
) {
event.preventDefault();
actions[event.key]();
} else if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
this.send();
}
};
onSend = (event: SyntheticEvent) => {
event.preventDefault();
this.send();
};
send() {
let { nextLocalID } = this.props;
const text = trimMessage(this.props.inputState.draft);
if (text) {
this.dispatchTextMessageAction(text, nextLocalID);
nextLocalID++;
}
if (this.props.inputState.pendingUploads.length > 0) {
this.props.inputState.createMultimediaMessage(
nextLocalID,
this.props.threadInfo,
);
}
}
dispatchTextMessageAction(text: string, nextLocalID: number) {
this.props.inputState.setDraft('');
const localID = `${localIDPrefix}${nextLocalID}`;
const creatorID = this.props.viewerID;
invariant(creatorID, 'should have viewer ID in order to send a message');
this.props.inputState.sendTextMessage(
{
type: messageTypes.TEXT,
localID,
threadID: this.props.threadInfo.id,
text,
creatorID,
time: Date.now(),
},
this.props.threadInfo,
);
}
multimediaInputRef = (multimediaInput: ?HTMLInputElement) => {
this.multimediaInput = multimediaInput;
};
onMultimediaClick = () => {
if (this.multimediaInput) {
this.multimediaInput.click();
}
};
onMultimediaFileChange = async (
event: SyntheticInputEvent,
) => {
const result = await this.props.inputState.appendFiles([
...event.target.files,
]);
if (!result && this.multimediaInput) {
this.multimediaInput.value = '';
}
};
onClickJoin = () => {
this.props.dispatchActionPromise(joinThreadActionTypes, this.joinAction());
};
async joinAction() {
const query = this.props.calendarQuery();
return await this.props.joinThread({
threadID: this.props.threadInfo.id,
calendarQuery: {
startDate: query.startDate,
endDate: query.endDate,
filters: [
...query.filters,
{ type: 'threads', threadIDs: [this.props.threadInfo.id] },
],
},
});
}
}
const joinThreadLoadingStatusSelector = createLoadingStatusSelector(
joinThreadActionTypes,
);
const createThreadLoadingStatusSelector = createLoadingStatusSelector(
newThreadActionTypes,
);
const ConnectedChatInputBar: React.ComponentType = React.memo(
function ConnectedChatInputBar(props) {
const viewerID = useSelector(
state => state.currentUserInfo && state.currentUserInfo.id,
);
const nextLocalID = useSelector(state => state.nextLocalID);
const isThreadActive = useSelector(
state => props.threadInfo.id === state.navInfo.activeChatThreadID,
);
const userInfos = useSelector(state => state.userStore.userInfos);
const joinThreadLoadingStatus = useSelector(
joinThreadLoadingStatusSelector,
);
const createThreadLoadingStatus = useSelector(
createThreadLoadingStatusSelector,
);
const threadCreationInProgress = createThreadLoadingStatus === 'loading';
const calendarQuery = useSelector(nonThreadCalendarQuery);
const dispatchActionPromise = useDispatchActionPromise();
const callJoinThread = useServerCall(joinThread);
const userSearchIndex = useSelector(userStoreSearchIndex);
const threadMembers = useSelector(
relativeMemberInfoSelectorForMembersOfThread(props.threadInfo.id),
);
const inputSliceEndingAtCursor = React.useMemo(
() =>
props.inputState.draft.slice(0, props.inputState.textCursorPosition),
[props.inputState.draft, props.inputState.textCursorPosition],
);
// we only try to match if there is end of text or whitespace after cursor
const typeaheadRegexMatches = React.useMemo(
() =>
inputSliceEndingAtCursor.length === props.inputState.draft.length ||
/\s/.test(props.inputState.draft[props.inputState.textCursorPosition])
? inputSliceEndingAtCursor.match(webTypeaheadRegex)
: null,
[
inputSliceEndingAtCursor,
props.inputState.textCursorPosition,
props.inputState.draft,
],
);
const typeaheadMatchedStrings: ?TypeaheadMatchedStrings = React.useMemo(
() =>
typeaheadRegexMatches !== null
? {
textBeforeAtSymbol:
typeaheadRegexMatches.groups?.textPrefix ?? '',
usernamePrefix: typeaheadRegexMatches.groups?.username ?? '',
}
: null,
[typeaheadRegexMatches],
);
React.useEffect(() => {
if (props.inputState.typeaheadState.keepUpdatingThreadMembers) {
const setter = props.inputState.setTypeaheadState;
setter({
frozenThreadMembers: threadMembers,
});
}
}, [
threadMembers,
props.inputState.setTypeaheadState,
props.inputState.typeaheadState.keepUpdatingThreadMembers,
]);
const suggestedUsers: $ReadOnlyArray = React.useMemo(() => {
if (!typeaheadMatchedStrings) {
return [];
}
return getTypeaheadUserSuggestions(
userSearchIndex,
props.inputState.typeaheadState.frozenThreadMembers,
viewerID,
typeaheadMatchedStrings.usernamePrefix,
);
}, [
userSearchIndex,
props.inputState.typeaheadState.frozenThreadMembers,
viewerID,
typeaheadMatchedStrings,
]);
return (
);
},
);
export default ConnectedChatInputBar;
diff --git a/web/chat/chat-thread-ancestors.react.js b/web/chat/chat-thread-ancestors.react.js
index 79daeab90..f0eed104b 100644
--- a/web/chat/chat-thread-ancestors.react.js
+++ b/web/chat/chat-thread-ancestors.react.js
@@ -1,128 +1,128 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { useAncestorThreads } from 'lib/shared/ancestor-threads';
import { colorIsDark } from 'lib/shared/thread-utils';
import { useKeyserverAdmin } from 'lib/shared/user-utils';
import type { ThreadInfo } from 'lib/types/thread-types';
import { useResolvedThreadInfo } from 'lib/utils/entity-helpers';
import CommIcon from '../CommIcon.react';
-import SWMansionIcon from '../SWMansionIcon.react';
import css from './chat-thread-ancestors.css';
const SHOW_SEE_FULL_STRUCTURE = false;
type ThreadAncestorsProps = {
+threadInfo: ThreadInfo,
};
function ThreadAncestors(props: ThreadAncestorsProps): React.Node {
const { threadInfo } = props;
const { color: threadColor } = threadInfo;
const darkColor = colorIsDark(threadColor);
const threadColorStyle = React.useMemo(
() => ({
backgroundColor: `#${threadColor}`,
color: darkColor
? 'var(--thread-ancestor-color-light)'
: 'var(--thread-ancestor-color-dark)',
}),
[darkColor, threadColor],
);
const fullStructureButtonColorStyle = React.useMemo(
() => ({ color: `#${threadColor}` }),
[threadColor],
);
const ancestorThreads = useAncestorThreads(threadInfo);
const community = ancestorThreads[0] ?? threadInfo;
const keyserverAdmin = useKeyserverAdmin(community);
const keyserverOwnerUsername = keyserverAdmin?.username;
const resolvedCommunity = useResolvedThreadInfo(community);
const keyserverInfo = React.useMemo(
() => (
{keyserverOwnerUsername}
{resolvedCommunity.uiName}
),
[resolvedCommunity.uiName, keyserverOwnerUsername, threadColorStyle],
);
const middlePath = React.useMemo(() => {
if (ancestorThreads.length < 2) {
return null;
}
return (
<>
…
>
);
}, [ancestorThreads.length, threadColorStyle]);
const threadHasNoAncestors = community === threadInfo;
const { uiName } = useResolvedThreadInfo(threadInfo);
const currentThread = React.useMemo(() => {
if (threadHasNoAncestors) {
return null;
}
return (
<>
{uiName}
>
);
}, [threadHasNoAncestors, threadColorStyle, uiName]);
let seeFullStructure = null;
if (SHOW_SEE_FULL_STRUCTURE) {
seeFullStructure = (
See full structure
);
}
return (
<>
{keyserverInfo}
{middlePath}
{currentThread}
{seeFullStructure}
>
);
}
export default ThreadAncestors;
diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js
index 4ec04ce5d..a194ea6b8 100644
--- a/web/chat/chat-thread-composer.react.js
+++ b/web/chat/chat-thread-composer.react.js
@@ -1,186 +1,186 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import { useDispatch } from 'react-redux';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { useENSNames } from 'lib/hooks/ens-cache';
import { userSearchIndexForPotentialMembers } from 'lib/selectors/user-selectors';
import { getPotentialMemberItems } from 'lib/shared/search-utils';
import { threadIsPending } from 'lib/shared/thread-utils';
import type { AccountUserInfo, UserListItem } from 'lib/types/user-types';
import Button from '../components/button.react';
import Label from '../components/label.react';
import Search from '../components/search.react';
import type { InputState } from '../input/input-state';
import { updateNavInfoActionType } from '../redux/action-types';
import { useSelector } from '../redux/redux-utils';
-import SWMansionIcon from '../SWMansionIcon.react';
import css from './chat-thread-composer.css';
type Props = {
+userInfoInputArray: $ReadOnlyArray,
+otherUserInfos: { [id: string]: AccountUserInfo },
+threadID: string,
+inputState: InputState,
};
type ActiveThreadBehavior =
| 'reset-active-thread-if-pending'
| 'keep-active-thread';
function ChatThreadComposer(props: Props): React.Node {
const { userInfoInputArray, otherUserInfos, threadID, inputState } = props;
const [usernameInputText, setUsernameInputText] = React.useState('');
const dispatch = useDispatch();
const userSearchIndex = useSelector(userSearchIndexForPotentialMembers);
const userInfoInputIDs = React.useMemo(
() => userInfoInputArray.map(userInfo => userInfo.id),
[userInfoInputArray],
);
const userListItems = React.useMemo(
() =>
getPotentialMemberItems(
usernameInputText,
otherUserInfos,
userSearchIndex,
userInfoInputIDs,
),
[usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs],
);
const userListItemsWithENSNames = useENSNames(userListItems);
const onSelectUserFromSearch = React.useCallback(
(id: string) => {
const selectedUserIDs = userInfoInputArray.map(user => user.id);
dispatch({
type: updateNavInfoActionType,
payload: {
selectedUserList: [...selectedUserIDs, id],
},
});
setUsernameInputText('');
},
[dispatch, userInfoInputArray],
);
const onRemoveUserFromSelected = React.useCallback(
(id: string) => {
const selectedUserIDs = userInfoInputArray.map(user => user.id);
if (!selectedUserIDs.includes(id)) {
return;
}
dispatch({
type: updateNavInfoActionType,
payload: {
selectedUserList: selectedUserIDs.filter(userID => userID !== id),
},
});
},
[dispatch, userInfoInputArray],
);
const userSearchResultList = React.useMemo(() => {
if (
!userListItemsWithENSNames.length ||
(!usernameInputText && userInfoInputArray.length)
) {
return null;
}
return (
{userListItemsWithENSNames.map((userSearchResult: UserListItem) => (
onSelectUserFromSearch(userSearchResult.id)}
className={css.searchResultsButton}
>
{userSearchResult.username}
{userSearchResult.alertTitle}
))}
);
}, [
onSelectUserFromSearch,
userInfoInputArray.length,
userListItemsWithENSNames,
usernameInputText,
]);
const hideSearch = React.useCallback(
(threadBehavior: ActiveThreadBehavior = 'keep-active-thread') => {
dispatch({
type: updateNavInfoActionType,
payload: {
chatMode: 'view',
activeChatThreadID:
threadBehavior === 'keep-active-thread' ||
!threadIsPending(threadID)
? threadID
: null,
},
});
},
[dispatch, threadID],
);
const onCloseSearch = React.useCallback(() => {
hideSearch('reset-active-thread-if-pending');
}, [hideSearch]);
const userInfoInputArrayWithENSNames = useENSNames(userInfoInputArray);
const tagsList = React.useMemo(() => {
if (!userInfoInputArrayWithENSNames?.length) {
return null;
}
const labels = userInfoInputArrayWithENSNames.map(user => {
return (
onRemoveUserFromSelected(user.id)}>
{user.username}
);
});
return {labels}
;
}, [userInfoInputArrayWithENSNames, onRemoveUserFromSelected]);
React.useEffect(() => {
if (!inputState) {
return;
}
inputState.registerSendCallback(hideSearch);
return () => inputState.unregisterSendCallback(hideSearch);
}, [hideSearch, inputState]);
const threadSearchContainerStyles = classNames(css.threadSearchContainer, {
[css.fullHeight]: !userInfoInputArray.length,
});
return (
{tagsList}
{userSearchResultList}
);
}
export default ChatThreadComposer;
diff --git a/web/chat/chat-thread-list-item-menu.react.js b/web/chat/chat-thread-list-item-menu.react.js
index 9902eae30..8722601ed 100644
--- a/web/chat/chat-thread-list-item-menu.react.js
+++ b/web/chat/chat-thread-list-item-menu.react.js
@@ -1,76 +1,76 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import useToggleUnreadStatus from 'lib/hooks/toggle-unread-status';
import type { ThreadInfo } from 'lib/types/thread-types';
import Button from '../components/button.react';
import { useThreadIsActive } from '../selectors/thread-selectors';
-import SWMansionIcon from '../SWMansionIcon.react';
import css from './chat-thread-list-item-menu.css';
type Props = {
+threadInfo: ThreadInfo,
+mostRecentNonLocalMessage: ?string,
+renderStyle?: 'chat' | 'thread',
};
function ChatThreadListItemMenu(props: Props): React.Node {
const { renderStyle = 'chat', threadInfo, mostRecentNonLocalMessage } = props;
const active = useThreadIsActive(threadInfo.id);
const [menuVisible, setMenuVisible] = React.useState(false);
const toggleMenu = React.useCallback(
event => {
event.stopPropagation();
setMenuVisible(!menuVisible);
},
[menuVisible],
);
const hideMenu = React.useCallback(() => {
setMenuVisible(false);
}, []);
const toggleUnreadStatus = useToggleUnreadStatus(
threadInfo,
mostRecentNonLocalMessage,
hideMenu,
);
const onToggleUnreadStatusClicked = React.useCallback(
event => {
event.stopPropagation();
toggleUnreadStatus();
},
[toggleUnreadStatus],
);
const toggleUnreadStatusButtonText = `Mark as ${
threadInfo.currentUser.unread ? 'read' : 'unread'
}`;
const menuIconSize = renderStyle === 'chat' ? 24 : 20;
const menuCls = classNames(css.menu, {
[css.menuSidebar]: renderStyle === 'thread',
});
const btnCls = classNames(css.menuContent, {
[css.menuContentVisible]: menuVisible,
[css.active]: active,
});
return (
{toggleUnreadStatusButtonText}
);
}
export default ChatThreadListItemMenu;
diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js
index eef652dac..9e0aafca8 100644
--- a/web/chat/chat-thread-list-item.react.js
+++ b/web/chat/chat-thread-list-item.react.js
@@ -1,165 +1,165 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors';
import { useAncestorThreads } from 'lib/shared/ancestor-threads';
import { shortAbsoluteDate } from 'lib/utils/date-utils';
import {
useResolvedThreadInfo,
useResolvedThreadInfos,
} from 'lib/utils/entity-helpers';
import { useSelector } from '../redux/redux-utils';
import {
useOnClickThread,
useThreadIsActive,
} from '../selectors/thread-selectors';
-import SWMansionIcon from '../SWMansionIcon.react';
import ChatThreadListItemMenu from './chat-thread-list-item-menu.react';
import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react';
import ChatThreadListSidebar from './chat-thread-list-sidebar.react';
import css from './chat-thread-list.css';
import MessagePreview from './message-preview.react';
type Props = {
+item: ChatThreadItem,
};
function ChatThreadListItem(props: Props): React.Node {
const { item } = props;
const {
threadInfo,
lastUpdatedTimeIncludingSidebars,
mostRecentNonLocalMessage,
mostRecentMessageInfo,
} = item;
const { id: threadID, currentUser } = threadInfo;
const unresolvedAncestorThreads = useAncestorThreads(threadInfo);
const ancestorThreads = useResolvedThreadInfos(unresolvedAncestorThreads);
const lastActivity = shortAbsoluteDate(lastUpdatedTimeIncludingSidebars);
const active = useThreadIsActive(threadID);
const isCreateMode = useSelector(
state => state.navInfo.chatMode === 'create',
);
const onClick = useOnClickThread(item.threadInfo);
const selectItemIfNotActiveCreation = React.useCallback(
(event: SyntheticEvent) => {
if (!isCreateMode || !active) {
onClick(event);
}
},
[isCreateMode, active, onClick],
);
const containerClassName = classNames({
[css.thread]: true,
[css.activeThread]: active,
});
const { unread } = currentUser;
const titleClassName = classNames({
[css.title]: true,
[css.unread]: unread,
});
const lastActivityClassName = classNames({
[css.lastActivity]: true,
[css.unread]: unread,
[css.dark]: !unread,
});
const breadCrumbsClassName = classNames(css.breadCrumbs, {
[css.unread]: unread,
});
let unreadDot;
if (unread) {
unreadDot =
;
}
const { color } = item.threadInfo;
const colorSplotchStyle = React.useMemo(
() => ({ backgroundColor: `#${color}` }),
[color],
);
const sidebars = item.sidebars.map((sidebarItem, index) => {
if (sidebarItem.type === 'sidebar') {
const { type, ...sidebarInfo } = sidebarItem;
return (
0}
key={sidebarInfo.threadInfo.id}
/>
);
} else if (sidebarItem.type === 'seeMore') {
return (
);
} else {
return
;
}
});
const ancestorPath = ancestorThreads.map((thread, idx) => {
const isNotLast = idx !== ancestorThreads.length - 1;
const chevron = isNotLast && (
);
return (
{thread.uiName}
{chevron}
);
});
const { uiName } = useResolvedThreadInfo(threadInfo);
return (
<>
{sidebars}
>
);
}
export default ChatThreadListItem;
diff --git a/web/chat/chat-thread-tab.react.js b/web/chat/chat-thread-tab.react.js
index 3bbc11555..020b2ea90 100644
--- a/web/chat/chat-thread-tab.react.js
+++ b/web/chat/chat-thread-tab.react.js
@@ -1,23 +1,26 @@
// @flow
import * as React from 'react';
-import SWMansionIcon, { type Icon } from '../SWMansionIcon.react';
+import SWMansionIcon, {
+ type Icon,
+} from 'lib/components/SWMansionIcon.react.js';
+
import css from './chat-tabs.css';
type Props = {
+title: string,
+icon: Icon,
};
function ChatThreadTab(props: Props): React.Node {
const { title, icon } = props;
return (
{title}
);
}
export default ChatThreadTab;
diff --git a/web/chat/thread-menu.react.js b/web/chat/thread-menu.react.js
index 9859933e8..61b860274 100644
--- a/web/chat/thread-menu.react.js
+++ b/web/chat/thread-menu.react.js
@@ -1,291 +1,291 @@
// @flow
import * as React from 'react';
import {
leaveThread,
leaveThreadActionTypes,
} from 'lib/actions/thread-actions';
import { useModalContext } from 'lib/components/modal-provider.react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { usePromoteSidebar } from 'lib/hooks/promote-sidebar.react';
import { childThreadInfos } from 'lib/selectors/thread-selectors';
import {
threadHasPermission,
viewerIsMember,
threadIsChannel,
} from 'lib/shared/thread-utils';
import {
type ThreadInfo,
threadTypes,
threadPermissions,
} from 'lib/types/thread-types';
import {
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-utils';
import MenuItem from '../components/menu-item.react';
import Menu from '../components/menu.react';
import SidebarPromoteModal from '../modals/chat/sidebar-promote-modal.react';
import ConfirmLeaveThreadModal from '../modals/threads/confirm-leave-thread-modal.react';
import ComposeSubchannelModal from '../modals/threads/create/compose-subchannel-modal.react';
import ThreadMembersModal from '../modals/threads/members/members-modal.react';
import ThreadNotificationsModal from '../modals/threads/notifications/notifications-modal.react';
import ThreadSettingsModal from '../modals/threads/settings/thread-settings-modal.react';
import SidebarsModal from '../modals/threads/sidebars/sidebars-modal.react';
import SubchannelsModal from '../modals/threads/subchannels/subchannels-modal.react';
import { useSelector } from '../redux/redux-utils';
-import SWMansionIcon from '../SWMansionIcon.react';
import css from './thread-menu.css';
type ThreadMenuProps = {
+threadInfo: ThreadInfo,
};
function ThreadMenu(props: ThreadMenuProps): React.Node {
const { pushModal, popModal } = useModalContext();
const { threadInfo } = props;
const { onPromoteSidebar, canPromoteSidebar } = usePromoteSidebar(threadInfo);
const onClickSettings = React.useCallback(
() => pushModal( ),
[pushModal, threadInfo.id],
);
const settingsItem = React.useMemo(() => {
return (
);
}, [onClickSettings]);
const onClickMembers = React.useCallback(
() =>
pushModal(
,
),
[popModal, pushModal, threadInfo.id],
);
const membersItem = React.useMemo(() => {
if (threadInfo.type === threadTypes.PERSONAL) {
return null;
}
return (
);
}, [onClickMembers, threadInfo.type]);
const childThreads = useSelector(
state => childThreadInfos(state)[threadInfo.id],
);
const hasSidebars = React.useMemo(() => {
return childThreads?.some(
childThreadInfo => childThreadInfo.type === threadTypes.SIDEBAR,
);
}, [childThreads]);
const onClickSidebars = React.useCallback(
() =>
pushModal( ),
[popModal, pushModal, threadInfo.id],
);
const sidebarItem = React.useMemo(() => {
if (!hasSidebars) {
return null;
}
return (
);
}, [hasSidebars, onClickSidebars]);
const canCreateSubchannels = React.useMemo(
() => threadHasPermission(threadInfo, threadPermissions.CREATE_SUBCHANNELS),
[threadInfo],
);
const hasSubchannels = React.useMemo(() => {
return !!childThreads?.some(threadIsChannel);
}, [childThreads]);
const onClickViewSubchannels = React.useCallback(
() =>
pushModal(
,
),
[popModal, pushModal, threadInfo.id],
);
const viewSubchannelsItem = React.useMemo(() => {
if (!hasSubchannels) {
return null;
}
return (
);
}, [hasSubchannels, onClickViewSubchannels]);
const onClickCreateSubchannel = React.useCallback(
() =>
pushModal(
,
),
[popModal, pushModal, threadInfo],
);
const createSubchannelsItem = React.useMemo(() => {
if (!canCreateSubchannels) {
return null;
}
return (
);
}, [canCreateSubchannels, onClickCreateSubchannel]);
const dispatchActionPromise = useDispatchActionPromise();
const callLeaveThread = useServerCall(leaveThread);
const onConfirmLeaveThread = React.useCallback(() => {
dispatchActionPromise(
leaveThreadActionTypes,
callLeaveThread(threadInfo.id),
);
popModal();
}, [callLeaveThread, popModal, dispatchActionPromise, threadInfo.id]);
const onClickLeaveThread = React.useCallback(
() =>
pushModal(
,
),
[popModal, onConfirmLeaveThread, pushModal, threadInfo],
);
const leaveThreadItem = React.useMemo(() => {
const canLeaveThread = threadHasPermission(
threadInfo,
threadPermissions.LEAVE_THREAD,
);
if (!viewerIsMember(threadInfo) || !canLeaveThread) {
return null;
}
return (
);
}, [onClickLeaveThread, threadInfo]);
const onClickPromoteSidebarToThread = React.useCallback(
() =>
pushModal(
,
),
[pushModal, threadInfo, popModal, onPromoteSidebar],
);
const promoteSidebar = React.useMemo(() => {
return (
);
}, [onClickPromoteSidebarToThread]);
const onClickNotifications = React.useCallback(() => {
pushModal(
,
);
}, [popModal, pushModal, threadInfo.id]);
const notificationsItem = React.useMemo(() => {
if (!viewerIsMember(threadInfo)) {
return null;
}
return (
);
}, [onClickNotifications, threadInfo]);
const menuItems = React.useMemo(() => {
const separator = ;
const items = [
settingsItem,
notificationsItem,
membersItem,
sidebarItem,
viewSubchannelsItem,
createSubchannelsItem,
leaveThreadItem && separator,
canPromoteSidebar && promoteSidebar,
leaveThreadItem,
];
return items.filter(Boolean);
}, [
settingsItem,
notificationsItem,
membersItem,
sidebarItem,
viewSubchannelsItem,
promoteSidebar,
createSubchannelsItem,
leaveThreadItem,
canPromoteSidebar,
]);
const icon = React.useMemo(
() => ,
[],
);
return {menuItems} ;
}
export default ThreadMenu;
diff --git a/web/components/clear-search-button.react.js b/web/components/clear-search-button.react.js
index 18f1f564e..100878d92 100644
--- a/web/components/clear-search-button.react.js
+++ b/web/components/clear-search-button.react.js
@@ -1,27 +1,28 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
-import SWMansionIcon from '../SWMansionIcon.react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
+
import Button from './button.react';
import css from './search.css';
type ClearSearchButtonProps = {
+active: boolean,
+onClick: () => void,
};
function ClearSearchButton(props: ClearSearchButtonProps): React.Node {
const { active, onClick } = props;
const searchClassNames = classNames(css.clearSearch, {
[css.clearSearchDisabled]: !active,
});
return (
);
}
export default ClearSearchButton;
diff --git a/web/components/enum-settings-option-info.react.js b/web/components/enum-settings-option-info.react.js
index 088785b6f..5c5897240 100644
--- a/web/components/enum-settings-option-info.react.js
+++ b/web/components/enum-settings-option-info.react.js
@@ -1,45 +1,46 @@
// @flow
import classnames from 'classnames';
import * as React from 'react';
-import SWMansionIcon from '../SWMansionIcon.react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
+
import css from './enum-settings-option-info.css';
type Props = {
+optionSelected: boolean,
+valid: boolean,
+styleStatementBasedOnValidity: boolean,
+children: React.Node,
};
function EnumSettingsOptionInfo(props: Props): React.Node {
const {
optionSelected,
valid,
styleStatementBasedOnValidity,
children,
} = props;
const optionInfoClasses = classnames({
[css.optionInfo]: true,
[css.optionInfoInvalid]: styleStatementBasedOnValidity && !valid,
[css.optionInfoInvalidSelected]:
styleStatementBasedOnValidity && !valid && optionSelected,
});
const icon = React.useMemo(() => {
if (!styleStatementBasedOnValidity) {
return null;
}
return ;
}, [styleStatementBasedOnValidity, valid]);
return (
{icon}
{children}
);
}
export default EnumSettingsOptionInfo;
diff --git a/web/components/label.react.js b/web/components/label.react.js
index 7a8549ca0..dafbcb721 100644
--- a/web/components/label.react.js
+++ b/web/components/label.react.js
@@ -1,54 +1,55 @@
// @flow
import * as React from 'react';
-import SWMansionIcon from '../SWMansionIcon.react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
+
import Button from './button.react';
import css from './label.css';
type Props = {
+size?: string | number,
+color?: string,
+bg?: string,
+children: React.Node,
+onClose?: () => mixed,
};
function Label(props: Props): React.Node {
const {
size = '12px',
color = 'var(--label-default-color)',
bg = 'var(--label-default-bg)',
children,
onClose,
} = props;
const labelStyle = React.useMemo(
() => ({
fontSize: size,
color: color,
background: bg,
}),
[bg, color, size],
);
const closeButton = React.useMemo(() => {
if (!onClose) {
return null;
}
return (
);
}, [onClose, size]);
return (
{children}
{closeButton}
);
}
export default Label;
diff --git a/web/components/menu-item.react.js b/web/components/menu-item.react.js
index d93abbfb7..4dd7ae367 100644
--- a/web/components/menu-item.react.js
+++ b/web/components/menu-item.react.js
@@ -1,38 +1,41 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
-import SWMansionIcon, { type Icon } from '../SWMansionIcon.react';
+import SWMansionIcon, {
+ type Icon,
+} from 'lib/components/SWMansionIcon.react.js';
+
import Button from './button.react';
import css from './menu.css';
type MenuItemProps = {
+onClick?: () => mixed,
+icon: Icon,
+text: string,
+dangerous?: boolean,
};
function MenuItem(props: MenuItemProps): React.Node {
const { onClick, icon, text, dangerous } = props;
const itemClasses = classNames(css.menuAction, {
[css.menuActionDangerous]: dangerous,
});
return (
{text}
);
}
const MemoizedMenuItem: React.ComponentType = React.memo(
MenuItem,
);
export default MemoizedMenuItem;
diff --git a/web/components/navigation-arrows.react.js b/web/components/navigation-arrows.react.js
index 7f620e8d2..1355b6c72 100644
--- a/web/components/navigation-arrows.react.js
+++ b/web/components/navigation-arrows.react.js
@@ -1,60 +1,61 @@
// @flow
import classnames from 'classnames';
import * as React from 'react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
+
import electron from '../electron.js';
import history from '../router-history.js';
-import SWMansionIcon from '../SWMansionIcon.react.js';
import css from './navigation-arrows.css';
const stopDoubleClickPropagation = e => e.stopPropagation();
function NavigationArrows(): React.Node {
const goBack = React.useCallback(
() => history.getHistoryObject().goBack(),
[],
);
const goForward = React.useCallback(
() => history.getHistoryObject().goForward(),
[],
);
const [disableBack, setDisableBack] = React.useState(false);
const [disableFoward, setDisableForward] = React.useState(false);
React.useEffect(
() =>
electron?.onNavigate(({ canGoBack, canGoForward }) => {
setDisableBack(!canGoBack);
setDisableForward(!canGoForward);
}),
[],
);
const goBackClasses = classnames(css.button, { [css.disabled]: disableBack });
const goForwardClasses = classnames(css.button, {
[css.disabled]: disableFoward,
});
return (
);
}
export default NavigationArrows;
diff --git a/web/components/search.react.js b/web/components/search.react.js
index 220f9a8b7..27ab8c432 100644
--- a/web/components/search.react.js
+++ b/web/components/search.react.js
@@ -1,54 +1,55 @@
// @flow
import * as React from 'react';
-import SWMansionIcon from '../SWMansionIcon.react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
+
import ClearSearchButton from './clear-search-button.react';
import css from './search.css';
type Props = {
+searchText: string,
+onChangeText: (searchText: string) => mixed,
+placeholder?: string,
};
function Search(props: Props, ref): React.Node {
const { searchText, onChangeText, placeholder } = props;
const showClearButton = !!searchText;
const onClear = React.useCallback(() => {
onChangeText('');
}, [onChangeText]);
const onChange = React.useCallback(
event => {
onChangeText(event.target.value);
},
[onChangeText],
);
return (
);
}
const ForwardedSearch: React.AbstractComponent<
Props,
HTMLInputElement,
> = React.forwardRef(Search);
export default ForwardedSearch;
diff --git a/web/modals/modal.react.js b/web/modals/modal.react.js
index c40798bb3..388bc16fa 100644
--- a/web/modals/modal.react.js
+++ b/web/modals/modal.react.js
@@ -1,83 +1,85 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import ModalOverlay from 'lib/components/modal-overlay.react';
+import SWMansionIcon, {
+ type Icon,
+} from 'lib/components/SWMansionIcon.react.js';
import Button from '../components/button.react';
-import SWMansionIcon, { type Icon } from '../SWMansionIcon.react';
import css from './modal.css';
export type ModalSize = 'small' | 'large' | 'fit-content';
export type ModalOverridableProps = {
+name: string,
+icon?: Icon,
+onClose: () => void,
+withCloseButton?: boolean,
+size?: ModalSize,
+modalHeaderCentered?: boolean,
};
type ModalProps = {
...ModalOverridableProps,
+children?: React.Node,
};
function Modal(props: ModalProps): React.Node {
const {
size = 'small',
children,
onClose,
name,
icon,
withCloseButton = true,
modalHeaderCentered = false,
} = props;
const modalContainerClasses = classNames(css.modalContainer, {
[css.modalContainerLarge]: size === 'large',
[css.modalContainerSmall]: size === 'small',
});
const modalHeader = classNames({
[css.modalHeader]: true,
[css.modalHeaderCentered]: modalHeaderCentered,
});
const cornerCloseButton = React.useMemo(() => {
if (!withCloseButton) {
return null;
}
return (
);
}, [onClose, withCloseButton]);
const headerIcon = React.useMemo(() => {
if (!icon) {
return null;
}
return ;
}, [icon]);
return (
{headerIcon}
{name}
{cornerCloseButton}
{children}
);
}
export default Modal;
diff --git a/web/modals/threads/create/steps/subchannel-settings.react.js b/web/modals/threads/create/steps/subchannel-settings.react.js
index ea9a3599c..c7edf5e4f 100644
--- a/web/modals/threads/create/steps/subchannel-settings.react.js
+++ b/web/modals/threads/create/steps/subchannel-settings.react.js
@@ -1,123 +1,123 @@
// @flow
import * as React from 'react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { threadTypeDescriptions } from 'lib/shared/thread-utils';
import { threadTypes } from 'lib/types/thread-types';
import CommIcon from '../../../../CommIcon.react';
import EnumSettingsOption from '../../../../components/enum-settings-option.react';
-import SWMansionIcon from '../../../../SWMansionIcon.react';
import Input from '../../../input.react';
import css from './subchannel-settings.css';
const { COMMUNITY_OPEN_SUBTHREAD, COMMUNITY_SECRET_SUBTHREAD } = threadTypes;
const openStatements = [
{
statement: threadTypeDescriptions[COMMUNITY_OPEN_SUBTHREAD],
isStatementValid: true,
styleStatementBasedOnValidity: false,
},
];
const secretStatements = [
{
statement: threadTypeDescriptions[COMMUNITY_SECRET_SUBTHREAD],
isStatementValid: true,
styleStatementBasedOnValidity: false,
},
];
const announcementStatements = [
{
statement: 'Admins can create Announcement channels.',
isStatementValid: true,
styleStatementBasedOnValidity: false,
},
];
export type VisibilityType = 'open' | 'secret';
type Props = {
+channelName: string,
+onChangeChannelName: (SyntheticEvent) => void,
+visibilityType: VisibilityType,
+onOpenTypeSelect: () => void,
+onSecretTypeSelect: () => void,
+announcement: boolean,
+onAnnouncementSelected: () => void,
};
function SubchannelSettings(props: Props): React.Node {
const {
channelName,
onChangeChannelName,
visibilityType,
onOpenTypeSelect,
onSecretTypeSelect,
announcement,
onAnnouncementSelected,
} = props;
const globeIcon = React.useMemo(
() => ,
[],
);
const lockIcon = React.useMemo(
() => ,
[],
);
const flagIcon = React.useMemo(
() => ,
[],
);
return (
<>
>
);
}
export default SubchannelSettings;
diff --git a/web/modals/threads/members/member.react.js b/web/modals/threads/members/member.react.js
index 0a8fe160d..d229b07c9 100644
--- a/web/modals/threads/members/member.react.js
+++ b/web/modals/threads/members/member.react.js
@@ -1,166 +1,166 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import {
removeUsersFromThread,
changeThreadMemberRoles,
} from 'lib/actions/thread-actions';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import {
memberIsAdmin,
memberHasAdminPowers,
removeMemberFromThread,
switchMemberAdminRoleInThread,
getAvailableThreadMemberActions,
} from 'lib/shared/thread-utils';
import { stringForUser } from 'lib/shared/user-utils';
import type { SetState } from 'lib/types/hook-types';
import {
type RelativeMemberInfo,
type ThreadInfo,
} from 'lib/types/thread-types';
import {
useDispatchActionPromise,
useServerCall,
} from 'lib/utils/action-utils';
import Label from '../../../components/label.react';
import MenuItem from '../../../components/menu-item.react';
import Menu from '../../../components/menu.react';
-import SWMansionIcon from '../../../SWMansionIcon.react';
import css from './members-modal.css';
type Props = {
+memberInfo: RelativeMemberInfo,
+threadInfo: ThreadInfo,
+setOpenMenu: SetState,
+isMenuOpen: boolean,
};
function ThreadMember(props: Props): React.Node {
const { memberInfo, threadInfo, setOpenMenu, isMenuOpen } = props;
const userName = stringForUser(memberInfo);
const onMenuChange = React.useCallback(
menuOpen => {
if (menuOpen) {
setOpenMenu(() => memberInfo.id);
} else {
setOpenMenu(menu => (menu === memberInfo.id ? null : menu));
}
},
[memberInfo.id, setOpenMenu],
);
const dispatchActionPromise = useDispatchActionPromise();
const boundRemoveUsersFromThread = useServerCall(removeUsersFromThread);
const onClickRemoveUser = React.useCallback(
() =>
removeMemberFromThread(
threadInfo,
memberInfo,
dispatchActionPromise,
boundRemoveUsersFromThread,
),
[boundRemoveUsersFromThread, dispatchActionPromise, memberInfo, threadInfo],
);
const isCurrentlyAdmin = memberIsAdmin(memberInfo, threadInfo);
const boundChangeThreadMemberRoles = useServerCall(changeThreadMemberRoles);
const onMemberAdminRoleToggled = React.useCallback(
() =>
switchMemberAdminRoleInThread(
threadInfo,
memberInfo,
isCurrentlyAdmin,
dispatchActionPromise,
boundChangeThreadMemberRoles,
),
[
boundChangeThreadMemberRoles,
dispatchActionPromise,
isCurrentlyAdmin,
memberInfo,
threadInfo,
],
);
const menuItems = React.useMemo(
() =>
getAvailableThreadMemberActions(memberInfo, threadInfo).map(action => {
if (action === 'remove_admin') {
return (
);
}
if (action === 'make_admin') {
return (
);
}
if (action === 'remove_user') {
return (
);
}
return null;
}),
[memberInfo, onClickRemoveUser, onMemberAdminRoleToggled, threadInfo],
);
const userSettingsIcon = React.useMemo(
() => ,
[],
);
const label = React.useMemo(() => {
if (memberIsAdmin(memberInfo, threadInfo)) {
return Admin ;
} else if (memberHasAdminPowers(memberInfo)) {
return Parent admin ;
}
return null;
}, [memberInfo, threadInfo]);
const memberContainerClasses = classNames(css.memberContainer, {
[css.memberContainerWithMenuOpen]: isMenuOpen,
});
return (
{userName} {label}
{menuItems}
);
}
export default ThreadMember;
diff --git a/web/modals/threads/settings/thread-settings-delete-tab.react.js b/web/modals/threads/settings/thread-settings-delete-tab.react.js
index f1136d17e..f642995ee 100644
--- a/web/modals/threads/settings/thread-settings-delete-tab.react.js
+++ b/web/modals/threads/settings/thread-settings-delete-tab.react.js
@@ -1,95 +1,95 @@
// @flow
import * as React from 'react';
import {
deleteThreadActionTypes,
deleteThread,
} from 'lib/actions/thread-actions';
import { useModalContext } from 'lib/components/modal-provider.react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { type SetState } from 'lib/types/hook-types';
import { type ThreadInfo } from 'lib/types/thread-types';
import {
useDispatchActionPromise,
useServerCall,
} from 'lib/utils/action-utils';
import { buttonThemes } from '../../../components/button.react';
-import SWMansionIcon from '../../../SWMansionIcon.react';
import SubmitSection from './submit-section.react';
import css from './thread-settings-delete-tab.css';
type ThreadSettingsDeleteTabProps = {
+threadSettingsOperationInProgress: boolean,
+threadInfo: ThreadInfo,
+setErrorMessage: SetState,
+errorMessage?: ?string,
};
function ThreadSettingsDeleteTab(
props: ThreadSettingsDeleteTabProps,
): React.Node {
const {
threadSettingsOperationInProgress,
threadInfo,
setErrorMessage,
errorMessage,
} = props;
const modalContext = useModalContext();
const dispatchActionPromise = useDispatchActionPromise();
const callDeleteThread = useServerCall(deleteThread);
const deleteThreadAction = React.useCallback(async () => {
try {
setErrorMessage('');
const response = await callDeleteThread(threadInfo.id);
modalContext.popModal();
return response;
} catch (e) {
setErrorMessage(
e.message === 'invalid_credentials'
? 'permission not granted'
: 'unknown error',
);
throw e;
}
}, [callDeleteThread, modalContext, setErrorMessage, threadInfo.id]);
const onDelete = React.useCallback(
(event: SyntheticEvent) => {
event.preventDefault();
dispatchActionPromise(deleteThreadActionTypes, deleteThreadAction());
},
[deleteThreadAction, dispatchActionPromise],
);
return (
);
}
export default ThreadSettingsDeleteTab;
diff --git a/web/modals/threads/settings/thread-settings-privacy-tab.react.js b/web/modals/threads/settings/thread-settings-privacy-tab.react.js
index 1d5c6271e..60ac08d1c 100644
--- a/web/modals/threads/settings/thread-settings-privacy-tab.react.js
+++ b/web/modals/threads/settings/thread-settings-privacy-tab.react.js
@@ -1,179 +1,179 @@
// @flow
import * as React from 'react';
import {
changeThreadSettings,
changeThreadSettingsActionTypes,
} from 'lib/actions/thread-actions';
import { useModalContext } from 'lib/components/modal-provider.react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { threadTypeDescriptions } from 'lib/shared/thread-utils';
import { type SetState } from 'lib/types/hook-types';
import {
type ThreadInfo,
type ThreadChanges,
threadTypes,
} from 'lib/types/thread-types';
import {
useDispatchActionPromise,
useServerCall,
} from 'lib/utils/action-utils';
import EnumSettingsOption from '../../../components/enum-settings-option.react';
-import SWMansionIcon from '../../../SWMansionIcon.react';
import SubmitSection from './submit-section.react';
import css from './thread-settings-privacy-tab.css';
const { COMMUNITY_OPEN_SUBTHREAD, COMMUNITY_SECRET_SUBTHREAD } = threadTypes;
const openStatements = [
{
statement: threadTypeDescriptions[COMMUNITY_OPEN_SUBTHREAD],
isStatementValid: true,
styleStatementBasedOnValidity: false,
},
];
const secretStatements = [
{
statement: threadTypeDescriptions[COMMUNITY_SECRET_SUBTHREAD],
isStatementValid: true,
styleStatementBasedOnValidity: false,
},
];
type ThreadSettingsPrivacyTabProps = {
+threadSettingsOperationInProgress: boolean,
+threadInfo: ThreadInfo,
+queuedChanges: ThreadChanges,
+setQueuedChanges: SetState,
+setErrorMessage: SetState,
+errorMessage?: ?string,
};
function ThreadSettingsPrivacyTab(
props: ThreadSettingsPrivacyTabProps,
): React.Node {
const {
threadSettingsOperationInProgress,
threadInfo,
queuedChanges,
setQueuedChanges,
setErrorMessage,
errorMessage,
} = props;
const modalContext = useModalContext();
const dispatchActionPromise = useDispatchActionPromise();
const callChangeThreadSettings = useServerCall(changeThreadSettings);
const changeQueued: boolean = React.useMemo(
() => Object.values(queuedChanges).some(v => v !== null && v !== undefined),
[queuedChanges],
);
const changeThreadSettingsAction = React.useCallback(async () => {
try {
setErrorMessage('');
const response = await callChangeThreadSettings({
threadID: threadInfo.id,
changes: queuedChanges,
});
modalContext.popModal();
return response;
} catch (e) {
setErrorMessage('unknown_error');
setQueuedChanges(Object.freeze({}));
throw e;
}
}, [
callChangeThreadSettings,
modalContext,
queuedChanges,
setErrorMessage,
setQueuedChanges,
threadInfo.id,
]);
const onSubmit = React.useCallback(
(event: SyntheticEvent) => {
event.preventDefault();
dispatchActionPromise(
changeThreadSettingsActionTypes,
changeThreadSettingsAction(),
);
},
[changeThreadSettingsAction, dispatchActionPromise],
);
const onOpenSelected = React.useCallback(() => {
setQueuedChanges(prevQueuedChanges =>
Object.freeze({
...prevQueuedChanges,
type:
COMMUNITY_OPEN_SUBTHREAD !== threadInfo.type
? COMMUNITY_OPEN_SUBTHREAD
: undefined,
}),
);
}, [setQueuedChanges, threadInfo.type]);
const onSecretSelected = React.useCallback(() => {
setQueuedChanges(prevQueuedChanges =>
Object.freeze({
...prevQueuedChanges,
type:
COMMUNITY_SECRET_SUBTHREAD !== threadInfo.type
? COMMUNITY_SECRET_SUBTHREAD
: undefined,
}),
);
}, [setQueuedChanges, threadInfo.type]);
const globeIcon = React.useMemo(
() => ,
[],
);
const lockIcon = React.useMemo(
() => ,
[],
);
return (
);
}
export default ThreadSettingsPrivacyTab;
diff --git a/web/modals/threads/subchannels/subchannel.react.js b/web/modals/threads/subchannels/subchannel.react.js
index b757d6e1d..4392b5b38 100644
--- a/web/modals/threads/subchannels/subchannel.react.js
+++ b/web/modals/threads/subchannels/subchannel.react.js
@@ -1,88 +1,88 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import { useModalContext } from 'lib/components/modal-provider.react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { type ChatThreadItem } from 'lib/selectors/chat-selectors';
import { useMessagePreview } from 'lib/shared/message-utils';
import { shortAbsoluteDate } from 'lib/utils/date-utils';
import { useResolvedThreadInfo } from 'lib/utils/entity-helpers';
import Button from '../../../components/button.react';
import { getDefaultTextMessageRules } from '../../../markdown/rules.react';
import { useOnClickThread } from '../../../selectors/thread-selectors';
-import SWMansionIcon from '../../../SWMansionIcon.react';
import css from './subchannels-modal.css';
type Props = {
+chatThreadItem: ChatThreadItem,
};
function Subchannel(props: Props): React.Node {
const { chatThreadItem } = props;
const {
threadInfo,
mostRecentMessageInfo,
lastUpdatedTimeIncludingSidebars,
} = chatThreadItem;
const { unread } = threadInfo.currentUser;
const subchannelTitleClassName = classNames({
[css.subchannelInfo]: true,
[css.unread]: unread,
});
const { popModal } = useModalContext();
const navigateToThread = useOnClickThread(threadInfo);
const onClickThread = React.useCallback(
event => {
popModal();
navigateToThread(event);
},
[popModal, navigateToThread],
);
const lastActivity = React.useMemo(
() => shortAbsoluteDate(lastUpdatedTimeIncludingSidebars),
[lastUpdatedTimeIncludingSidebars],
);
const messagePreviewResult = useMessagePreview(
mostRecentMessageInfo,
threadInfo,
getDefaultTextMessageRules().simpleMarkdownRules,
);
const lastMessage = React.useMemo(() => {
if (!messagePreviewResult) {
return No messages
;
}
const { message, username } = messagePreviewResult;
const previewText = username
? `${username.text}: ${message.text}`
: message.text;
return (
<>
{previewText}
{lastActivity}
>
);
}, [lastActivity, messagePreviewResult]);
const { uiName } = useResolvedThreadInfo(threadInfo);
return (
);
}
export default Subchannel;
diff --git a/web/settings/account-delete-modal.react.js b/web/settings/account-delete-modal.react.js
index 0de7be60a..2e74f8bda 100644
--- a/web/settings/account-delete-modal.react.js
+++ b/web/settings/account-delete-modal.react.js
@@ -1,213 +1,213 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import {
deleteAccount,
deleteAccountActionTypes,
} from 'lib/actions/user-actions';
import { useModalContext } from 'lib/components/modal-provider.react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { preRequestUserStateSelector } from 'lib/selectors/account-selectors';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors';
import { accountHasPassword } from 'lib/shared/account-utils.js';
import type { LogOutResult } from 'lib/types/account-types';
import type { PreRequestUserState } from 'lib/types/session-types';
import type { DispatchActionPromise } from 'lib/utils/action-utils';
import {
useDispatchActionPromise,
useServerCall,
} from 'lib/utils/action-utils';
import Button, { buttonThemes } from '../components/button.react';
import Input from '../modals/input.react';
import Modal from '../modals/modal.react';
import { useSelector } from '../redux/redux-utils';
-import SWMansionIcon from '../SWMansionIcon.react.js';
import css from './account-delete-modal.css';
type Props = {
+isAccountWithPassword: boolean,
+preRequestUserState: PreRequestUserState,
+inputDisabled: boolean,
+dispatchActionPromise: DispatchActionPromise,
+deleteAccount: (
password: ?string,
preRequestUserState: PreRequestUserState,
) => Promise,
+popModal: () => void,
};
type State = {
+currentPassword: ?string,
+errorMessage: string,
};
class AccountDeleteModal extends React.PureComponent {
currentPasswordInput: ?HTMLInputElement;
constructor(props: Props) {
super(props);
this.state = {
currentPassword: props.isAccountWithPassword ? '' : null,
errorMessage: '',
};
}
componentDidMount() {
invariant(
!this.props.isAccountWithPassword || this.currentPasswordInput,
'newPasswordInput ref unset',
);
this.currentPasswordInput?.focus();
}
render() {
const { inputDisabled } = this.props;
let errorMsg;
if (this.state.errorMessage) {
errorMsg = (
{this.state.errorMessage}
);
}
let passwordConfirmation;
if (this.props.isAccountWithPassword) {
invariant(
this.state.currentPassword !== null &&
this.state.currentPassword !== undefined,
'currentPassword must be set if isAccountWithPassword',
);
passwordConfirmation = (
<>
Please enter your account password to confirm your identity.
Account password
>
);
}
return (
);
}
currentPasswordInputRef = (currentPasswordInput: ?HTMLInputElement) => {
this.currentPasswordInput = currentPasswordInput;
};
onChangeCurrentPassword = (event: SyntheticEvent) => {
const target = event.target;
invariant(target instanceof HTMLInputElement, 'target not input');
this.setState({ currentPassword: target.value });
};
onDelete = (event: SyntheticEvent) => {
event.preventDefault();
this.props.dispatchActionPromise(
deleteAccountActionTypes,
this.deleteAction(),
);
};
async deleteAction() {
try {
const response = await this.props.deleteAccount(
this.state.currentPassword,
this.props.preRequestUserState,
);
this.props.popModal();
return response;
} catch (e) {
const errorMessage =
e.message === 'invalid_credentials'
? 'wrong password'
: 'unknown error';
this.setState(
{
currentPassword: this.props.isAccountWithPassword ? '' : null,
errorMessage: errorMessage,
},
() => {
invariant(
!this.props.isAccountWithPassword || this.currentPasswordInput,
'currentPasswordInput ref unset',
);
this.currentPasswordInput?.focus();
},
);
throw e;
}
}
}
const deleteAccountLoadingStatusSelector = createLoadingStatusSelector(
deleteAccountActionTypes,
);
const ConnectedAccountDeleteModal: React.ComponentType<{}> = React.memo<{}>(
function ConnectedAccountDeleteModal(): React.Node {
const isAccountWithPassword = useSelector(state =>
accountHasPassword(state.currentUserInfo),
);
const preRequestUserState = useSelector(preRequestUserStateSelector);
const inputDisabled = useSelector(
state => deleteAccountLoadingStatusSelector(state) === 'loading',
);
const callDeleteAccount = useServerCall(deleteAccount);
const dispatchActionPromise = useDispatchActionPromise();
const modalContext = useModalContext();
return (
);
},
);
export default ConnectedAccountDeleteModal;
diff --git a/web/settings/account-settings.react.js b/web/settings/account-settings.react.js
index 5e3bb4fdc..07a6a503b 100644
--- a/web/settings/account-settings.react.js
+++ b/web/settings/account-settings.react.js
@@ -1,110 +1,110 @@
// @flow
import * as React from 'react';
import { logOut, logOutActionTypes } from 'lib/actions/user-actions';
import { useModalContext } from 'lib/components/modal-provider.react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { useStringForUser } from 'lib/hooks/ens-cache';
import { preRequestUserStateSelector } from 'lib/selectors/account-selectors';
import { accountHasPassword } from 'lib/shared/account-utils.js';
import {
useDispatchActionPromise,
useServerCall,
} from 'lib/utils/action-utils';
import Button from '../components/button.react';
import { useSelector } from '../redux/redux-utils';
-import SWMansionIcon from '../SWMansionIcon.react';
import css from './account-settings.css';
import PasswordChangeModal from './password-change-modal';
import BlockListModal from './relationship/block-list-modal.react';
import FriendListModal from './relationship/friend-list-modal.react';
function AccountSettings(): React.Node {
const sendLogoutRequest = useServerCall(logOut);
const preRequestUserState = useSelector(preRequestUserStateSelector);
const dispatchActionPromise = useDispatchActionPromise();
const logOutUser = React.useCallback(
() =>
dispatchActionPromise(
logOutActionTypes,
sendLogoutRequest(preRequestUserState),
),
[dispatchActionPromise, preRequestUserState, sendLogoutRequest],
);
const { pushModal, popModal } = useModalContext();
const showPasswordChangeModal = React.useCallback(
() => pushModal( ),
[pushModal],
);
const openFriendList = React.useCallback(
() => pushModal( ),
[popModal, pushModal],
);
const openBlockList = React.useCallback(
() => pushModal( ),
[popModal, pushModal],
);
const isAccountWithPassword = useSelector(state =>
accountHasPassword(state.currentUserInfo),
);
const currentUserInfo = useSelector(state => state.currentUserInfo);
const stringForUser = useStringForUser(currentUserInfo);
if (!currentUserInfo || currentUserInfo.anonymous) {
return null;
}
let changePasswordSection;
if (isAccountWithPassword) {
changePasswordSection = (
Password
******
);
}
return (
);
}
export default AccountSettings;
diff --git a/web/settings/relationship/block-list-row.react.js b/web/settings/relationship/block-list-row.react.js
index 37f45349f..6be62fc24 100644
--- a/web/settings/relationship/block-list-row.react.js
+++ b/web/settings/relationship/block-list-row.react.js
@@ -1,36 +1,36 @@
// @flow
import * as React from 'react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { useRelationshipCallbacks } from 'lib/hooks/relationship-prompt';
import MenuItem from '../../components/menu-item.react';
import Menu from '../../components/menu.react';
-import SWMansionIcon from '../../SWMansionIcon.react';
import css from './user-list-row.css';
import type { UserRowProps } from './user-list.react';
function BlockListRow(props: UserRowProps): React.Node {
const { userInfo, onMenuVisibilityChange } = props;
const { unblockUser } = useRelationshipCallbacks(userInfo.id);
const editIcon = ;
return (
);
}
export default BlockListRow;
diff --git a/web/settings/relationship/friend-list-row.react.js b/web/settings/relationship/friend-list-row.react.js
index 1b0a8fef3..79645ab83 100644
--- a/web/settings/relationship/friend-list-row.react.js
+++ b/web/settings/relationship/friend-list-row.react.js
@@ -1,89 +1,89 @@
// @flow
import * as React from 'react';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { useRelationshipCallbacks } from 'lib/hooks/relationship-prompt';
import { userRelationshipStatus } from 'lib/types/relationship-types';
import Button from '../../components/button.react';
import MenuItem from '../../components/menu-item.react';
import Menu from '../../components/menu.react';
-import SWMansionIcon from '../../SWMansionIcon.react';
import css from './user-list-row.css';
import type { UserRowProps } from './user-list.react';
const dangerButtonColor = {
color: 'var(--btn-bg-danger)',
};
function FriendListRow(props: UserRowProps): React.Node {
const { userInfo, onMenuVisibilityChange } = props;
const { friendUser, unfriendUser } = useRelationshipCallbacks(userInfo.id);
const buttons = React.useMemo(() => {
if (userInfo.relationshipStatus === userRelationshipStatus.REQUEST_SENT) {
return (
Cancel request
);
}
if (
userInfo.relationshipStatus === userRelationshipStatus.REQUEST_RECEIVED
) {
return (
<>
Accept
Reject
>
);
}
if (userInfo.relationshipStatus === userRelationshipStatus.FRIEND) {
const editIcon = ;
return (
);
}
return undefined;
}, [
friendUser,
unfriendUser,
userInfo.relationshipStatus,
onMenuVisibilityChange,
]);
return (
{userInfo.username}
{buttons}
);
}
export default FriendListRow;
diff --git a/web/sidebar/app-switcher.react.js b/web/sidebar/app-switcher.react.js
index d727701e7..e5cb90bfc 100644
--- a/web/sidebar/app-switcher.react.js
+++ b/web/sidebar/app-switcher.react.js
@@ -1,134 +1,134 @@
// @flow
import * as React from 'react';
import { useDispatch } from 'react-redux';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import {
mostRecentlyReadThreadSelector,
unreadCount,
} from 'lib/selectors/thread-selectors';
import { updateNavInfoActionType } from '../redux/action-types';
import { useSelector } from '../redux/redux-utils';
import { navTabSelector } from '../selectors/nav-selectors.js';
-import SWMansionIcon from '../SWMansionIcon.react';
import css from './left-layout-aside.css';
import NavigationPanel from './navigation-panel.react';
function AppSwitcher(): React.Node {
const activeChatThreadID = useSelector(
state => state.navInfo.activeChatThreadID,
);
const mostRecentlyReadThread = useSelector(mostRecentlyReadThreadSelector);
const isActiveThreadCurrentlyUnread = useSelector(
state =>
!activeChatThreadID ||
!!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread,
);
const dispatch = useDispatch();
const onClickChat = React.useCallback(
(event: SyntheticEvent) => {
event.preventDefault();
dispatch({
type: updateNavInfoActionType,
payload: {
tab: 'chat',
activeChatThreadID: isActiveThreadCurrentlyUnread
? mostRecentlyReadThread
: activeChatThreadID,
},
});
},
[
dispatch,
isActiveThreadCurrentlyUnread,
mostRecentlyReadThread,
activeChatThreadID,
],
);
const boundUnreadCount = useSelector(unreadCount);
let chatBadge = null;
if (boundUnreadCount > 0) {
chatBadge = {boundUnreadCount} ;
}
const chatNavigationItem = React.useMemo(
() => (
{chatBadge}
Chat
),
[chatBadge, onClickChat],
);
const onClickCalendar = React.useCallback(
(event: SyntheticEvent) => {
event.preventDefault();
dispatch({
type: updateNavInfoActionType,
payload: { tab: 'calendar' },
});
},
[dispatch],
);
const isCalendarEnabled = useSelector(state => state.enabledApps.calendar);
const calendarNavigationItem = React.useMemo(() => {
if (!isCalendarEnabled) {
return null;
}
return (
Calendar
);
}, [isCalendarEnabled, onClickCalendar]);
const onClickApps = React.useCallback(
(event: SyntheticEvent) => {
event.preventDefault();
dispatch({
type: updateNavInfoActionType,
payload: {
tab: 'apps',
},
});
},
[dispatch],
);
const appNavigationItem = React.useMemo(
() => (
Apps
),
[onClickApps],
);
return (
{chatNavigationItem}
{calendarNavigationItem}
{appNavigationItem}
);
}
export default AppSwitcher;
diff --git a/web/sidebar/community-picker.react.js b/web/sidebar/community-picker.react.js
index c0c8681f1..528cc7951 100644
--- a/web/sidebar/community-picker.react.js
+++ b/web/sidebar/community-picker.react.js
@@ -1,65 +1,66 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import { useDispatch } from 'react-redux';
+import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
+
import { updateNavInfoActionType } from '../redux/action-types.js';
import { useSelector } from '../redux/redux-utils.js';
-import SWMansionIcon from '../SWMansionIcon.react';
import css from './community-picker.css';
function CommunityPicker(): React.Node {
const dispatch = useDispatch();
const openAccountSettings = React.useCallback(
(event: SyntheticEvent) => {
event.preventDefault();
dispatch({
type: updateNavInfoActionType,
payload: { tab: 'settings', settingsSection: 'account' },
});
},
[dispatch],
);
const isSettingsOpen = useSelector(state => state.navInfo.tab === 'settings');
const settingsButtonContainerClass = classNames({
[css.activeContainer]: isSettingsOpen,
});
const openChat = React.useCallback(
(event: SyntheticEvent) => {
event.preventDefault();
dispatch({
type: updateNavInfoActionType,
payload: { tab: 'chat' },
});
},
[dispatch],
);
const isInboxOpen = useSelector(
state =>
state.navInfo.tab === 'chat' ||
state.navInfo.tab === 'apps' ||
state.navInfo.tab === 'calendar',
);
const inboxButtonContainerClass = classNames({
[css.activeContainer]: isInboxOpen,
});
return (
);
}
export default CommunityPicker;