diff --git a/web/apps/app-listing.react.js b/web/apps/app-listing.react.js
index 7fb5a5df7..44eb6b49b 100644
--- a/web/apps/app-listing.react.js
+++ b/web/apps/app-listing.react.js
@@ -1,83 +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 {
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.appListingIcon,
- css.appListingIconState,
- {
- [css.iconEnabled]: enabled,
- [css.iconDisabled]: !enabled,
- },
- );
+ 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/apps/apps.css b/web/apps/apps.css
index a7b27d37b..c037c411f 100644
--- a/web/apps/apps.css
+++ b/web/apps/apps.css
@@ -1,63 +1,64 @@
div.appsDirectoryContainer {
display: flex;
flex-direction: column;
align-items: flex-start;
}
h4.appsHeader {
color: var(--fg);
padding: 20px 0 40px 40px;
font-weight: var(--semi-bold);
}
div.appsDirectoryList {
margin-left: 20px;
display: flex;
flex-direction: column;
row-gap: 10px;
}
div.appListingContainer {
color: var(--fg);
display: flex;
flex-direction: row;
align-items: center;
}
div.appListingTextContainer {
display: flex;
flex-direction: column;
flex: 1;
}
h5.appName {
font-weight: var(--semi-bold);
margin-bottom: 4px;
}
small.appCopy {
font-size: var(--xs-font-12);
}
div.appListingIcon {
padding: 0 20px;
align-self: stretch;
display: flex;
align-items: center;
}
-div.appListingIconState {
+.appListingIconState {
+ padding: 0 20px;
font-size: var(--xl-font-20);
}
div.iconReadOnly {
color: var(--app-list-icon-read-only-color);
}
-div.iconEnabled {
+.iconEnabled {
color: var(--app-list-icon-enabled-color);
}
-div.iconDisabled {
+.iconDisabled {
color: var(--app-list-icon-disabled-color);
}
diff --git a/web/chat/chat-thread-composer.css b/web/chat/chat-thread-composer.css
index ce5024400..0c38c9021 100644
--- a/web/chat/chat-thread-composer.css
+++ b/web/chat/chat-thread-composer.css
@@ -1,74 +1,71 @@
div.threadSearchContainer {
background-color: var(--thread-creation-search-container-bg);
color: var(--fg);
display: flex;
flex-direction: column;
max-height: 50%;
overflow: auto;
flex-shrink: 0;
}
div.fullHeight {
flex-grow: 1;
max-height: 100%;
}
div.userSelectedTags {
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-items: center;
gap: 4px;
padding: 4px 12px;
margin-bottom: 8px;
}
div.searchRow {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 8px;
}
div.searchField {
flex-grow: 1;
}
-div.closeSearch {
- cursor: pointer;
- display: flex;
- align-items: center;
+.closeSearch {
color: var(--thread-creation-close-search-color);
margin: 0 8px;
}
ul.searchResultsContainer {
display: flex;
flex-direction: column;
overflow: auto;
padding: 0 12px 8px;
list-style-type: none;
}
.searchResultsItem {
display: flex;
}
.searchResultsItem:hover {
background-color: var(--thread-creation-search-item-bg-hover);
}
.searchResultsButton {
justify-content: space-between;
flex: 1;
padding: 8px 12px;
}
div.userName {
color: var(--fg);
}
div.userInfo {
font-style: italic;
color: var(--thread-creation-search-item-info-color);
}
diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js
index f121853f6..974919d08 100644
--- a/web/chat/chat-thread-composer.react.js
+++ b/web/chat/chat-thread-composer.react.js
@@ -1,187 +1,187 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import { useDispatch } from 'react-redux';
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 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 (
!userListItems.length ||
(!usernameInputText && userInfoInputArray.length)
) {
return null;
}
return (
{userListItems.map((userSearchResult: UserListItem) => (
-
))}
);
}, [
onSelectUserFromSearch,
userInfoInputArray.length,
userListItems,
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 tagsList = React.useMemo(() => {
if (!userInfoInputArray?.length) {
return null;
}
const labels = userInfoInputArray.map(user => {
return (
);
});
return {labels}
;
}, [userInfoInputArray, onRemoveUserFromSelected]);
React.useEffect(() => {
if (!inputState) {
return;
}
inputState.registerSendCallback(hideSearch);
return () => inputState.unregisterSendCallback(hideSearch);
}, [hideSearch, inputState]);
const threadSearchContainerStyles = React.useMemo(
() =>
classNames(css.threadSearchContainer, {
[css.fullHeight]: !userInfoInputArray.length,
}),
[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 fad35ca95..4d015b193 100644
--- a/web/chat/chat-thread-list-item-menu.react.js
+++ b/web/chat/chat-thread-list-item-menu.react.js
@@ -1,75 +1,76 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
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/nav-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/components/clear-search-button.react.js b/web/components/clear-search-button.react.js
index dc32e3214..18f1f564e 100644
--- a/web/components/clear-search-button.react.js
+++ b/web/components/clear-search-button.react.js
@@ -1,26 +1,27 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import SWMansionIcon from '../SWMansionIcon.react';
+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/label.css b/web/components/label.css
index 3ac7fc1ae..bced94463 100644
--- a/web/components/label.css
+++ b/web/components/label.css
@@ -1,16 +1,12 @@
div.label {
line-height: 1.5;
padding: 4px 8px;
border-radius: 8px;
display: flex;
word-break: break-all;
}
button.close {
- display: flex;
- align-items: center;
margin-left: 4px;
- background: transparent;
- border: none;
color: inherit;
}
diff --git a/web/components/label.react.js b/web/components/label.react.js
index cbe9bd559..7a8549ca0 100644
--- a/web/components/label.react.js
+++ b/web/components/label.react.js
@@ -1,53 +1,54 @@
// @flow
import * as React from 'react';
import SWMansionIcon from '../SWMansionIcon.react';
+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 fff1c7fe6..d93abbfb7 100644
--- a/web/components/menu-item.react.js
+++ b/web/components/menu-item.react.js
@@ -1,36 +1,38 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import SWMansionIcon, { type Icon } from '../SWMansionIcon.react';
+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/menu.css b/web/components/menu.css
index 24429c33f..897874772 100644
--- a/web/components/menu.css
+++ b/web/components/menu.css
@@ -1,78 +1,74 @@
button.menuButton {
background-color: transparent;
border: none;
cursor: pointer;
color: inherit;
}
div.menuActionList {
position: absolute;
z-index: 4;
display: flex;
flex-direction: column;
background-color: var(--menu-bg);
color: var(--menu-color);
border-radius: 4px;
padding: 4px 0;
line-height: var(--line-height-text);
min-width: max-content;
}
div.menuActionListThreadActions {
font-size: var(--m-font-16);
top: 40px;
right: -20px;
}
div.menuActionListMemberActions {
font-size: var(--xs-font-12);
background-color: var(--menu-bg-light);
color: var(--menu-color-light);
top: 0;
right: 5px;
}
button.menuAction {
color: inherit;
z-index: 1;
- background-color: transparent;
padding: 12px 16px;
line-height: 1.5;
- border: none;
- cursor: pointer;
- display: flex;
- align-items: center;
font-size: inherit;
+ justify-content: start;
}
button.menuAction:hover {
color: var(--menu-color-hover);
}
div.menuActionIcon {
display: flex;
justify-content: center;
margin-right: 8px;
height: 24px;
width: 24px;
}
div.menuActionListMemberActions div.menuActionIcon {
height: 18px;
width: 18px;
}
button.menuActionDangerous {
color: var(--menu-color-dangerous);
}
button.menuActionDangerous:hover {
color: var(--menu-color-dangerous-hover);
}
hr.separator {
height: 1px;
background: var(--menu-separator-color);
margin: 10px 16px;
max-width: 130px;
border: none;
}
diff --git a/web/components/search.css b/web/components/search.css
index 9dd8ad7cd..c9818df56 100644
--- a/web/components/search.css
+++ b/web/components/search.css
@@ -1,42 +1,40 @@
div.searchContainer {
background-color: var(--text-input-bg);
display: flex;
align-items: center;
margin: 1rem;
border-radius: 16px;
padding: 8px;
}
div.searchIcon {
display: flex;
color: var(--search-icon-color);
}
input.searchInput {
background-color: transparent;
font-size: var(--m-font-16);
line-height: 1.5;
padding: 4px 12px;
flex: 1;
border: none;
color: var(--search-input-color);
outline: none;
}
input.searchInput::placeholder {
color: var(--search-input-placeholder);
}
button.clearSearch {
color: var(--search-clear-color);
transition: ease-in-out 0.15s;
- border: none;
background: var(--search-clear-bg);
border-radius: 50%;
- display: flex;
padding: 6px;
}
button.clearSearchDisabled {
opacity: 0;
}
diff --git a/web/components/tabs-header.js b/web/components/tabs-header.js
index e39fb8a59..c9fa39937 100644
--- a/web/components/tabs-header.js
+++ b/web/components/tabs-header.js
@@ -1,28 +1,29 @@
// @flow
import classnames from 'classnames';
import * as React from 'react';
+import Button from './button.react';
import css from './tabs.css';
type Props = {
- +children?: React.Node,
+ +children: React.Node,
+isActive: boolean,
+setTab: T => mixed,
+id: T,
};
function TabsHeader(props: Props): React.Node {
const { children, isActive, setTab, id } = props;
const headerClasses = classnames(css.tabHeader, {
[css.backgroundTabHeader]: !isActive,
});
const onClickSetTab = React.useCallback(() => setTab(id), [setTab, id]);
return (
-
+
{children}
-
+
);
}
export default TabsHeader;
diff --git a/web/components/tabs.css b/web/components/tabs.css
index e0737224f..b705bbfd8 100644
--- a/web/components/tabs.css
+++ b/web/components/tabs.css
@@ -1,31 +1,30 @@
div.tabsContainer {
color: var(--fg);
display: flex;
flex-direction: column;
overflow: hidden;
max-height: 100%;
flex: 1;
}
+
div.tabsHeaderContainer {
display: flex;
}
-div.tabHeader {
+.tabHeader {
flex: 1;
padding: 16px;
- display: flex;
- justify-content: center;
+ font-size: var(--m-font-16);
color: var(--tabs-header-active-color);
border-bottom: 2px solid var(--tabs-header-active-border);
}
-div.backgroundTabHeader {
- cursor: pointer;
+.backgroundTabHeader {
color: var(--tabs-header-background-color);
border-bottom-color: var(--tabs-header-background-border);
}
-div.backgroundTabHeader:hover {
+.backgroundTabHeader:hover {
color: var(--tabs-header-background-color-hover);
border-bottom-color: var(--tabs-header-background-border-hover);
}
diff --git a/web/media/media.css b/web/media/media.css
index 726f8a7d1..4ca4ce173 100644
--- a/web/media/media.css
+++ b/web/media/media.css
@@ -1,106 +1,102 @@
span.clickable {
cursor: pointer;
}
span.multimedia {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
vertical-align: top;
}
-span.multimedia > span.multimediaImage {
- display: inline-flex;
- align-items: center;
- justify-content: center;
+span.multimedia > .multimediaImage {
position: relative;
min-height: 50px;
min-width: 50px;
}
-span.multimedia > span.multimediaImage > img {
+span.multimedia > .multimediaImage > img {
max-height: 200px;
max-width: 100%;
}
-span.multimedia > span.multimediaImage > svg.removeUpload {
+span.multimedia > .multimediaImage svg.removeUpload {
display: none;
position: absolute;
- cursor: pointer;
top: 3px;
right: 3px;
color: white;
border-radius: 50%;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
background-color: rgba(34, 34, 34, 0.67);
}
-span.multimedia:hover > span.multimediaImage > svg.removeUpload {
+span.multimedia:hover > .multimediaImage svg.removeUpload {
display: inherit;
}
span.multimedia > svg.uploadError {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto auto;
color: white;
border-radius: 50%;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
background-color: #dd2222;
}
span.multimedia > svg.progressIndicator {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto auto;
width: 50px;
height: 50px;
}
:global(.CircularProgressbar-background) {
fill: #666 !important;
}
:global(.CircularProgressbar-text) {
fill: #fff !important;
}
:global(.CircularProgressbar-path) {
stroke: #fff !important;
}
:global(.CircularProgressbar-trail) {
stroke: transparent !important;
}
div.multimediaModalOverlay {
position: fixed;
left: 0;
top: 0;
z-index: 4;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
overflow: auto;
padding: 10px;
box-sizing: border-box;
display: flex;
justify-content: center;
}
div.multimediaModalOverlay > img {
object-fit: scale-down;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
}
svg.closeMultimediaModal {
position: absolute;
cursor: pointer;
top: 15px;
right: 15px;
color: white;
border-radius: 50%;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
background-color: rgba(34, 34, 34, 0.67);
height: 36px;
width: 36px;
}
diff --git a/web/media/multimedia.react.js b/web/media/multimedia.react.js
index e54abf373..4f0fbe4f8 100644
--- a/web/media/multimedia.react.js
+++ b/web/media/multimedia.react.js
@@ -1,126 +1,127 @@
// @flow
import classNames from 'classnames';
import invariant from 'invariant';
import * as React from 'react';
import { CircularProgressbar } from 'react-circular-progressbar';
import 'react-circular-progressbar/dist/styles.css';
import {
XCircle as XCircleIcon,
AlertCircle as AlertCircleIcon,
} from 'react-feather';
import { useModalContext } from 'lib/components/modal-provider.react';
+import Button from '../components/button.react';
import { type PendingMultimediaUpload } from '../input/input-state';
import css from './media.css';
import MultimediaModal from './multimedia-modal.react';
type BaseProps = {
+uri: string,
+pendingUpload?: ?PendingMultimediaUpload,
+remove?: (uploadID: string) => void,
+multimediaCSSClass: string,
+multimediaImageCSSClass: string,
};
type Props = {
...BaseProps,
+pushModal: (modal: React.Node) => void,
};
class Multimedia extends React.PureComponent {
componentDidUpdate(prevProps: Props) {
const { uri, pendingUpload } = this.props;
if (uri === prevProps.uri) {
return;
}
if (
(!pendingUpload || pendingUpload.uriIsReal) &&
(!prevProps.pendingUpload || !prevProps.pendingUpload.uriIsReal)
) {
URL.revokeObjectURL(prevProps.uri);
}
}
render(): React.Node {
let progressIndicator, errorIndicator, removeButton;
const { pendingUpload, remove } = this.props;
if (pendingUpload) {
const { progressPercent, failed } = pendingUpload;
if (progressPercent !== 0 && progressPercent !== 1) {
const outOfHundred = Math.floor(progressPercent * 100);
const text = `${outOfHundred}%`;
progressIndicator = (
);
}
if (failed) {
errorIndicator = (
);
}
if (remove) {
removeButton = (
-
+
+
+
);
}
}
const imageContainerClasses = [
css.multimediaImage,
this.props.multimediaImageCSSClass,
];
imageContainerClasses.push(css.clickable);
const containerClasses = [css.multimedia, this.props.multimediaCSSClass];
return (
-
{removeButton}
-
+
{progressIndicator}
{errorIndicator}
);
}
remove: (event: SyntheticEvent) => void = event => {
event.stopPropagation();
const { remove, pendingUpload } = this.props;
invariant(
remove && pendingUpload,
'Multimedia cannot be removed as either remove or pendingUpload ' +
'are unspecified',
);
remove(pendingUpload.localID);
};
- onClick: (event: SyntheticEvent) => void = event => {
- event.stopPropagation();
-
+ onClick: () => void = () => {
const { pushModal, uri } = this.props;
pushModal();
};
}
function ConnectedMultimediaContainer(props: BaseProps): React.Node {
const modalContext = useModalContext();
return ;
}
export default ConnectedMultimediaContainer;
diff --git a/web/modals/components/add-members-item.react.js b/web/modals/components/add-members-item.react.js
index 365d10dde..3bd6bff55 100644
--- a/web/modals/components/add-members-item.react.js
+++ b/web/modals/components/add-members-item.react.js
@@ -1,50 +1,51 @@
// @flow
import * as React from 'react';
import type { UserListItem } from 'lib/types/user-types';
+import Button from '../../components/button.react';
import css from './add-members.css';
type AddMembersItemProps = {
+userInfo: UserListItem,
+onClick: (userID: string) => void,
+userAdded: boolean,
};
function AddMemberItem(props: AddMembersItemProps): React.Node {
const { userInfo, onClick, userAdded = false } = props;
const canBeAdded = !userInfo.alertText;
const onClickCallback = React.useCallback(() => {
if (!canBeAdded) {
return;
}
onClick(userInfo.id);
}, [canBeAdded, onClick, userInfo.id]);
const action = React.useMemo(() => {
if (!canBeAdded) {
return userInfo.alertTitle;
}
if (userAdded) {
return Remove;
} else {
return 'Add';
}
}, [canBeAdded, userAdded, userInfo.alertTitle]);
return (
-
{userInfo.username}
{action}
-
+
);
}
export default AddMemberItem;
diff --git a/web/modals/components/add-members.css b/web/modals/components/add-members.css
index c5c2d6634..96498af39 100644
--- a/web/modals/components/add-members.css
+++ b/web/modals/components/add-members.css
@@ -1,41 +1,36 @@
div.addMemberItemsGroupHeader {
font-size: var(--s-font-14);
color: var(--add-members-group-header-color);
margin: 16px;
}
button.addMemberItem {
- display: flex;
- flex-direction: row;
justify-content: space-between;
- align-items: center;
color: var(--add-members-item-color);
font-size: var(--l-font-18);
- background-color: transparent;
- border: none;
width: 100%;
}
button.addMemberItem:hover {
color: var(--add-members-item-color-hover);
}
button.addMemberItem:disabled {
color: var(--add-members-item-disabled-color);
cursor: not-allowed;
}
button.addMemberItem:hover:disabled {
color: var(--add-members-item-disabled-color-hover);
}
button.addMemberItem .label {
padding: 8px 16px;
}
button.addMemberItem .danger {
color: var(--add-members-remove-pending-color);
}
button.addMemberItem:hover .danger {
color: var(--add-members-remove-pending-color-hover);
}
diff --git a/web/modals/modal.css b/web/modals/modal.css
index 2d64163bf..6bffc6511 100644
--- a/web/modals/modal.css
+++ b/web/modals/modal.css
@@ -1,43 +1,41 @@
div.modalContainer {
display: flex;
background-color: var(--modal-bg);
border-radius: 8px;
flex-direction: column;
margin: 20px;
overflow: hidden;
}
div.modalContainerSmall {
width: 330px;
}
div.modalContainerLarge {
width: 500px;
}
-span.modalClose {
- display: flex;
+.modalClose {
color: var(--modal-close-color);
}
-span.modalClose:hover {
- cursor: pointer;
+.modalClose:hover {
color: var(--modal-close-color-hover);
}
div.modalHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32px 32px 0 32px;
}
h2.title {
font-size: 20px;
font-weight: 500;
line-height: 32px;
color: var(--fg);
display: flex;
align-items: center;
column-gap: 8px;
}
diff --git a/web/modals/modal.react.js b/web/modals/modal.react.js
index 858fe8656..3f72f142b 100644
--- a/web/modals/modal.react.js
+++ b/web/modals/modal.react.js
@@ -1,79 +1,80 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import ModalOverlay from 'lib/components/modal-overlay.react';
+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,
};
type ModalProps = {
...ModalOverridableProps,
+children?: React.Node,
};
function Modal(props: ModalProps): React.Node {
const {
size = 'small',
children,
onClose,
name,
icon,
withCloseButton = true,
} = props;
const modalContainerClasses = React.useMemo(
() =>
classNames(css.modalContainer, {
[css.modalContainerLarge]: size === 'large',
[css.modalContainerSmall]: size === 'small',
}),
[size],
);
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/color-selector-button.css b/web/modals/threads/color-selector-button.css
index 1cf2e271c..dbdeff1fe 100644
--- a/web/modals/threads/color-selector-button.css
+++ b/web/modals/threads/color-selector-button.css
@@ -1,21 +1,17 @@
-div.container {
+.container {
height: 48px;
width: 48px;
border-radius: 24px;
- cursor: pointer;
- align-items: center;
- justify-content: center;
- display: flex;
}
-div.active,
-div.container:hover {
+.active,
+.container:hover {
background-color: var(--color-selector-active-bg);
}
div.colorSplotch {
height: 32px;
width: 32px;
border-radius: 16px;
cursor: pointer;
}
diff --git a/web/modals/threads/color-selector-button.react.js b/web/modals/threads/color-selector-button.react.js
index e0869ab8f..452326aa0 100644
--- a/web/modals/threads/color-selector-button.react.js
+++ b/web/modals/threads/color-selector-button.react.js
@@ -1,40 +1,41 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import tinycolor from 'tinycolor2';
+import Button from '../../components/button.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 = tinycolor.equals(color, currentColor);
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/sidebars/sidebar.react.js b/web/modals/threads/sidebars/sidebar.react.js
index e30398bf0..bcaaf18fd 100644
--- a/web/modals/threads/sidebars/sidebar.react.js
+++ b/web/modals/threads/sidebars/sidebar.react.js
@@ -1,79 +1,80 @@
// @flow
import * as React from 'react';
import { useModalContext } from 'lib/components/modal-provider.react';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors';
import { getMessagePreview } from 'lib/shared/message-utils';
import { shortAbsoluteDate } from 'lib/utils/date-utils';
+import Button from '../../../components/button.react';
import { getDefaultTextMessageRules } from '../../../markdown/rules.react';
import { useSelector } from '../../../redux/redux-utils';
import { useOnClickThread } from '../../../selectors/nav-selectors';
import css from './sidebars-modal.css';
type Props = {
+sidebar: ChatThreadItem,
+isLastItem?: boolean,
};
function Sidebar(props: Props): React.Node {
const { sidebar, isLastItem } = props;
const { threadInfo, lastUpdatedTime, mostRecentMessageInfo } = sidebar;
const timeZone = useSelector(state => state.timeZone);
const { popModal } = useModalContext();
const navigateToThread = useOnClickThread(threadInfo);
const onClickThread = React.useCallback(
event => {
popModal();
navigateToThread(event);
},
[popModal, navigateToThread],
);
const lastActivity = React.useMemo(
() => shortAbsoluteDate(lastUpdatedTime, timeZone),
[lastUpdatedTime, timeZone],
);
const lastMessage = React.useMemo(() => {
if (!mostRecentMessageInfo) {
return No messages
;
}
const { message, username } = getMessagePreview(
mostRecentMessageInfo,
threadInfo,
getDefaultTextMessageRules().simpleMarkdownRules,
);
const previewText = username ? `${username}: ${message}` : message;
return (
<>
{previewText}
{lastActivity}
>
);
}, [lastActivity, mostRecentMessageInfo, threadInfo]);
return (
-
+
{threadInfo.uiName}
{lastMessage}
-
+
);
}
export default Sidebar;
diff --git a/web/modals/threads/sidebars/sidebars-modal.css b/web/modals/threads/sidebars/sidebars-modal.css
index faa7e4555..76aa6bdc8 100644
--- a/web/modals/threads/sidebars/sidebars-modal.css
+++ b/web/modals/threads/sidebars/sidebars-modal.css
@@ -1,71 +1,68 @@
div.sidebarListContainer {
display: flex;
flex-direction: column;
line-height: var(--line-height-text);
width: 383px;
height: 458px;
}
div.sidebarList {
overflow: auto;
color: var(--sidebars-modal-color);
}
div.noSidebars {
padding: 16px;
text-align: center;
color: var(--sidebars-modal-color);
}
button.sidebarContainer {
- cursor: pointer;
- display: flex;
padding: 0 16px;
column-gap: 8px;
align-items: flex-start;
width: 100%;
- border: none;
font-size: inherit;
text-align: inherit;
line-height: inherit;
color: inherit;
background: inherit;
}
button.sidebarContainer:hover {
color: var(--sidebars-modal-color-hover);
}
div.sidebarInfo {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 8px 0;
}
div.longTextEllipsis {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
div.lastMessage {
display: flex;
justify-content: space-between;
column-gap: 14px;
}
div.noMessage {
text-align: center;
font-style: italic;
}
div.lastActivity {
white-space: nowrap;
}
img.sidebarArrow {
position: relative;
top: -12px;
}
diff --git a/web/modals/threads/subchannels/subchannel.react.js b/web/modals/threads/subchannels/subchannel.react.js
index d0a030c3f..16ad07fce 100644
--- a/web/modals/threads/subchannels/subchannel.react.js
+++ b/web/modals/threads/subchannels/subchannel.react.js
@@ -1,75 +1,76 @@
// @flow
import * as React from 'react';
import { useModalContext } from 'lib/components/modal-provider.react';
import { type ChatThreadItem } from 'lib/selectors/chat-selectors';
import { getMessagePreview } from 'lib/shared/message-utils';
import { shortAbsoluteDate } from 'lib/utils/date-utils';
+import Button from '../../../components/button.react';
import { getDefaultTextMessageRules } from '../../../markdown/rules.react';
import { useSelector } from '../../../redux/redux-utils';
import { useOnClickThread } from '../../../selectors/nav-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 timeZone = useSelector(state => state.timeZone);
const { popModal } = useModalContext();
const navigateToThread = useOnClickThread(threadInfo);
const onClickThread = React.useCallback(
event => {
popModal();
navigateToThread(event);
},
[popModal, navigateToThread],
);
const lastActivity = React.useMemo(
() => shortAbsoluteDate(lastUpdatedTimeIncludingSidebars, timeZone),
[lastUpdatedTimeIncludingSidebars, timeZone],
);
const lastMessage = React.useMemo(() => {
if (!mostRecentMessageInfo) {
return No messages
;
}
const { message, username } = getMessagePreview(
mostRecentMessageInfo,
threadInfo,
getDefaultTextMessageRules().simpleMarkdownRules,
);
const previewText = username ? `${username}: ${message}` : message;
return (
<>
{previewText}
{lastActivity}
>
);
}, [lastActivity, mostRecentMessageInfo, threadInfo]);
return (
-
+
{threadInfo.uiName}
{lastMessage}
-
+
);
}
export default Subchannel;
diff --git a/web/modals/threads/subchannels/subchannels-modal.css b/web/modals/threads/subchannels/subchannels-modal.css
index 064a8f3cf..9242aed33 100644
--- a/web/modals/threads/subchannels/subchannels-modal.css
+++ b/web/modals/threads/subchannels/subchannels-modal.css
@@ -1,54 +1,55 @@
div.subchannelsListContainer {
display: flex;
flex-direction: column;
overflow: auto;
- line-height: var(--line-height-text);
- color: var(--subchannels-modal-color);
row-gap: 8px;
width: 383px;
height: 458px;
}
div.noSubchannels {
text-align: center;
}
-div.subchannelContainer {
- cursor: pointer;
- display: flex;
+.subchannelContainer {
+ align-items: flex-start;
+ font-size: var(--m-font-18);
+ line-height: var(--line-height-text);
+ color: var(--subchannels-modal-color);
padding: 8px 16px;
column-gap: 8px;
}
-div.subchannelContainer:hover {
+.subchannelContainer:hover {
color: var(--subchannels-modal-color-hover);
}
div.subchannelInfo {
flex: 1;
display: flex;
flex-direction: column;
row-gap: 8px;
overflow: hidden;
}
div.longTextEllipsis {
+ align-self: flex-start;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
div.lastMessage {
display: flex;
justify-content: space-between;
column-gap: 14px;
}
div.noMessage {
text-align: center;
font-style: italic;
}
div.lastActivity {
white-space: nowrap;
}
diff --git a/web/modals/threads/thread-picker-modal.css b/web/modals/threads/thread-picker-modal.css
index 8c231a4a0..073b8d5b4 100644
--- a/web/modals/threads/thread-picker-modal.css
+++ b/web/modals/threads/thread-picker-modal.css
@@ -1,41 +1,45 @@
div.container {
display: flex;
flex-direction: column;
overflow: hidden;
margin: 16px;
}
div.contentContainer {
overflow: scroll;
height: 448px;
}
div.threadPickerOptionContainer {
display: flex;
- align-items: center;
+}
+
+.threadPickerOptionButton {
+ flex: 1;
+ justify-content: left;
padding: 12px 16px;
- cursor: pointer;
+ font-size: var(--m-font-16);
}
-div.threadPickerOptionContainer:hover {
+.threadPickerOptionButton:hover {
background-color: var(--thread-hover-bg);
border-radius: 8px;
}
div.threadSplotch {
min-width: 40px;
height: 40px;
border-radius: 10px;
}
div.threadNameText {
color: var(--shades-white-100);
margin-left: 16px;
}
div.noResultsText {
text-align: center;
color: var(--shades-white-100);
margin-top: 24px;
font-weight: 500;
}
diff --git a/web/modals/threads/thread-picker-modal.react.js b/web/modals/threads/thread-picker-modal.react.js
index fb60aac0c..36a8b67de 100644
--- a/web/modals/threads/thread-picker-modal.react.js
+++ b/web/modals/threads/thread-picker-modal.react.js
@@ -1,134 +1,136 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { createSelector } from 'reselect';
import { threadSearchIndex } from 'lib/selectors/nav-selectors';
import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors';
import type { ThreadInfo } from 'lib/types/thread-types';
+import Button from '../../components/button.react';
import Search from '../../components/search.react';
import { useSelector } from '../../redux/redux-utils';
import Modal, { type ModalOverridableProps } from '../modal.react';
import css from './thread-picker-modal.css';
type OptionProps = {
+threadInfo: ThreadInfo,
+createNewEntry: (threadID: string) => void,
+onCloseModal: () => void,
};
function ThreadPickerOption(props: OptionProps) {
const { threadInfo, createNewEntry, onCloseModal } = props;
const onClickThreadOption = React.useCallback(() => {
createNewEntry(threadInfo.id);
onCloseModal();
}, [threadInfo.id, createNewEntry, onCloseModal]);
const splotchColorStyle = React.useMemo(
() => ({
backgroundColor: `#${threadInfo.color}`,
}),
[threadInfo.color],
);
return (
-
-
-
{threadInfo.uiName}
+
+
+
+ {threadInfo.uiName}
+
);
}
type Props = {
...ModalOverridableProps,
+createNewEntry: (threadID: string) => void,
};
function ThreadPickerModal(props: Props): React.Node {
const { createNewEntry, ...modalProps } = props;
const onScreenThreadInfos = useSelector(onScreenEntryEditableThreadInfos);
const searchIndex = useSelector(state => threadSearchIndex(state));
invariant(
onScreenThreadInfos.length > 0,
"ThreadPicker can't be open when onScreenThreadInfos is empty",
);
const [searchText, setSearchText] = React.useState
('');
const [searchResults, setSearchResults] = React.useState>(
new Set(),
);
const onChangeSearchText = React.useCallback(
(text: string) => {
const results = searchIndex.getSearchResults(text);
setSearchText(text);
setSearchResults(new Set(results));
},
[searchIndex],
);
const listDataSelector = createSelector(
state => state.onScreenThreadInfos,
state => state.searchText,
state => state.searchResults,
(
threadInfos: $ReadOnlyArray,
text: string,
results: Set,
) =>
text
? threadInfos.filter(threadInfo => results.has(threadInfo.id))
: [...threadInfos],
);
const threads = useSelector(() =>
listDataSelector({
onScreenThreadInfos,
searchText,
searchResults,
}),
);
const threadPickerContent = React.useMemo(() => {
const options = threads.map(threadInfo => (
));
if (options.length === 0 && searchText.length > 0) {
return (
No results for {searchText}
);
} else {
return options;
}
}, [threads, createNewEntry, modalProps.onClose, searchText]);
return (
);
}
export default ThreadPickerModal;
diff --git a/web/settings/relationship/add-users-list-item.react.js b/web/settings/relationship/add-users-list-item.react.js
index c495aa2e5..2e6547ce6 100644
--- a/web/settings/relationship/add-users-list-item.react.js
+++ b/web/settings/relationship/add-users-list-item.react.js
@@ -1,28 +1,29 @@
// @flow
import * as React from 'react';
import type { AccountUserInfo } from 'lib/types/user-types.js';
+import Button from '../../components/button.react';
import css from './add-users-list.css';
type Props = {
+userInfo: AccountUserInfo,
+selectUser: (userID: string) => mixed,
};
function AddUsersListItem(props: Props): React.Node {
const { userInfo, selectUser } = props;
const addUser = React.useCallback(() => selectUser(userInfo.id), [
selectUser,
userInfo.id,
]);
return (
-
+
{userInfo.username}
Add
-
+
);
}
export default AddUsersListItem;
diff --git a/web/settings/relationship/add-users-list.css b/web/settings/relationship/add-users-list.css
index 6cfe88d46..d161fc6fe 100644
--- a/web/settings/relationship/add-users-list.css
+++ b/web/settings/relationship/add-users-list.css
@@ -1,63 +1,59 @@
.container {
height: 625px;
display: flex;
flex-direction: column;
}
.userTagsContainer {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 8px;
}
.userRowsContainer {
overflow: auto;
display: flex;
flex-direction: column;
flex: 1;
margin-bottom: 8px;
}
.addUserButton {
- display: flex;
- flex-direction: row;
justify-content: space-between;
padding: 16px;
color: var(--relationship-modal-color);
font-size: var(--l-font-18);
line-height: var(--line-height-display);
- background: transparent;
- border: none;
}
.addUserButtonUsername {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.buttons {
display: flex;
justify-content: space-between;
}
.confirmButtonContainer {
display: flex;
flex-direction: column;
align-items: center;
}
.hidden {
visibility: hidden;
height: 0;
}
.error {
padding-bottom: 8px;
font-size: var(--s-font-14);
line-height: var(--line-height-display);
color: var(--error);
padding-left: 6px;
font-style: italic;
}