Bluesky app fork with some witchin' additions ๐Ÿ’ซ

[๐Ÿด] Chat muted state (#3988)

* separate out chatlistitem and add muted icon

* move bell icon to the right of the timeelapsed

authored by samuel.fm and committed by

GitHub bffb9b59 9173be68

+222 -184
+1
assets/icons/bellOff_filled_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="m19.785 8.815 1.034 7.761L7.595 3.352a7.853 7.853 0 0 1 12.19 5.463ZM4 19h3.354c.904 1.748 2.607 3 4.646 3 2.038 0 3.742-1.252 4.646-3h.94l2.707 2.707a1 1 0 0 0 1.414-1.414l-18-18a1 1 0 0 0-1.414 1.414l2.666 2.666a7.842 7.842 0 0 0-.743 2.442l-1.207 9.053A1 1 0 0 0 4 19Zm8 1c-.823 0-1.613-.363-2.222-1h4.443c-.608.637-1.398 1-2.221 1Z" clip-rule="evenodd"/></svg>
+1
assets/icons/bellOff_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M2.293 2.293a1 1 0 0 1 1.414 0l18 18a1 1 0 0 1-1.414 1.414L17.586 19h-.94c-.904 1.748-2.607 3-4.646 3-2.039 0-3.742-1.252-4.646-3H4a1 1 0 0 1-.991-1.132l1.207-9.053c.116-.87.372-1.69.743-2.442L2.293 3.707a1 1 0 0 1 0-1.414Zm4.19 5.604c-.134.376-.23.772-.285 1.183L5.142 17h10.444L6.483 7.897ZM9.778 19c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1H9.778ZM8.834 2.666a7.853 7.853 0 0 1 10.95 6.15l.645 4.832a1 1 0 0 1-1.983.265l-.644-4.833A5.853 5.853 0 0 0 9.64 4.495a1 1 0 0 1-.807-1.83Z" clip-rule="evenodd"/></svg>
+8
src/components/icons/Bell2.tsx
··· 7 7 export const Bell2_Filled_Corner0_Rounded = createSinglePathSVG({ 8 8 path: 'M12 2a7.307 7.307 0 0 0-7.298 6.943l-.19 3.798-1.321 2.641A1.809 1.809 0 0 0 4.809 18H7.1a5.002 5.002 0 0 0 9.8 0h2.291a1.809 1.809 0 0 0 1.618-2.618l-1.32-2.641-.19-3.798A7.308 7.308 0 0 0 12 2Zm0 18a3.001 3.001 0 0 1-2.83-2h5.66A3.001 3.001 0 0 1 12 20Z', 9 9 }) 10 + 11 + export const Bell2Off_Stroke2_Corner0_Rounded = createSinglePathSVG({ 12 + path: 'M2.293 2.293a1 1 0 0 1 1.414 0l18 18a1 1 0 0 1-1.414 1.414L17.586 19h-.94c-.904 1.748-2.607 3-4.646 3-2.039 0-3.742-1.252-4.646-3H4a1 1 0 0 1-.991-1.132l1.207-9.053c.116-.87.372-1.69.743-2.442L2.293 3.707a1 1 0 0 1 0-1.414Zm4.19 5.604c-.134.376-.23.772-.285 1.183L5.142 17h10.444L6.483 7.897ZM9.778 19c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1H9.778ZM8.834 2.666a7.853 7.853 0 0 1 10.95 6.15l.645 4.832a1 1 0 0 1-1.983.265l-.644-4.833A5.853 5.853 0 0 0 9.64 4.495a1 1 0 0 1-.807-1.83Z', 13 + }) 14 + 15 + export const Bell2Off_Filled_Corner0_Rounded = createSinglePathSVG({ 16 + path: 'm19.785 8.815 1.034 7.761L7.595 3.352a7.853 7.853 0 0 1 12.19 5.463ZM4 19h3.354c.904 1.748 2.607 3 4.646 3 2.038 0 3.742-1.252 4.646-3h.94l2.707 2.707a1 1 0 0 0 1.414-1.414l-18-18a1 1 0 0 0-1.414 1.414l2.666 2.666a7.842 7.842 0 0 0-.743 2.442l-1.207 9.053A1 1 0 0 0 4 19Zm8 1c-.823 0-1.613-.363-2.222-1h4.443c-.608.637-1.398 1-2.221 1Z', 17 + })
+209
src/screens/Messages/List/ChatListItem.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {ChatBskyConvoDefs} from '@atproto-labs/api' 4 + import {msg} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + import {useNavigation} from '@react-navigation/native' 7 + 8 + import {NavigationProp} from '#/lib/routes/types' 9 + import {isNative} from '#/platform/detection' 10 + import {useSession} from '#/state/session' 11 + import {TimeElapsed} from '#/view/com/util/TimeElapsed' 12 + import {UserAvatar} from '#/view/com/util/UserAvatar' 13 + import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 14 + import {Button} from '#/components/Button' 15 + import {ConvoMenu} from '#/components/dms/ConvoMenu' 16 + import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' 17 + import {useMenuControl} from '#/components/Menu' 18 + import {Text} from '#/components/Typography' 19 + 20 + export function ChatListItem({ 21 + convo, 22 + index, 23 + }: { 24 + convo: ChatBskyConvoDefs.ConvoView 25 + index: number 26 + }) { 27 + const t = useTheme() 28 + const {_} = useLingui() 29 + const {currentAccount} = useSession() 30 + const menuControl = useMenuControl() 31 + const {gtMobile} = useBreakpoints() 32 + 33 + let lastMessage = _(msg`No messages yet`) 34 + let lastMessageSentAt: string | null = null 35 + if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) { 36 + if (convo.lastMessage.sender?.did === currentAccount?.did) { 37 + lastMessage = _(msg`You: ${convo.lastMessage.text}`) 38 + } else { 39 + lastMessage = convo.lastMessage.text 40 + } 41 + lastMessageSentAt = convo.lastMessage.sentAt 42 + } 43 + if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) { 44 + lastMessage = _(msg`Message deleted`) 45 + } 46 + 47 + const otherUser = convo.members.find( 48 + member => member.did !== currentAccount?.did, 49 + ) 50 + 51 + const navigation = useNavigation<NavigationProp>() 52 + const [showActions, setShowActions] = React.useState(false) 53 + 54 + const onMouseEnter = React.useCallback(() => { 55 + setShowActions(true) 56 + }, []) 57 + 58 + const onMouseLeave = React.useCallback(() => { 59 + setShowActions(false) 60 + }, []) 61 + 62 + const onFocus = React.useCallback<React.FocusEventHandler>(e => { 63 + if (e.nativeEvent.relatedTarget == null) return 64 + setShowActions(true) 65 + }, []) 66 + 67 + const onPress = React.useCallback(() => { 68 + navigation.push('MessagesConversation', { 69 + conversation: convo.id, 70 + }) 71 + }, [convo.id, navigation]) 72 + 73 + if (!otherUser) { 74 + return null 75 + } 76 + 77 + return ( 78 + <View 79 + // @ts-expect-error web only 80 + onMouseEnter={onMouseEnter} 81 + onMouseLeave={onMouseLeave} 82 + onFocus={onFocus} 83 + onBlur={onMouseLeave}> 84 + <Button 85 + label={otherUser.displayName || otherUser.handle} 86 + onPress={onPress} 87 + style={a.flex_1} 88 + onLongPress={isNative ? menuControl.open : undefined}> 89 + {({hovered, pressed}) => ( 90 + <View 91 + style={[ 92 + a.flex_row, 93 + a.flex_1, 94 + a.px_lg, 95 + a.py_md, 96 + a.gap_md, 97 + (hovered || pressed) && t.atoms.bg_contrast_25, 98 + index === 0 && [a.border_t, a.pt_lg], 99 + t.atoms.border_contrast_low, 100 + ]}> 101 + <UserAvatar avatar={otherUser?.avatar} size={52} /> 102 + <View style={[a.flex_1, a.flex_row, a.align_center]}> 103 + <View style={[a.flex_1]}> 104 + <View 105 + style={[ 106 + a.flex_1, 107 + a.flex_row, 108 + a.align_end, 109 + a.pb_2xs, 110 + web([{marginTop: -2}]), 111 + ]}> 112 + <Text 113 + numberOfLines={1} 114 + style={[{maxWidth: '85%'}, web([a.leading_normal])]}> 115 + <Text style={[a.text_md, t.atoms.text, a.font_bold]}> 116 + {otherUser.displayName || otherUser.handle} 117 + </Text> 118 + </Text> 119 + {lastMessageSentAt && ( 120 + <TimeElapsed timestamp={lastMessageSentAt}> 121 + {({timeElapsed}) => ( 122 + <Text 123 + style={[ 124 + a.text_sm, 125 + web([a.leading_normal, {whiteSpace: 'pre'}]), 126 + t.atoms.text_contrast_medium, 127 + ]}> 128 + {' '} 129 + &middot; {timeElapsed} 130 + </Text> 131 + )} 132 + </TimeElapsed> 133 + )} 134 + {convo.muted && ( 135 + <Text 136 + style={[ 137 + a.text_sm, 138 + web([a.leading_normal, {whiteSpace: 'pre'}]), 139 + t.atoms.text_contrast_medium, 140 + ]}> 141 + {' '} 142 + &middot;{' '} 143 + <BellStroke 144 + size="xs" 145 + style={t.atoms.text_contrast_medium} 146 + /> 147 + </Text> 148 + )} 149 + </View> 150 + <Text 151 + numberOfLines={1} 152 + style={[a.text_sm, t.atoms.text_contrast_medium, a.pb_xs]}> 153 + @{otherUser.handle} 154 + </Text> 155 + <Text 156 + numberOfLines={2} 157 + style={[ 158 + a.text_sm, 159 + a.leading_snug, 160 + convo.unreadCount > 0 161 + ? a.font_bold 162 + : t.atoms.text_contrast_high, 163 + convo.muted && t.atoms.text_contrast_medium, 164 + ]}> 165 + {lastMessage} 166 + </Text> 167 + </View> 168 + {convo.unreadCount > 0 && ( 169 + <View 170 + style={[ 171 + a.absolute, 172 + a.rounded_full, 173 + { 174 + backgroundColor: convo.muted 175 + ? t.palette.contrast_200 176 + : t.palette.primary_500, 177 + height: 7, 178 + width: 7, 179 + }, 180 + isNative 181 + ? { 182 + top: 15, 183 + right: 12, 184 + } 185 + : { 186 + top: 0, 187 + right: 0, 188 + }, 189 + ]} 190 + /> 191 + )} 192 + <ConvoMenu 193 + convo={convo} 194 + profile={otherUser} 195 + control={menuControl} 196 + currentScreen="list" 197 + showMarkAsRead={convo.unreadCount > 0} 198 + hideTrigger={isNative} 199 + triggerOpacity={ 200 + !gtMobile || showActions || menuControl.isOpen ? 1 : 0 201 + } 202 + /> 203 + </View> 204 + </View> 205 + )} 206 + </Button> 207 + </View> 208 + ) 209 + }
+3 -184
src/screens/Messages/List/index.tsx
··· 3 3 import {ChatBskyConvoDefs} from '@atproto/api' 4 4 import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 - import {useNavigation} from '@react-navigation/native' 7 6 import {NativeStackScreenProps} from '@react-navigation/native-stack' 8 7 import {sha256} from 'js-sha256' 9 8 10 9 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 11 - import {MessagesTabNavigatorParams, NavigationProp} from '#/lib/routes/types' 10 + import {MessagesTabNavigatorParams} from '#/lib/routes/types' 12 11 import {useGate} from '#/lib/statsig/statsig' 13 12 import {cleanError} from '#/lib/strings/errors' 14 13 import {logger} from '#/logger' 15 - import {isNative} from '#/platform/detection' 16 14 import {useListConvos} from '#/state/queries/messages/list-converations' 17 - import {useSession} from '#/state/session' 18 15 import {List} from '#/view/com/util/List' 19 - import {TimeElapsed} from '#/view/com/util/TimeElapsed' 20 - import {UserAvatar} from '#/view/com/util/UserAvatar' 21 16 import {ViewHeader} from '#/view/com/util/ViewHeader' 22 17 import {CenteredView} from '#/view/com/util/Views' 23 18 import {ScrollView} from '#/view/com/util/Views' 24 - import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 19 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 25 20 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 26 21 import {DialogControlProps, useDialogControl} from '#/components/Dialog' 27 - import {ConvoMenu} from '#/components/dms/ConvoMenu' 28 22 import {NewChat} from '#/components/dms/NewChat' 29 23 import * as TextField from '#/components/forms/TextField' 30 24 import {useRefreshOnFocus} from '#/components/hooks/useRefreshOnFocus' ··· 32 26 import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider' 33 27 import {Link} from '#/components/Link' 34 28 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 35 - import {useMenuControl} from '#/components/Menu' 36 29 import {Text} from '#/components/Typography' 37 30 import {ClipClopGate} from '../gate' 38 31 import {useDmServiceUrlStorage} from '../Temp/useDmServiceUrlStorage' 32 + import {ChatListItem} from './ChatListItem' 39 33 40 34 type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'Messages'> 41 35 ··· 251 245 // @ts-ignore our .web version only -sfn 252 246 desktopFixedHeight 253 247 /> 254 - </View> 255 - ) 256 - } 257 - 258 - function ChatListItem({ 259 - convo, 260 - index, 261 - }: { 262 - convo: ChatBskyConvoDefs.ConvoView 263 - index: number 264 - }) { 265 - const t = useTheme() 266 - const {_} = useLingui() 267 - const {currentAccount} = useSession() 268 - const menuControl = useMenuControl() 269 - const {gtMobile} = useBreakpoints() 270 - 271 - let lastMessage = _(msg`No messages yet`) 272 - let lastMessageSentAt: string | null = null 273 - if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) { 274 - if (convo.lastMessage.sender?.did === currentAccount?.did) { 275 - lastMessage = _(msg`You: ${convo.lastMessage.text}`) 276 - } else { 277 - lastMessage = convo.lastMessage.text 278 - } 279 - lastMessageSentAt = convo.lastMessage.sentAt 280 - } 281 - if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) { 282 - lastMessage = _(msg`Message deleted`) 283 - } 284 - 285 - const otherUser = convo.members.find( 286 - member => member.did !== currentAccount?.did, 287 - ) 288 - 289 - const navigation = useNavigation<NavigationProp>() 290 - const [showActions, setShowActions] = React.useState(false) 291 - 292 - const onMouseEnter = React.useCallback(() => { 293 - setShowActions(true) 294 - }, []) 295 - 296 - const onMouseLeave = React.useCallback(() => { 297 - setShowActions(false) 298 - }, []) 299 - 300 - const onFocus = React.useCallback<React.FocusEventHandler>(e => { 301 - if (e.nativeEvent.relatedTarget == null) return 302 - setShowActions(true) 303 - }, []) 304 - 305 - const onPress = React.useCallback(() => { 306 - navigation.push('MessagesConversation', { 307 - conversation: convo.id, 308 - }) 309 - }, [convo.id, navigation]) 310 - 311 - if (!otherUser) { 312 - return null 313 - } 314 - 315 - return ( 316 - <View 317 - // @ts-expect-error web only 318 - onMouseEnter={onMouseEnter} 319 - onMouseLeave={onMouseLeave} 320 - onFocus={onFocus} 321 - onBlur={onMouseLeave}> 322 - <Button 323 - label={otherUser.displayName || otherUser.handle} 324 - onPress={onPress} 325 - style={a.flex_1} 326 - onLongPress={isNative ? menuControl.open : undefined}> 327 - {({hovered, pressed}) => ( 328 - <View 329 - style={[ 330 - a.flex_row, 331 - a.flex_1, 332 - a.px_lg, 333 - a.py_md, 334 - a.gap_md, 335 - (hovered || pressed) && t.atoms.bg_contrast_25, 336 - index === 0 && [a.border_t, a.pt_lg], 337 - t.atoms.border_contrast_low, 338 - ]}> 339 - <UserAvatar avatar={otherUser?.avatar} size={52} /> 340 - <View style={[a.flex_1, a.flex_row, a.align_center]}> 341 - <View style={[a.flex_1]}> 342 - <View 343 - style={[ 344 - a.flex_1, 345 - a.flex_row, 346 - a.align_end, 347 - a.pb_2xs, 348 - web([{marginTop: -2}]), 349 - ]}> 350 - <Text 351 - numberOfLines={1} 352 - style={[{maxWidth: '85%'}, web([a.leading_normal])]}> 353 - <Text style={[a.text_md, t.atoms.text, a.font_bold]}> 354 - {otherUser.displayName || otherUser.handle} 355 - </Text> 356 - </Text> 357 - {lastMessageSentAt && ( 358 - <TimeElapsed timestamp={lastMessageSentAt}> 359 - {({timeElapsed}) => ( 360 - <Text 361 - style={[ 362 - a.text_sm, 363 - web([a.leading_normal]), 364 - t.atoms.text_contrast_medium, 365 - ]}> 366 - {' '} 367 - &middot; {timeElapsed} 368 - </Text> 369 - )} 370 - </TimeElapsed> 371 - )} 372 - </View> 373 - <Text 374 - numberOfLines={1} 375 - style={[a.text_sm, t.atoms.text_contrast_medium, a.pb_xs]}> 376 - @{otherUser.handle} 377 - </Text> 378 - <Text 379 - numberOfLines={2} 380 - style={[ 381 - a.text_sm, 382 - a.leading_snug, 383 - convo.unreadCount > 0 384 - ? a.font_bold 385 - : t.atoms.text_contrast_high, 386 - ]}> 387 - {lastMessage} 388 - </Text> 389 - </View> 390 - {convo.unreadCount > 0 && ( 391 - <View 392 - style={[ 393 - a.absolute, 394 - a.rounded_full, 395 - { 396 - backgroundColor: convo.muted 397 - ? t.palette.contrast_200 398 - : t.palette.primary_500, 399 - height: 7, 400 - width: 7, 401 - }, 402 - isNative 403 - ? { 404 - top: 15, 405 - right: 12, 406 - } 407 - : { 408 - top: 0, 409 - right: 0, 410 - }, 411 - ]} 412 - /> 413 - )} 414 - <ConvoMenu 415 - convo={convo} 416 - profile={otherUser} 417 - control={menuControl} 418 - currentScreen="list" 419 - showMarkAsRead={convo.unreadCount > 0} 420 - hideTrigger={isNative} 421 - triggerOpacity={ 422 - !gtMobile || showActions || menuControl.isOpen ? 1 : 0 423 - } 424 - /> 425 - </View> 426 - </View> 427 - )} 428 - </Button> 429 248 </View> 430 249 ) 431 250 }