diff --git a/native/chat/chat-thread-list-item.react.js b/native/chat/chat-thread-list-item.react.js
index 67940afaf..c95bf8082 100644
--- a/native/chat/chat-thread-list-item.react.js
+++ b/native/chat/chat-thread-list-item.react.js
@@ -1,297 +1,328 @@
// @flow
+import Icon from '@expo/vector-icons/FontAwesome.js';
import * as React from 'react';
import { Text, View } from 'react-native';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js';
import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
+import { threadTypeIsThick } from 'lib/types/thread-types-enum.js';
import type { UserInfo } from 'lib/types/user-types.js';
import { shortAbsoluteDate } from 'lib/utils/date-utils.js';
import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react.js';
import ChatThreadListSidebar from './chat-thread-list-sidebar.react.js';
import MessagePreview from './message-preview.react.js';
import SwipeableThread from './swipeable-thread.react.js';
import ThreadAvatar from '../avatars/thread-avatar.react.js';
import Button from '../components/button.react.js';
import SingleLine from '../components/single-line.react.js';
import ThreadAncestorsLabel from '../components/thread-ancestors-label.react.js';
import UnreadDot from '../components/unread-dot.react.js';
import { useColors, useStyles } from '../themes/colors.js';
type Props = {
+data: ChatThreadItem,
+onPressItem: (
threadInfo: ThreadInfo,
pendingPersonalThreadUserInfo?: UserInfo,
) => void,
+onPressSeeMoreSidebars: (threadInfo: ThreadInfo) => void,
+onSwipeableWillOpen: (threadInfo: ThreadInfo) => void,
+currentlyOpenedSwipeableId: string,
};
function ChatThreadListItem({
data,
onPressItem,
onPressSeeMoreSidebars,
onSwipeableWillOpen,
currentlyOpenedSwipeableId,
}: Props): React.Node {
const styles = useStyles(unboundStyles);
const colors = useColors();
const lastMessage = React.useMemo(() => {
const mostRecentMessageInfo = data.mostRecentMessageInfo;
if (!mostRecentMessageInfo) {
return (
No messages
);
}
return (
);
}, [data.mostRecentMessageInfo, data.threadInfo, styles]);
const numOfSidebarsWithExtendedArrow =
data.sidebars.filter(sidebarItem => sidebarItem.type === 'sidebar').length -
1;
const sidebars = React.useMemo(
() =>
data.sidebars.map((sidebarItem, index) => {
if (sidebarItem.type === 'sidebar') {
const { type, ...sidebarInfo } = sidebarItem;
return (
);
} else if (sidebarItem.type === 'seeMore') {
return (
);
} else {
return ;
}
}),
[
currentlyOpenedSwipeableId,
data.sidebars,
data.threadInfo,
numOfSidebarsWithExtendedArrow,
onPressItem,
onPressSeeMoreSidebars,
onSwipeableWillOpen,
styles.spacer,
],
);
const onPress = React.useCallback(() => {
onPressItem(data.threadInfo, data.pendingPersonalThreadUserInfo);
}, [onPressItem, data.threadInfo, data.pendingPersonalThreadUserInfo]);
const threadNameStyle = React.useMemo(() => {
if (!data.threadInfo.currentUser.unread) {
return styles.threadName;
}
return [styles.threadName, styles.unreadThreadName];
}, [
data.threadInfo.currentUser.unread,
styles.threadName,
styles.unreadThreadName,
]);
const lastActivity = shortAbsoluteDate(data.lastUpdatedTime);
const lastActivityStyle = React.useMemo(() => {
if (!data.threadInfo.currentUser.unread) {
return styles.lastActivity;
}
return [styles.lastActivity, styles.unreadLastActivity];
}, [
data.threadInfo.currentUser.unread,
styles.lastActivity,
styles.unreadLastActivity,
]);
const resolvedThreadInfo = useResolvedThreadInfo(data.threadInfo);
const unreadDot = React.useMemo(
() => (
),
[data.threadInfo.currentUser.unread, styles.avatarContainer],
);
const threadAvatar = React.useMemo(
() => (
),
[data.threadInfo, styles.avatarContainer],
);
+ const isThick = threadTypeIsThick(data.threadInfo.type);
+ const iconStyle = data.threadInfo.currentUser.unread
+ ? styles.iconUnread
+ : styles.iconRead;
+
+ const iconName = isThick ? 'lock' : 'server';
+
const threadDetails = React.useMemo(
() => (
-
+
+
+
+
+
+
{resolvedThreadInfo.uiName}
{lastMessage}
{lastActivity}
),
[
+ iconStyle,
data.threadInfo,
+ iconName,
lastActivity,
lastActivityStyle,
lastMessage,
resolvedThreadInfo.uiName,
+ styles.header,
+ styles.iconContainer,
styles.row,
styles.threadDetails,
threadNameStyle,
],
);
const swipeableThreadContent = React.useMemo(
() => (
),
[
colors.listIosHighlightUnderlay,
onPress,
styles.container,
styles.content,
threadAvatar,
threadDetails,
unreadDot,
],
);
const swipeableThread = React.useMemo(
() => (
{swipeableThreadContent}
),
[
currentlyOpenedSwipeableId,
data.mostRecentNonLocalMessage,
data.threadInfo,
onSwipeableWillOpen,
swipeableThreadContent,
],
);
const chatThreadListItem = React.useMemo(
() => (
<>
{swipeableThread}
{sidebars}
>
),
[sidebars, swipeableThread],
);
return chatThreadListItem;
}
const chatThreadListItemHeight = 70;
const spacerHeight = 6;
const unboundStyles = {
container: {
height: chatThreadListItemHeight,
justifyContent: 'center',
backgroundColor: 'listBackground',
},
content: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
avatarContainer: {
marginLeft: 6,
marginBottom: 12,
},
threadDetails: {
paddingLeft: 12,
paddingRight: 18,
justifyContent: 'center',
flex: 1,
marginTop: 5,
},
lastActivity: {
color: 'listForegroundTertiaryLabel',
fontSize: 14,
marginLeft: 10,
},
unreadLastActivity: {
color: 'listForegroundLabel',
fontWeight: 'bold',
},
noMessages: {
color: 'listForegroundTertiaryLabel',
flex: 1,
fontSize: 14,
fontStyle: 'italic',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
threadName: {
color: 'listForegroundSecondaryLabel',
flex: 1,
fontSize: 21,
},
unreadThreadName: {
color: 'listForegroundLabel',
fontWeight: '500',
},
spacer: {
height: spacerHeight,
},
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ iconContainer: {
+ marginRight: 6,
+ },
+ iconRead: {
+ color: 'listForegroundTertiaryLabel',
+ },
+ iconUnread: {
+ color: 'listForegroundLabel',
+ },
};
export { ChatThreadListItem, chatThreadListItemHeight, spacerHeight };
diff --git a/native/components/thread-ancestors-label.react.js b/native/components/thread-ancestors-label.react.js
index 5a03f56f5..a556e366f 100644
--- a/native/components/thread-ancestors-label.react.js
+++ b/native/components/thread-ancestors-label.react.js
@@ -1,79 +1,81 @@
// @flow
import Icon from '@expo/vector-icons/FontAwesome5.js';
import * as React from 'react';
import { Text, View } from 'react-native';
import { useAncestorThreads } from 'lib/shared/ancestor-threads.js';
import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
+import { threadTypeIsThick } from 'lib/types/thread-types-enum.js';
import { useResolvedThreadInfos } from 'lib/utils/entity-helpers.js';
import { useColors, useStyles } from '../themes/colors.js';
type Props = {
+threadInfo: ThreadInfo,
};
function ThreadAncestorsLabel(props: Props): React.Node {
const { threadInfo } = props;
const { unread } = threadInfo.currentUser;
const styles = useStyles(unboundStyles);
const colors = useColors();
const ancestorThreads = useAncestorThreads(threadInfo);
const resolvedAncestorThreads = useResolvedThreadInfos(ancestorThreads);
const chevronIcon = React.useMemo(
() => (
),
[colors.listForegroundTertiaryLabel],
);
const ancestorPath = React.useMemo(() => {
const path: Array = [];
for (const thread of resolvedAncestorThreads) {
path.push({thread.uiName});
path.push(
${thread.id}`} style={styles.chevron}>
{chevronIcon}
,
);
}
path.pop();
return path;
}, [resolvedAncestorThreads, chevronIcon, styles.chevron]);
const ancestorPathStyle = React.useMemo(() => {
return unread ? [styles.pathText, styles.unread] : styles.pathText;
}, [styles.pathText, styles.unread, unread]);
- const threadAncestorsLabel = React.useMemo(
- () => (
+ const isThick = threadTypeIsThick(threadInfo.type);
+
+ return React.useMemo(() => {
+ const label = isThick ? 'Local DM' : ancestorPath;
+
+ return (
- {ancestorPath}
+ {label}
- ),
- [ancestorPath, ancestorPathStyle],
- );
-
- return threadAncestorsLabel;
+ );
+ }, [ancestorPath, ancestorPathStyle, isThick]);
}
const unboundStyles = {
pathText: {
opacity: 0.8,
fontSize: 12,
color: 'listForegroundTertiaryLabel',
},
unread: {
color: 'listForegroundLabel',
},
chevron: {
paddingHorizontal: 3,
},
};
export default ThreadAncestorsLabel;
diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js
index 1dc0c9eb3..7a7309060 100644
--- a/web/chat/chat-thread-list-item.react.js
+++ b/web/chat/chat-thread-list-item.react.js
@@ -1,161 +1,178 @@
// @flow
+import {
+ faServer as server,
+ faLock as lock,
+} from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import * as React from 'react';
import SWMansionIcon from 'lib/components/swmansion-icon.react.js';
import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js';
import { useAncestorThreads } from 'lib/shared/ancestor-threads.js';
+import { threadTypeIsThick } from 'lib/types/thread-types-enum.js';
import { shortAbsoluteDate } from 'lib/utils/date-utils.js';
import {
useResolvedThreadInfo,
useResolvedThreadInfos,
} from 'lib/utils/entity-helpers.js';
import ChatThreadListItemMenu from './chat-thread-list-item-menu.react.js';
import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react.js';
import ChatThreadListSidebar from './chat-thread-list-sidebar.react.js';
import css from './chat-thread-list.css';
import MessagePreview from './message-preview.react.js';
import ThreadAvatar from '../avatars/thread-avatar.react.js';
import { useSelector } from '../redux/redux-utils.js';
import {
useOnClickThread,
useThreadIsActive,
} from '../selectors/thread-selectors.js';
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 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);
+ const isThick = threadTypeIsThick(threadInfo.type);
+
+ const iconClass = unread ? css.iconUnread : css.iconRead;
+ const icon = isThick ? lock : server;
+ const breadCrumbs = isThick ? 'Local DM' : ancestorPath;
+
return (
<>
{sidebars}
>
);
}
export default ChatThreadListItem;
diff --git a/web/chat/chat-thread-list.css b/web/chat/chat-thread-list.css
index 323c879b6..7c74999cf 100644
--- a/web/chat/chat-thread-list.css
+++ b/web/chat/chat-thread-list.css
@@ -1,286 +1,304 @@
.thread {
display: flex;
flex-direction: row;
}
.sidebarItem {
display: flex;
flex-direction: row;
width: 100%;
}
.threadListSidebar {
display: flex;
flex-direction: row;
height: 32px;
padding-right: 8px;
position: relative;
cursor: pointer;
}
.threadListSidebar > svg {
position: absolute;
top: -7px;
left: 30px;
}
.activeThread,
.threadListSidebar:hover {
background: var(--thread-active-bg);
}
.activeThread :is(.title, .lastMessage, .lastMessage *) {
color: var(--chat-thread-list-color-active);
}
.activeThread.thread:hover {
background: var(--thread-active-bg);
}
.thread:hover {
background: var(--thread-hover-bg);
}
div.title {
flex: 1;
font-size: var(--m-font-16);
font-weight: var(--semi-bold);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--thread-color-read);
line-height: var(--line-height-text);
}
.threadButton {
flex: 1;
cursor: pointer;
overflow: hidden;
padding-left: 12px;
}
.threadButton + div {
display: flex;
flex-direction: column;
}
.threadButtonSidebar {
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
padding-left: 12px;
}
p.breadCrumbs {
display: flex;
padding: 8px 0 2px 0;
font-size: var(--xs-font-12);
font-weight: var(--normal);
color: var(--breadcrumb-color);
}
p.breadCrumbs.unread {
color: var(--breadcrumb-color-unread);
}
span.breadCrumb {
display: flex;
align-items: center;
white-space: nowrap;
text-overflow: ellipsis;
}
span.breadCrumb svg {
margin-left: 4px;
margin-right: 4px;
}
div.colorContainer {
display: flex;
padding-top: 8px;
}
div.spacer {
width: 42px;
border-radius: 1.68px;
}
div.avatarContainer {
height: 42px;
display: flex;
}
div.lastActivity {
font-size: var(--xxs-font-10);
color: var(--fg);
line-height: 1.5;
padding-right: 16px;
font-weight: var(--semi-bold);
white-space: nowrap;
flex-grow: 1;
padding-bottom: 12px;
align-items: flex-end;
display: flex;
}
div.lastMessage {
font-size: var(--s-font-14);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: var(--line-height-text);
padding-bottom: 8px;
}
div.unread {
color: var(--fg);
font-weight: var(--semi-bold);
}
div.dark {
color: var(--thread-color-read);
padding-right: 16px;
}
.messagePreviewPrimary {
color: var(--thread-color-read);
}
.messagePreviewSecondary {
color: var(--thread-preview-secondary);
}
div.dotContainer {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
}
div.unreadDot {
height: 4px;
width: 4px;
background: var(--fg);
border-radius: 15px;
align-self: center;
}
div.italic {
font-style: italic;
}
div.sidebarTitle {
flex: 1;
font-size: var(--s-font-14);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--thread-color-read);
align-self: flex-start;
}
.threadListSidebar > div.dotContainer {
width: 16px;
}
div.sidebarTitle.unread {
color: var(--fg);
}
div.seeMoreButton {
display: flex;
align-items: center;
padding-left: 22px;
}
div.seeMoreText {
padding-left: 14px;
}
div.sidebar .menu > button svg {
font-size: 16px;
color: var(--thread-color-read);
}
div.sidebar .menu {
opacity: 0;
}
div.sidebar:hover .menu {
display: flex;
align-self: flex-end;
opacity: 1;
}
.menu {
position: relative;
display: flex;
justify-content: flex-end;
}
.menu > button {
background-color: transparent;
color: var(--thread-color-read);
border: none;
cursor: pointer;
display: flex;
align-items: center;
}
.menu > button:focus {
outline: none;
}
.menuContent {
display: none;
position: absolute;
top: calc(100% + 1px);
right: 0;
z-index: 1;
width: max-content;
overflow: hidden;
background-color: #eeeeee;
border-radius: 5px;
box-shadow: 1px 1px 5px 2px #00000022;
}
.menuContentVisible {
display: block;
}
button.menuContent {
border: none;
cursor: pointer;
padding: 10px;
font-size: 16px;
}
button.menuContent:hover {
background-color: #dddddd;
}
ul.list {
margin: 5px 3px 10px 0;
overflow: auto;
}
div.spacer {
height: 6px;
}
div.emptyItemContainer {
display: flex;
flex-direction: column;
align-items: center;
}
div.emptyItemText {
padding: 16px;
font-size: 16px;
text-align: center;
white-space: pre-wrap;
color: var(--fg);
}
div.threadListContainer {
display: flex;
flex-direction: column;
overflow: hidden;
flex: 1;
}
div.createNewThread {
display: flex;
flex-direction: column;
align-items: stretch;
padding: 8px;
}
img.longArrow {
height: 40px;
width: 25px;
position: absolute;
left: 28.5px;
top: -18px;
}
img.arrow {
position: absolute;
left: 28px;
top: -10px;
}
.searchBarContainer {
padding: 1rem;
}
+
+.iconRead {
+ color: var(--breadcrumb-color);
+}
+
+.iconUnread {
+ color: var(--breadcrumb-color-unread);
+}
+
+.iconWrapper {
+ padding: 7px 6px 2px 0;
+}
+
+.header {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}