diff --git a/web/modals/threads/color-selector-button.react.js b/web/modals/threads/color-selector-button.react.js
index 2f4f225e5..9492ba29a 100644
--- a/web/modals/threads/color-selector-button.react.js
+++ b/web/modals/threads/color-selector-button.react.js
@@ -1,39 +1,39 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import css from './color-selector-button.css';
type ColorSelectorButtonProps = {
+color: string,
+currentColor: string,
+onColorSelection: (hex: string) => void,
};
function ColorSelectorButton(props: ColorSelectorButtonProps): React.Node {
const { color, currentColor, onColorSelection } = props;
- const active = color === currentColor;
+ const active = color.toLowerCase() === currentColor.toLowerCase();
const containerClassName = classNames(css.container, {
[css.active]: active,
});
const colorSplotchStyle = React.useMemo(
() => ({
backgroundColor: `#${color}`,
}),
[color],
);
const onColorSplotchClicked = React.useCallback(() => {
onColorSelection(color);
}, [onColorSelection, color]);
return (
);
}
export default ColorSelectorButton;
diff --git a/web/modals/threads/thread-settings-modal.react.js b/web/modals/threads/thread-settings-modal.react.js
index cec6b844a..c155ed04d 100644
--- a/web/modals/threads/thread-settings-modal.react.js
+++ b/web/modals/threads/thread-settings-modal.react.js
@@ -1,599 +1,595 @@
// @flow
import classNames from 'classnames';
import invariant from 'invariant';
import _pickBy from 'lodash/fp/pickBy';
import * as React from 'react';
import {
deleteThreadActionTypes,
deleteThread,
changeThreadSettingsActionTypes,
changeThreadSettings,
} from 'lib/actions/thread-actions';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors';
import { threadInfoSelector } from 'lib/selectors/thread-selectors';
import {
threadHasPermission,
threadTypeDescriptions,
robotextName,
} from 'lib/shared/thread-utils';
import {
type ThreadInfo,
threadTypes,
assertThreadType,
type ChangeThreadSettingsPayload,
type UpdateThreadRequest,
type LeaveThreadPayload,
threadPermissions,
type ThreadChanges,
} from 'lib/types/thread-types';
import type { UserInfos } from 'lib/types/user-types';
import {
useDispatchActionPromise,
useServerCall,
type DispatchActionPromise,
} from 'lib/utils/action-utils';
import { firstLine } from 'lib/utils/string-utils';
import Button from '../../components/button.react';
import { useModalContext } from '../../modals/modal-provider.react';
import { useSelector } from '../../redux/redux-utils';
import Input from '../input.react';
import Modal from '../modal.react';
-import ColorPicker from './color-picker.react';
+import ColorSelector from './color-selector.react';
import css from './thread-settings-modal.css';
const { COMMUNITY_OPEN_SUBTHREAD, COMMUNITY_SECRET_SUBTHREAD } = threadTypes;
type TabType = 'general' | 'privacy' | 'delete';
type TabProps = {
+name: string,
+tabType: TabType,
+selected: boolean,
+onClick: (tabType: TabType) => void,
};
class Tab extends React.PureComponent {
render() {
const classNamesForTab = classNames({
[css['current-tab']]: this.props.selected,
[css['delete-tab']]:
this.props.selected && this.props.tabType === 'delete',
});
return (
{this.props.name}
);
}
onClick = () => {
return this.props.onClick(this.props.tabType);
};
}
type BaseProps = {
+threadID: string,
};
type Props = {
...BaseProps,
+threadInfo: ThreadInfo,
+changeInProgress: boolean,
+viewerID: ?string,
+userInfos: UserInfos,
+dispatchActionPromise: DispatchActionPromise,
+deleteThread: (
threadID: string,
currentAccountPassword: string,
) => Promise,
+changeThreadSettings: (
update: UpdateThreadRequest,
) => Promise,
+onClose: () => void,
};
type State = {
+queuedChanges: ThreadChanges,
+errorMessage: string,
+accountPassword: string,
+currentTabType: TabType,
};
class ThreadSettingsModal extends React.PureComponent {
nameInput: ?HTMLInputElement;
newThreadPasswordInput: ?HTMLInputElement;
accountPasswordInput: ?HTMLInputElement;
constructor(props: Props) {
super(props);
this.state = {
queuedChanges: Object.freeze({}),
errorMessage: '',
accountPassword: '',
currentTabType: 'general',
};
}
componentDidMount() {
invariant(this.nameInput, 'nameInput ref unset');
this.nameInput.focus();
}
componentDidUpdate(prevProps: Props) {
if (this.state.currentTabType !== 'delete') {
return;
}
const permissionForDeleteTab = this.hasPermissionForTab(
this.props.threadInfo,
'delete',
);
const prevPermissionForDeleteTab = this.hasPermissionForTab(
prevProps.threadInfo,
'delete',
);
if (!permissionForDeleteTab && prevPermissionForDeleteTab) {
this.setTab('general');
}
}
hasPermissionForTab(threadInfo: ThreadInfo, tab: TabType) {
if (tab === 'general') {
return threadHasPermission(
threadInfo,
threadPermissions.EDIT_THREAD_NAME,
);
} else if (tab === 'privacy') {
return threadHasPermission(
threadInfo,
threadPermissions.EDIT_PERMISSIONS,
);
} else if (tab === 'delete') {
return threadHasPermission(threadInfo, threadPermissions.DELETE_THREAD);
}
invariant(false, `invalid tab ${tab}`);
}
possiblyChangedValue(key: string) {
const valueChanged =
this.state.queuedChanges[key] !== null &&
this.state.queuedChanges[key] !== undefined;
return valueChanged
? this.state.queuedChanges[key]
: this.props.threadInfo[key];
}
namePlaceholder() {
return robotextName(
this.props.threadInfo,
this.props.viewerID,
this.props.userInfos,
);
}
changeQueued() {
return (
Object.keys(
_pickBy(
value => value !== null && value !== undefined,
// the lodash/fp libdef coerces the returned object's properties to the
// same type, which means it only works for object-as-maps $FlowFixMe
)(this.state.queuedChanges),
).length > 0
);
}
render() {
const { threadInfo } = this.props;
const inputDisabled =
this.props.changeInProgress ||
!this.hasPermissionForTab(threadInfo, this.state.currentTabType);
let mainContent = null;
if (this.state.currentTabType === 'general') {
mainContent = (
);
} else if (this.state.currentTabType === 'privacy') {
mainContent = (
);
} else if (this.state.currentTabType === 'delete') {
mainContent = (
<>
Your thread will be permanently deleted. There is no way to
reverse this.
Please enter your account password to confirm your identity
Account password
>
);
}
let buttons = null;
if (this.state.currentTabType === 'delete') {
buttons = (
Delete
);
} else {
buttons = (
Save
);
}
const tabs = [
,
];
// This UI needs to be updated to handle sidebars but we haven't gotten
// there yet. We'll probably end up ripping it out anyways, so for now we
// are just hiding the privacy tab for any thread that was created as a
// sidebar
const canSeePrivacyTab =
this.possiblyChangedValue('parentThreadID') &&
threadInfo.sourceMessageID &&
(threadInfo.type === threadTypes.COMMUNITY_OPEN_SUBTHREAD ||
threadInfo.type === threadTypes.COMMUNITY_SECRET_SUBTHREAD);
if (canSeePrivacyTab) {
tabs.push(
,
);
}
const canDeleteThread = this.hasPermissionForTab(threadInfo, 'delete');
if (canDeleteThread) {
tabs.push(
,
);
}
return (
);
}
setTab = (tabType: TabType) => {
this.setState({ currentTabType: tabType });
};
nameInputRef = (nameInput: ?HTMLInputElement) => {
this.nameInput = nameInput;
};
newThreadPasswordInputRef = (newThreadPasswordInput: ?HTMLInputElement) => {
this.newThreadPasswordInput = newThreadPasswordInput;
};
accountPasswordInputRef = (accountPasswordInput: ?HTMLInputElement) => {
this.accountPasswordInput = accountPasswordInput;
};
onChangeName = (event: SyntheticEvent) => {
const target = event.currentTarget;
const newValue =
target.value !== this.props.threadInfo.name ? target.value : undefined;
this.setState((prevState: State) => ({
...prevState,
queuedChanges: {
...prevState.queuedChanges,
name: firstLine(newValue),
},
}));
};
onChangeDescription = (event: SyntheticEvent) => {
const target = event.currentTarget;
const newValue =
target.value !== this.props.threadInfo.description
? target.value
: undefined;
this.setState((prevState: State) => ({
...prevState,
queuedChanges: {
...prevState.queuedChanges,
description: newValue,
},
}));
};
onChangeColor = (color: string) => {
const newValue = color !== this.props.threadInfo.color ? color : undefined;
this.setState((prevState: State) => ({
...prevState,
queuedChanges: {
...prevState.queuedChanges,
color: newValue,
},
}));
};
onChangeThreadType = (event: SyntheticEvent) => {
const uiValue = assertThreadType(parseInt(event.currentTarget.value, 10));
const newValue =
uiValue !== this.props.threadInfo.type ? uiValue : undefined;
this.setState((prevState: State) => ({
...prevState,
queuedChanges: {
...prevState.queuedChanges,
type: newValue,
},
}));
};
onChangeAccountPassword = (event: SyntheticEvent) => {
const target = event.currentTarget;
this.setState({ accountPassword: target.value });
};
onSubmit = (event: SyntheticEvent) => {
event.preventDefault();
this.props.dispatchActionPromise(
changeThreadSettingsActionTypes,
this.changeThreadSettingsAction(),
);
};
async changeThreadSettingsAction() {
try {
const response = await this.props.changeThreadSettings({
threadID: this.props.threadInfo.id,
changes: this.state.queuedChanges,
});
this.props.onClose();
return response;
} catch (e) {
this.setState(
prevState => ({
...prevState,
queuedChanges: Object.freeze({}),
accountPassword: '',
errorMessage: 'unknown error',
currentTabType: 'general',
}),
() => {
invariant(this.nameInput, 'nameInput ref unset');
this.nameInput.focus();
},
);
throw e;
}
}
onDelete = (event: SyntheticEvent) => {
event.preventDefault();
this.props.dispatchActionPromise(
deleteThreadActionTypes,
this.deleteThreadAction(),
);
};
async deleteThreadAction() {
try {
const response = await this.props.deleteThread(
this.props.threadInfo.id,
this.state.accountPassword,
);
this.props.onClose();
return response;
} catch (e) {
const errorMessage =
e.message === 'invalid_credentials'
? 'wrong password'
: 'unknown error';
this.setState(
{
accountPassword: '',
errorMessage: errorMessage,
},
() => {
invariant(
this.accountPasswordInput,
'accountPasswordInput ref unset',
);
this.accountPasswordInput.focus();
},
);
throw e;
}
}
}
const deleteThreadLoadingStatusSelector = createLoadingStatusSelector(
deleteThreadActionTypes,
);
const changeThreadSettingsLoadingStatusSelector = createLoadingStatusSelector(
changeThreadSettingsActionTypes,
);
const ConnectedThreadSettingsModal: React.ComponentType = React.memo(
function ConnectedThreadSettingsModal(props) {
const changeInProgress = useSelector(
state =>
deleteThreadLoadingStatusSelector(state) === 'loading' ||
changeThreadSettingsLoadingStatusSelector(state) === 'loading',
);
const viewerID = useSelector(
state => state.currentUserInfo && state.currentUserInfo.id,
);
const userInfos = useSelector(state => state.userStore.userInfos);
const callDeleteThread = useServerCall(deleteThread);
const callChangeThreadSettings = useServerCall(changeThreadSettings);
const dispatchActionPromise = useDispatchActionPromise();
const threadInfo: ?ThreadInfo = useSelector(
state => threadInfoSelector(state)[props.threadID],
);
const modalContext = useModalContext();
if (!threadInfo) {
return (
You no longer have permission to view this thread
);
}
return (
);
},
);
export default ConnectedThreadSettingsModal;