diff --git a/native/chat/sidebar-list-modal.react.js b/native/chat/sidebar-list-modal.react.js
index f55b6d437..e05ebd984 100644
--- a/native/chat/sidebar-list-modal.react.js
+++ b/native/chat/sidebar-list-modal.react.js
@@ -1,144 +1,146 @@
// @flow
import * as React from 'react';
import { View } from 'react-native';
import { useSearchSidebars } from 'lib/hooks/search-threads';
import type { ThreadInfo, SidebarInfo } from 'lib/types/thread-types';
import ExtendedArrow from '../components/arrow-extended.react';
import Arrow from '../components/arrow.react';
import Button from '../components/button.react';
import type { RootNavigationProp } from '../navigation/root-navigator.react';
import type { NavigationRoute } from '../navigation/route-names';
import { useColors, useStyles } from '../themes/colors';
import { SidebarItem } from './sidebar-item.react';
import ThreadListModal from './thread-list-modal.react';
export type SidebarListModalParams = {
+threadInfo: ThreadInfo,
};
type Props = {
+navigation: RootNavigationProp<'SidebarListModal'>,
+route: NavigationRoute<'SidebarListModal'>,
};
function SidebarListModal(props: Props): React.Node {
const {
listData,
searchState,
setSearchState,
onChangeSearchInputText,
} = useSearchSidebars(props.route.params.threadInfo);
const numOfSidebarsWithExtendedArrow = listData.length - 1;
const createRenderItem = React.useCallback(
(
onPressItem: (threadInfo: ThreadInfo) => void,
// eslint-disable-next-line react/display-name
) => (row: { +item: SidebarInfo, +index: number, ... }) => {
let extendArrow: boolean = false;
if (row.index < numOfSidebarsWithExtendedArrow) {
extendArrow = true;
}
return (
);
},
[numOfSidebarsWithExtendedArrow],
);
return (
);
}
function Item(props: {
item: SidebarInfo,
onPressItem: (threadInfo: ThreadInfo) => void,
extendArrow: boolean,
}): React.Node {
const { item, onPressItem, extendArrow } = props;
const { threadInfo } = item;
const onPressButton = React.useCallback(() => onPressItem(threadInfo), [
onPressItem,
threadInfo,
]);
const colors = useColors();
const styles = useStyles(unboundStyles);
let arrow;
if (extendArrow) {
arrow = (
);
} else {
arrow = (
);
}
return (
);
}
const unboundStyles = {
arrow: {
position: 'absolute',
top: -12,
},
extendedArrow: {
position: 'absolute',
top: -6,
},
sidebar: {
paddingLeft: 0,
paddingRight: 5,
+ height: 38,
},
sidebarItemContainer: {
flex: 1,
},
sidebarRowContainer: {
flex: 1,
flexDirection: 'row',
},
spacer: {
width: 30,
},
};
export default SidebarListModal;
diff --git a/native/chat/subchannel-item.react.js b/native/chat/subchannel-item.react.js
index a91a47a66..e75be2a8d 100644
--- a/native/chat/subchannel-item.react.js
+++ b/native/chat/subchannel-item.react.js
@@ -1,114 +1,112 @@
// @flow
import * as React from 'react';
import { Text, View } from 'react-native';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors';
import { shortAbsoluteDate } from 'lib/utils/date-utils';
import { SingleLine } from '../components/single-line.react';
import SWMansionIcon from '../components/swmansion-icon.react';
import { useStyles } from '../themes/colors';
import MessagePreview from './message-preview.react';
type Props = {
+subchannelInfo: ChatThreadItem,
};
function SubchannelItem(props: Props): React.Node {
const {
lastUpdatedTime,
threadInfo,
mostRecentMessageInfo,
} = props.subchannelInfo;
const lastActivity = shortAbsoluteDate(lastUpdatedTime);
const styles = useStyles(unboundStyles);
const unreadStyle = threadInfo.currentUser.unread ? styles.unread : null;
const lastMessage = React.useMemo(() => {
if (!mostRecentMessageInfo) {
return (
No messages
);
}
return (
);
}, [mostRecentMessageInfo, threadInfo, styles]);
return (
{threadInfo.uiName}
{lastMessage}
{lastActivity}
);
}
-
+const fontSize = 14;
const unboundStyles = {
outerView: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
paddingVertical: 8,
- paddingHorizontal: 16,
+ paddingHorizontal: 8,
height: 60,
},
itemRowContainer: {
flexDirection: 'row',
- height: 24,
alignItems: 'center',
},
unread: {
color: 'listForegroundLabel',
fontWeight: 'bold',
},
name: {
color: 'listForegroundSecondaryLabel',
flex: 1,
fontSize: 16,
- paddingLeft: 3,
- paddingBottom: 2,
+ paddingBottom: 8,
},
lastActivity: {
color: 'listForegroundTertiaryLabel',
- fontSize: 14,
+ fontSize,
marginLeft: 10,
},
iconWrapper: {
marginRight: 8,
alignItems: 'center',
},
icon: {
fontSize: 22,
color: 'listForegroundSecondaryLabel',
alignItems: 'center',
height: 24,
},
noMessages: {
color: 'listForegroundTertiaryLabel',
flex: 1,
- fontSize: 14,
+ fontSize,
fontStyle: 'italic',
},
};
export default SubchannelItem;
diff --git a/native/chat/subchannels-list-modal.react.js b/native/chat/subchannels-list-modal.react.js
index 8dc3f4b64..05ab325b6 100644
--- a/native/chat/subchannels-list-modal.react.js
+++ b/native/chat/subchannels-list-modal.react.js
@@ -1,99 +1,100 @@
// @flow
import * as React from 'react';
import { View } from 'react-native';
import { useSearchSubchannels } from 'lib/hooks/search-threads';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors';
import { type ThreadInfo } from 'lib/types/thread-types';
import Button from '../components/button.react';
import type { RootNavigationProp } from '../navigation/root-navigator.react';
import type { NavigationRoute } from '../navigation/route-names';
import { useColors, useStyles } from '../themes/colors';
import SubchannelItem from './subchannel-item.react';
import ThreadListModal from './thread-list-modal.react';
export type SubchannelListModalParams = {
+threadInfo: ThreadInfo,
};
type Props = {
+navigation: RootNavigationProp<'SubchannelsListModal'>,
+route: NavigationRoute<'SubchannelsListModal'>,
};
function SubchannelListModal(props: Props): React.Node {
const {
listData,
searchState,
setSearchState,
onChangeSearchInputText,
} = useSearchSubchannels(props.route.params.threadInfo);
return (
);
}
const createRenderItem = (
onPressItem: (threadInfo: ThreadInfo) => void,
// eslint-disable-next-line react/display-name
) => (row: { +item: ChatThreadItem, +index: number, ... }) => {
return ;
};
function Item(props: {
onPressItem: (threadInfo: ThreadInfo) => void,
subchannelInfo: ChatThreadItem,
}): React.Node {
const { onPressItem, subchannelInfo } = props;
const { threadInfo } = subchannelInfo;
const onPressButton = React.useCallback(() => onPressItem(threadInfo), [
onPressItem,
threadInfo,
]);
const colors = useColors();
const styles = useStyles(unboundStyles);
return (
);
}
const unboundStyles = {
subchannel: {
paddingLeft: 0,
paddingRight: 5,
},
subchannelItemContainer: {
flex: 1,
},
subchannelRowContainer: {
flex: 1,
flexDirection: 'row',
},
};
export default SubchannelListModal;
diff --git a/native/chat/thread-list-modal.react.js b/native/chat/thread-list-modal.react.js
index a9c964de5..7b94d8ed4 100644
--- a/native/chat/thread-list-modal.react.js
+++ b/native/chat/thread-list-modal.react.js
@@ -1,118 +1,184 @@
// @flow
+import { useNavigation } from '@react-navigation/native';
import * as React from 'react';
-import { TextInput, FlatList, StyleSheet } from 'react-native';
+import {
+ Text,
+ TextInput,
+ FlatList,
+ View,
+ TouchableOpacity,
+} from 'react-native';
import type { ThreadSearchState } from 'lib/hooks/search-threads';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors';
import type { SetState } from 'lib/types/hook-types';
import type { ThreadInfo, SidebarInfo } from 'lib/types/thread-types';
import Modal from '../components/modal.react';
import Search from '../components/search.react';
-import { useIndicatorStyle } from '../themes/colors';
+import SWMansionIcon from '../components/swmansion-icon.react';
+import { useIndicatorStyle, useStyles } from '../themes/colors';
import { waitForModalInputFocus } from '../utils/timers';
import { useNavigateToThread } from './message-list-types';
function keyExtractor(sidebarInfo: SidebarInfo | ChatThreadItem) {
return sidebarInfo.threadInfo.id;
}
function getItemLayout(
data: ?$ReadOnlyArray,
index: number,
) {
return { length: 24, offset: 24 * index, index };
}
type Props = {
+threadInfo: ThreadInfo,
+createRenderItem: (
onPressItem: (threadInfo: ThreadInfo) => void,
) => (row: {
+item: U,
+index: number,
...
}) => React.Node,
+listData: $ReadOnlyArray,
+searchState: ThreadSearchState,
+setSearchState: SetState,
+onChangeSearchInputText: (text: string) => mixed,
- +searchPlaceholder: string,
+ +searchPlaceholder?: string,
+ +modalTitle: string,
};
function ThreadListModal(
props: Props,
): React.Node {
const {
searchState,
setSearchState,
onChangeSearchInputText,
listData,
createRenderItem,
searchPlaceholder,
+ modalTitle,
} = props;
const searchTextInputRef = React.useRef();
const setSearchTextInputRef = React.useCallback(
async (textInput: ?React.ElementRef) => {
searchTextInputRef.current = textInput;
if (!textInput) {
return;
}
await waitForModalInputFocus();
if (searchTextInputRef.current) {
searchTextInputRef.current.focus();
}
},
[],
);
const navigateToThread = useNavigateToThread();
const onPressItem = React.useCallback(
(threadInfo: ThreadInfo) => {
setSearchState({
text: '',
results: new Set(),
});
if (searchTextInputRef.current) {
searchTextInputRef.current.blur();
}
navigateToThread({ threadInfo });
},
[navigateToThread, setSearchState],
);
const renderItem = React.useMemo(() => createRenderItem(onPressItem), [
createRenderItem,
onPressItem,
]);
+ const styles = useStyles(unboundStyles);
const indicatorStyle = useIndicatorStyle();
+ const navigation = useNavigation();
+
return (
-
-
-
+
+
+ {modalTitle}
+
+
+
+
+
+
+
+
);
}
-const styles = StyleSheet.create({
+const unboundStyles = {
+ body: {
+ paddingHorizontal: 16,
+ flex: 1,
+ },
+ header: {
+ borderBottomColor: 'subthreadsModalSearch',
+ borderBottomWidth: 1,
+ height: 72,
+ padding: 16,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ modal: {
+ borderRadius: 8,
+ paddingHorizontal: 0,
+ backgroundColor: 'subthreadsModalBackgroud',
+ paddingTop: 0,
+ justifyContent: 'flex-start',
+ },
search: {
- marginBottom: 8,
+ height: 40,
+ marginVertical: 16,
+ backgroundColor: 'subthreadsModalSearch',
},
-});
+ title: {
+ color: 'listForegroundLabel',
+ fontSize: 20,
+ fontWeight: '500',
+ lineHeight: 26,
+ alignSelf: 'center',
+ marginLeft: 2,
+ },
+ closeIcon: {
+ color: 'subthreadsModalClose',
+ },
+ closeButton: {
+ marginRight: 2,
+ height: 40,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+};
export default ThreadListModal;
diff --git a/native/themes/colors.js b/native/themes/colors.js
index 5c14c547b..9f5b61fd1 100644
--- a/native/themes/colors.js
+++ b/native/themes/colors.js
@@ -1,320 +1,326 @@
// @flow
import * as React from 'react';
import { StyleSheet } from 'react-native';
import { createSelector } from 'reselect';
import { selectBackgroundIsDark } from '../navigation/nav-selectors';
import { NavContext } from '../navigation/navigation-context';
import { useSelector } from '../redux/redux-utils';
import type { AppState } from '../redux/state-types';
import type { GlobalTheme } from '../types/themes';
const light = Object.freeze({
blockQuoteBackground: '#D3D3D3',
blockQuoteBorder: '#C0C0C0',
codeBackground: '#DCDCDC',
disabledButton: '#D3D3D3',
disconnectedBarBackground: '#F5F5F5',
editButton: '#A4A4A2',
floatingButtonBackground: '#999999',
floatingButtonLabel: '#EEEEEE',
greenButton: '#6EC472',
greenText: 'green',
headerChevron: '#0A0A0A',
inlineSidebarBackground: '#E0E0E0',
inlineSidebarLabel: '#000000',
link: '#036AFF',
listBackground: 'white',
listBackgroundLabel: 'black',
listBackgroundSecondaryLabel: '#444444',
listBackgroundTernaryLabel: '#999999',
listChatBubble: '#F1F0F5',
listForegroundLabel: 'black',
listForegroundQuaternaryLabel: '#AAAAAA',
listForegroundSecondaryLabel: '#333333',
listForegroundTertiaryLabel: '#666666',
listInputBackground: '#F5F5F5',
listInputBar: '#E2E2E2',
listInputBorder: '#AAAAAAAA',
listInputButton: '#8E8D92',
listIosHighlightUnderlay: '#DDDDDDDD',
listSearchBackground: '#F5F5F5',
listSearchIcon: '#8E8D92',
listSeparator: '#EEEEEE',
listSeparatorLabel: '#555555',
mintButton: '#44CC99',
modalBackground: '#EEEEEE',
modalBackgroundLabel: '#333333',
modalBackgroundSecondaryLabel: '#AAAAAA',
modalButton: '#BBBBBB',
modalButtonLabel: 'black',
modalContrastBackground: 'black',
modalContrastForegroundLabel: 'white',
modalContrastOpacity: 0.7,
modalForeground: 'white',
modalForegroundBorder: '#CCCCCC',
modalForegroundLabel: 'black',
modalForegroundSecondaryLabel: '#888888',
modalForegroundTertiaryLabel: '#AAAAAA',
modalIosHighlightUnderlay: '#CCCCCCDD',
modalSubtext: '#CCCCCC',
modalSubtextLabel: '#555555',
navigationCard: '#FFFFFF',
navigationChevron: '#BAB9BE',
panelBackground: '#F5F5F5',
panelBackgroundLabel: '#888888',
panelForeground: 'white',
panelForegroundBorder: '#CCCCCC',
panelForegroundLabel: 'black',
panelForegroundSecondaryLabel: '#333333',
panelForegroundTertiaryLabel: '#888888',
panelIosHighlightUnderlay: '#EEEEEEDD',
panelSecondaryForeground: '#F5F5F5',
panelSecondaryForegroundBorder: '#D1D1D6',
purpleLink: '#7E57C2',
purpleButton: '#7E57C2',
redButton: '#BB8888',
redText: '#FF4444',
spoiler: '#33332C',
tabBarAccent: '#7E57C2',
tabBarBackground: '#F5F5F5',
tabBarActiveTintColor: '#7E57C2',
vibrantGreenButton: '#00C853',
vibrantRedButton: '#F53100',
tooltipBackground: '#E0E0E0',
logInSpacer: '#FFFFFF33',
logInText: 'white',
siweButton: 'white',
siweButtonText: '#1F1F1F',
drawerExpandButton: '#808080',
drawerExpandButtonDisabled: '#CCCCCC',
drawerItemLabelLevel0: '#0A0A0A',
drawerItemLabelLevel1: '#0A0A0A',
drawerItemLabelLevel2: '#1F1F1F',
drawerOpenCommunityBackground: '#F5F5F5',
drawerBackgroud: '#FFFFFF',
+ subthreadsModalClose: '#808080',
+ subthreadsModalBackgroud: '#EEEEEE',
+ subthreadsModalSearch: 'rgba(0, 0, 0, 0.08)',
});
export type Colors = $Exact;
const dark: Colors = Object.freeze({
blockQuoteBackground: '#A9A9A9',
blockQuoteBorder: '#808080',
codeBackground: '#0A0A0A',
disabledButton: '#444444',
disconnectedBarBackground: '#1D1D1D',
editButton: '#5B5B5D',
floatingButtonBackground: '#666666',
floatingButtonLabel: 'white',
greenButton: '#43A047',
greenText: '#44FF44',
headerChevron: '#FFFFFF',
inlineSidebarBackground: '#666666',
inlineSidebarLabel: '#FFFFFF',
link: '#129AFF',
listBackground: '#0A0A0A',
listBackgroundLabel: '#C7C7CC',
listBackgroundSecondaryLabel: '#BBBBBB',
listBackgroundTernaryLabel: '#888888',
listChatBubble: '#26252A',
listForegroundLabel: 'white',
listForegroundQuaternaryLabel: '#555555',
listForegroundSecondaryLabel: '#CCCCCC',
listForegroundTertiaryLabel: '#999999',
listInputBackground: '#1D1D1D',
listInputBar: '#555555',
listInputBorder: '#333333',
listInputButton: '#AAAAAA',
listIosHighlightUnderlay: '#BBBBBB88',
listSearchBackground: '#1D1D1D',
listSearchIcon: '#AAAAAA',
listSeparator: '#3A3A3C',
listSeparatorLabel: '#EEEEEE',
mintButton: '#44CC99',
modalBackground: '#0A0A0A',
modalBackgroundLabel: '#CCCCCC',
modalBackgroundSecondaryLabel: '#555555',
modalButton: '#666666',
modalButtonLabel: 'white',
modalContrastBackground: 'white',
modalContrastForegroundLabel: 'black',
modalContrastOpacity: 0.85,
modalForeground: '#1C1C1E',
modalForegroundBorder: '#1C1C1E',
modalForegroundLabel: 'white',
modalForegroundSecondaryLabel: '#AAAAAA',
modalForegroundTertiaryLabel: '#666666',
modalIosHighlightUnderlay: '#AAAAAA88',
modalSubtext: '#444444',
modalSubtextLabel: '#AAAAAA',
navigationCard: '#2A2A2A',
navigationChevron: '#5B5B5D',
panelBackground: '#0A0A0A',
panelBackgroundLabel: '#C7C7CC',
panelForeground: '#1D1D1D',
panelForegroundBorder: '#2C2C2E',
panelForegroundLabel: 'white',
panelForegroundSecondaryLabel: '#CCCCCC',
panelForegroundTertiaryLabel: '#AAAAAA',
panelIosHighlightUnderlay: '#313035',
panelSecondaryForeground: '#333333',
panelSecondaryForegroundBorder: '#666666',
purpleLink: '#AE94DB',
purpleButton: '#7E57C2',
redButton: '#FF4444',
redText: '#FF4444',
spoiler: '#33332C',
tabBarAccent: '#AE94DB',
tabBarBackground: '#0A0A0A',
tabBarActiveTintColor: '#AE94DB',
vibrantGreenButton: '#00C853',
vibrantRedButton: '#F53100',
tooltipBackground: '#1F1F1F',
logInSpacer: '#FFFFFF33',
logInText: 'white',
siweButton: 'white',
siweButtonText: '#1F1F1F',
drawerExpandButton: '#808080',
drawerExpandButtonDisabled: '#404040',
drawerItemLabelLevel0: '#CCCCCC',
drawerItemLabelLevel1: '#CCCCCC',
drawerItemLabelLevel2: '#F5F5F5',
drawerOpenCommunityBackground: '#191919',
drawerBackgroud: '#1F1F1F',
+ subthreadsModalClose: '#808080',
+ subthreadsModalBackgroud: '#1F1F1F',
+ subthreadsModalSearch: 'rgba(255, 255, 255, 0.04)',
});
const colors = { light, dark };
const colorsSelector: (state: AppState) => Colors = createSelector(
(state: AppState) => state.globalThemeInfo.activeTheme,
(theme: ?GlobalTheme) => {
const explicitTheme = theme ? theme : 'light';
return colors[explicitTheme];
},
);
const magicStrings = new Set();
for (const theme in colors) {
for (const magicString in colors[theme]) {
magicStrings.add(magicString);
}
}
type Styles = { [name: string]: { [field: string]: mixed } };
type ReplaceField = (input: any) => any;
export type StyleSheetOf = $ObjMap;
function stylesFromColors(
obj: IS,
themeColors: Colors,
): StyleSheetOf {
const result = {};
for (const key in obj) {
const style = obj[key];
const filledInStyle = { ...style };
for (const styleKey in style) {
const styleValue = style[styleKey];
if (typeof styleValue !== 'string') {
continue;
}
if (magicStrings.has(styleValue)) {
const mapped = themeColors[styleValue];
if (mapped) {
filledInStyle[styleKey] = mapped;
}
}
}
result[key] = filledInStyle;
}
return StyleSheet.create(result);
}
function styleSelector(
obj: IS,
): (state: AppState) => StyleSheetOf {
return createSelector(colorsSelector, (themeColors: Colors) =>
stylesFromColors(obj, themeColors),
);
}
function useStyles(obj: IS): StyleSheetOf {
const ourColors = useColors();
return React.useMemo(() => stylesFromColors(obj, ourColors), [
obj,
ourColors,
]);
}
function useOverlayStyles(obj: IS): StyleSheetOf {
const navContext = React.useContext(NavContext);
const navigationState = navContext && navContext.state;
const theme = useSelector(
(state: AppState) => state.globalThemeInfo.activeTheme,
);
const backgroundIsDark = React.useMemo(
() => selectBackgroundIsDark(navigationState, theme),
[navigationState, theme],
);
const syntheticTheme = backgroundIsDark ? 'dark' : 'light';
return React.useMemo(() => stylesFromColors(obj, colors[syntheticTheme]), [
obj,
syntheticTheme,
]);
}
function useColors(): Colors {
return useSelector(colorsSelector);
}
function getStylesForTheme(
obj: IS,
theme: GlobalTheme,
): StyleSheetOf {
return stylesFromColors(obj, colors[theme]);
}
export type IndicatorStyle = 'white' | 'black';
function useIndicatorStyle(): IndicatorStyle {
const theme = useSelector(
(state: AppState) => state.globalThemeInfo.activeTheme,
);
return theme && theme === 'dark' ? 'white' : 'black';
}
const indicatorStyleSelector: (
state: AppState,
) => IndicatorStyle = createSelector(
(state: AppState) => state.globalThemeInfo.activeTheme,
(theme: ?GlobalTheme) => {
return theme && theme === 'dark' ? 'white' : 'black';
},
);
export type KeyboardAppearance = 'default' | 'light' | 'dark';
const keyboardAppearanceSelector: (
state: AppState,
) => KeyboardAppearance = createSelector(
(state: AppState) => state.globalThemeInfo.activeTheme,
(theme: ?GlobalTheme) => {
return theme && theme === 'dark' ? 'dark' : 'light';
},
);
function useKeyboardAppearance(): KeyboardAppearance {
return useSelector(keyboardAppearanceSelector);
}
export {
colors,
colorsSelector,
styleSelector,
useStyles,
useOverlayStyles,
useColors,
getStylesForTheme,
useIndicatorStyle,
indicatorStyleSelector,
useKeyboardAppearance,
};