Group Clops Feature Branch (#10360)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import {useCallback, useEffect} from 'react'
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {useEffect} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Animated, {
|
||||
Easing,
|
||||
interpolate,
|
||||
type SharedValue,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
@@ -16,44 +15,32 @@ import {atoms as a, useTheme} from '#/alf'
|
||||
import {Person_Filled_Corner2_Rounded as PersonIcon} from '#/components/icons/Person'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
type Layout = {
|
||||
size: number
|
||||
x: number
|
||||
y: number
|
||||
zIndex?: number
|
||||
border?: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
animate?: boolean
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
size?: 'small' | 'medium' | 'large' | number
|
||||
size?: number
|
||||
}
|
||||
|
||||
export function AvatarBubbles({
|
||||
animate = false,
|
||||
profiles: allProfiles,
|
||||
size = 'large',
|
||||
size = 120,
|
||||
}: Props) {
|
||||
const {currentAccount} = useSession()
|
||||
const profiles =
|
||||
allProfiles.length > 2
|
||||
? allProfiles.filter(p => p.did !== currentAccount?.did)
|
||||
: allProfiles
|
||||
const containerSize =
|
||||
typeof size === 'number'
|
||||
? size
|
||||
: size === 'small'
|
||||
? 40
|
||||
: size === 'medium'
|
||||
? 56
|
||||
: 120
|
||||
const scale =
|
||||
typeof size === 'number'
|
||||
? size / 120
|
||||
: size === 'small'
|
||||
? 40 / 120
|
||||
: size === 'medium'
|
||||
? 56 / 120
|
||||
: 1
|
||||
const marginOffset =
|
||||
(typeof size === 'number' && size < 120) ||
|
||||
size === 'small' ||
|
||||
size === 'medium'
|
||||
? -2
|
||||
: 0
|
||||
const scale = profiles.length <= 1 ? 1 : size / 120
|
||||
const marginOffset = size < 120 ? -2 : 0
|
||||
|
||||
const initialValue = animate ? 0 : 1
|
||||
const p0 = useSharedValue(initialValue)
|
||||
@@ -61,130 +48,50 @@ export function AvatarBubbles({
|
||||
const p2 = useSharedValue(initialValue)
|
||||
const p3 = useSharedValue(initialValue)
|
||||
|
||||
const animateScale = (p: Animated.SharedValue<number>, index: number) => {
|
||||
p.set(0)
|
||||
p.set(() =>
|
||||
withDelay(
|
||||
500 + index * 100,
|
||||
withTiming(1, {
|
||||
duration: 250,
|
||||
easing: Easing.out(Easing.back(1.75)),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const playScaleAnimation = useCallback(() => {
|
||||
animateScale(p0, 0)
|
||||
animateScale(p1, 1)
|
||||
animateScale(p2, 2)
|
||||
animateScale(p3, 3)
|
||||
}, [p0, p1, p2, p3])
|
||||
|
||||
useEffect(() => {
|
||||
if (!animate) return
|
||||
playScaleAnimation()
|
||||
}, [animate, playScaleAnimation])
|
||||
const animateBubble = (p: SharedValue<number>, i: number) => {
|
||||
p.set(0)
|
||||
p.set(() =>
|
||||
withDelay(
|
||||
500 + i * 100,
|
||||
withTiming(1, {
|
||||
duration: 250,
|
||||
easing: Easing.out(Easing.back(1.75)),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
animateBubble(p0, 0)
|
||||
animateBubble(p1, 1)
|
||||
animateBubble(p2, 2)
|
||||
animateBubble(p3, 3)
|
||||
}, [animate, p0, p1, p2, p3])
|
||||
|
||||
let avatars = (
|
||||
<>
|
||||
<AvatarBubble
|
||||
profile={profiles[0]}
|
||||
scale={p0}
|
||||
size={76}
|
||||
x={-2}
|
||||
y={-2}
|
||||
style={[a.z_20]}
|
||||
includeProfileBorder
|
||||
/>
|
||||
<AvatarBubble
|
||||
profile={profiles[1]}
|
||||
scale={p1}
|
||||
size={76}
|
||||
x={42}
|
||||
y={42}
|
||||
style={[a.z_10]}
|
||||
includeProfileBorder
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
if (profiles.length === 3) {
|
||||
avatars = (
|
||||
<>
|
||||
<AvatarBubble
|
||||
profile={profiles[0]}
|
||||
scale={p0}
|
||||
size={68}
|
||||
x={-2}
|
||||
y={-2}
|
||||
/>
|
||||
<AvatarBubble
|
||||
profile={profiles[1]}
|
||||
scale={p1}
|
||||
size={56}
|
||||
x={38}
|
||||
y={62}
|
||||
/>
|
||||
<AvatarBubble
|
||||
profile={profiles[2]}
|
||||
scale={p2}
|
||||
size={46}
|
||||
x={71}
|
||||
y={18}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (profiles.length >= 4) {
|
||||
avatars = (
|
||||
<>
|
||||
<AvatarBubble
|
||||
profile={profiles[0]}
|
||||
scale={p0}
|
||||
size={68}
|
||||
x={-2}
|
||||
y={-2}
|
||||
/>
|
||||
<AvatarBubble
|
||||
profile={profiles[1]}
|
||||
scale={p1}
|
||||
size={56}
|
||||
x={60}
|
||||
y={49}
|
||||
/>
|
||||
<AvatarBubble
|
||||
profile={profiles[2]}
|
||||
scale={p2}
|
||||
size={42}
|
||||
x={14}
|
||||
y={74}
|
||||
/>
|
||||
<AvatarBubble profile={profiles[3]} scale={p3} size={32} x={72} y={9} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
const scales = [p0, p1, p2, p3]
|
||||
const layouts = getLayouts(profiles.length)
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
a.p_2xs,
|
||||
{
|
||||
height: containerSize,
|
||||
width: containerSize,
|
||||
},
|
||||
]}>
|
||||
<Animated.View style={[a.p_2xs, {height: size, width: size}]}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
marginTop: marginOffset,
|
||||
marginLeft: marginOffset,
|
||||
transform: [{scale}],
|
||||
transformOrigin: 'top left',
|
||||
},
|
||||
]}>
|
||||
{avatars}
|
||||
style={{
|
||||
marginTop: marginOffset,
|
||||
marginLeft: marginOffset,
|
||||
transform: [{scale}],
|
||||
transformOrigin: 'top left',
|
||||
}}>
|
||||
{layouts.map((layout, i) => (
|
||||
<AvatarBubble
|
||||
key={i}
|
||||
profile={profiles[i]}
|
||||
scale={scales[i]}
|
||||
size={layout.size}
|
||||
x={layout.x}
|
||||
y={layout.y}
|
||||
zIndex={layout.zIndex}
|
||||
includeProfileBorder={layout.border}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</Animated.View>
|
||||
)
|
||||
@@ -194,27 +101,23 @@ function AvatarBubble({
|
||||
profile,
|
||||
scale,
|
||||
size,
|
||||
style,
|
||||
x,
|
||||
y,
|
||||
zIndex,
|
||||
includeProfileBorder,
|
||||
}: {
|
||||
profile?: bsky.profile.AnyProfileView
|
||||
scale: SharedValue<number>
|
||||
size: number
|
||||
style?: StyleProp<ViewStyle>
|
||||
x: number
|
||||
y: number
|
||||
zIndex?: number
|
||||
includeProfileBorder?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{translateX: x},
|
||||
{translateY: y},
|
||||
{scale: interpolate(scale.get(), [0, 1], [0, 1])},
|
||||
],
|
||||
transform: [{translateX: x}, {translateY: y}, {scale: scale.get()}],
|
||||
}))
|
||||
|
||||
return (
|
||||
@@ -227,11 +130,17 @@ function AvatarBubble({
|
||||
borderColor: t.atoms.text_inverted.color,
|
||||
borderWidth: 2,
|
||||
},
|
||||
style,
|
||||
zIndex != null && {zIndex},
|
||||
animatedStyle,
|
||||
]}>
|
||||
{profile ? (
|
||||
<Avatar profile={profile} size={size} />
|
||||
<UserAvatar
|
||||
avatar={profile.avatar}
|
||||
size={size}
|
||||
type="user"
|
||||
hideLiveBadge
|
||||
noBorder
|
||||
/>
|
||||
) : (
|
||||
<AvatarPlaceholder size={size} />
|
||||
)}
|
||||
@@ -239,25 +148,7 @@ function AvatarBubble({
|
||||
)
|
||||
}
|
||||
|
||||
function Avatar({
|
||||
profile,
|
||||
size = 76,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
size?: number
|
||||
}) {
|
||||
return (
|
||||
<UserAvatar
|
||||
avatar={profile.avatar}
|
||||
size={size}
|
||||
type="user"
|
||||
hideLiveBadge
|
||||
noBorder
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarPlaceholder({size = 76}: {size?: number}) {
|
||||
function AvatarPlaceholder({size}: {size: number}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
@@ -267,10 +158,7 @@ function AvatarPlaceholder({size = 76}: {size?: number}) {
|
||||
a.justify_center,
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_200,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
},
|
||||
{width: size, height: size},
|
||||
]}>
|
||||
<PersonIcon
|
||||
width={size * 0.5}
|
||||
@@ -280,3 +168,25 @@ function AvatarPlaceholder({size = 76}: {size?: number}) {
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function getLayouts(count: number): Layout[] {
|
||||
if (count === 3) {
|
||||
return [
|
||||
{size: 68, x: -2, y: -2},
|
||||
{size: 56, x: 38, y: 62},
|
||||
{size: 46, x: 71, y: 18},
|
||||
]
|
||||
}
|
||||
if (count >= 4) {
|
||||
return [
|
||||
{size: 68, x: -2, y: -2},
|
||||
{size: 56, x: 60, y: 49},
|
||||
{size: 42, x: 14, y: 74},
|
||||
{size: 32, x: 72, y: 9},
|
||||
]
|
||||
}
|
||||
return [
|
||||
{size: 76, x: -2, y: -2, zIndex: 20, border: true},
|
||||
{size: 76, x: 42, y: 42, zIndex: 10, border: true},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -77,6 +77,10 @@ export type ButtonState = {
|
||||
focused: boolean
|
||||
pressed: boolean
|
||||
disabled: boolean
|
||||
/**
|
||||
* Alias for hovered || focused || pressed
|
||||
*/
|
||||
interacting: boolean
|
||||
}
|
||||
|
||||
export type ButtonContext = VariantProps & ButtonState
|
||||
@@ -120,6 +124,7 @@ const Context = createContext<VariantProps & ButtonState>({
|
||||
focused: false,
|
||||
pressed: false,
|
||||
disabled: false,
|
||||
interacting: false,
|
||||
})
|
||||
Context.displayName = 'ButtonContext'
|
||||
|
||||
@@ -536,6 +541,7 @@ export const Button = forwardRef<View, ButtonProps>(
|
||||
const context = useMemo<ButtonContext>(
|
||||
() => ({
|
||||
...state,
|
||||
interacting: state.hovered || state.focused || state.pressed,
|
||||
variant,
|
||||
color,
|
||||
size,
|
||||
|
||||
@@ -201,7 +201,7 @@ export function AvatarPlaceholder({size = 40}: {size?: number}) {
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
@@ -348,7 +348,7 @@ export function NameAndHandlePlaceholder() {
|
||||
<View
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
width: '60%',
|
||||
height: 14,
|
||||
@@ -359,7 +359,7 @@ export function NameAndHandlePlaceholder() {
|
||||
<View
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
width: '40%',
|
||||
height: 10,
|
||||
@@ -377,7 +377,7 @@ export function NamePlaceholder({style}: ViewStyleProp) {
|
||||
<View
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
width: '60%',
|
||||
height: 14,
|
||||
@@ -439,7 +439,7 @@ export function DescriptionPlaceholder({
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
a.w_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
{height: 12, width: i + 1 === numberOfLines ? '60%' : '100%'},
|
||||
]}
|
||||
/>
|
||||
@@ -600,7 +600,7 @@ export function FollowButtonPlaceholder({style}: ViewStyleProp) {
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
a.w_full,
|
||||
{
|
||||
height: 33,
|
||||
|
||||
@@ -512,7 +512,7 @@ function ExistingChatCard({
|
||||
]}>
|
||||
<ProfileCard.Header>
|
||||
{convo.kind === 'group' ? (
|
||||
<AvatarBubbles profiles={convo.members} size="small" />
|
||||
<AvatarBubbles profiles={convo.members} size={40} />
|
||||
) : (
|
||||
<ProfileCard.Avatar
|
||||
profile={convo.primaryMember}
|
||||
|
||||
@@ -4,23 +4,29 @@ import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export function ActionsWrapper({
|
||||
message,
|
||||
isFromSelf,
|
||||
senderProfile,
|
||||
children,
|
||||
onTap,
|
||||
}: {
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
hasReactions?: boolean
|
||||
isFromSelf: boolean
|
||||
senderProfile?: bsky.profile.AnyProfileView
|
||||
children: React.ReactNode
|
||||
onTap?: () => void
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
return (
|
||||
<MessageContextMenu message={message} onTap={onTap}>
|
||||
<MessageContextMenu
|
||||
message={message}
|
||||
senderProfile={senderProfile}
|
||||
onTap={onTap}>
|
||||
{trigger =>
|
||||
// will always be true, since this file is platform split
|
||||
trigger.IS_NATIVE && (
|
||||
|
||||
@@ -10,6 +10,7 @@ import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
|
||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
|
||||
import {EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {EmojiReactionPicker} from './EmojiReactionPicker'
|
||||
import {hasReachedReactionLimit} from './util'
|
||||
|
||||
@@ -17,12 +18,14 @@ export function ActionsWrapper({
|
||||
message,
|
||||
hasReactions,
|
||||
isFromSelf,
|
||||
senderProfile,
|
||||
children,
|
||||
onTap,
|
||||
}: {
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
hasReactions?: boolean
|
||||
isFromSelf: boolean
|
||||
senderProfile?: bsky.profile.AnyProfileView
|
||||
children: React.ReactNode
|
||||
onTap?: () => void
|
||||
}) {
|
||||
@@ -114,7 +117,7 @@ export function ActionsWrapper({
|
||||
)
|
||||
}}
|
||||
</EmojiReactionPicker>
|
||||
<MessageContextMenu message={message}>
|
||||
<MessageContextMenu message={message} senderProfile={senderProfile}>
|
||||
{({props, state, IS_NATIVE, control}) => {
|
||||
// always false, file is platform split
|
||||
if (IS_NATIVE) return null
|
||||
|
||||
@@ -98,11 +98,16 @@ function reducer(state: State, action: Action): State {
|
||||
}
|
||||
|
||||
export function AddMembersFlow({
|
||||
members,
|
||||
title,
|
||||
onAddMembers,
|
||||
}: {
|
||||
members: string[]
|
||||
title: string
|
||||
onAddMembers: (dids: string[]) => void
|
||||
onAddMembers: (
|
||||
dids: string[],
|
||||
profiles: bsky.profile.AnyProfileView[],
|
||||
) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
@@ -154,7 +159,11 @@ export function AddMembersFlow({
|
||||
} else if (searchText.length) {
|
||||
if (results?.length) {
|
||||
for (const profile of results) {
|
||||
if (profile.did === currentAccount?.did) continue
|
||||
if (
|
||||
profile.did === currentAccount?.did ||
|
||||
members.includes(profile.did)
|
||||
)
|
||||
continue
|
||||
_items.push({
|
||||
type: 'profile',
|
||||
key: profile.did,
|
||||
@@ -202,7 +211,7 @@ export function AddMembersFlow({
|
||||
}
|
||||
|
||||
return _items
|
||||
}, [isError, searchText, l, results, currentAccount?.did, follows])
|
||||
}, [isError, searchText, l, results, currentAccount?.did, members, follows])
|
||||
|
||||
if (searchText && !isFetching && !items.length && !isError) {
|
||||
items.push({type: 'empty', key: 'empty', message: l`No results`})
|
||||
@@ -213,8 +222,8 @@ export function AddMembersFlow({
|
||||
}, [control])
|
||||
|
||||
const handlePressAdd = useCallback(() => {
|
||||
onAddMembers(groupChatDids)
|
||||
}, [groupChatDids, onAddMembers])
|
||||
onAddMembers(groupChatDids, groupChatProfiles)
|
||||
}, [groupChatDids, groupChatProfiles, onAddMembers])
|
||||
|
||||
const renderItems = useCallback(
|
||||
({item}: {item: Item}) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {subDays} from 'date-fns'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '../Typography'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {localDateString} from './util'
|
||||
|
||||
const timeFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
|
||||
@@ -25,15 +25,18 @@ import {usePromptControl} from '#/components/Prompt'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {EmojiReactionPicker} from './EmojiReactionPicker'
|
||||
import {hasReachedReactionLimit} from './util'
|
||||
|
||||
export let MessageContextMenu = ({
|
||||
message,
|
||||
senderProfile,
|
||||
children,
|
||||
onTap,
|
||||
}: {
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
senderProfile?: bsky.profile.AnyProfileView
|
||||
children: TriggerProps['children']
|
||||
onTap?: () => void
|
||||
}): React.ReactNode => {
|
||||
@@ -110,9 +113,7 @@ export let MessageContextMenu = ({
|
||||
[l, convo, message, currentAccount?.did],
|
||||
)
|
||||
|
||||
const sender = convo.convo.members.find(
|
||||
member => member.did === message.sender.did,
|
||||
)
|
||||
const sender = senderProfile
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -183,7 +184,7 @@ export let MessageContextMenu = ({
|
||||
control={reportControl}
|
||||
subject={{
|
||||
view: 'message',
|
||||
convoId: convo.convo.id,
|
||||
convoId: convo.convo.view.id,
|
||||
message,
|
||||
}}
|
||||
onAfterSubmit={() => {
|
||||
@@ -197,7 +198,7 @@ export let MessageContextMenu = ({
|
||||
control={blockOrDeleteControl}
|
||||
currentScreen="conversation"
|
||||
params={{
|
||||
convoId: convo.convo.id,
|
||||
convoId: convo.convo.view.id,
|
||||
message,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {useConvoActive} from '#/state/messages/convo'
|
||||
import {type ConvoItem} from '#/state/messages/convo/types'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
|
||||
@@ -43,7 +42,6 @@ import {InlineLinkText, Link} from '#/components/Link'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {DateDivider} from './DateDivider'
|
||||
import {useDateDividerToggle} from './DateDividerToggle'
|
||||
import {MessageItemEmbed} from './MessageItemEmbed'
|
||||
@@ -93,19 +91,18 @@ function isWithinClusterBoundary({
|
||||
let MessageItem = ({
|
||||
item,
|
||||
isGroupChat = false,
|
||||
profile,
|
||||
}: {
|
||||
item: ConvoItem & {type: 'message' | 'pending-message'}
|
||||
isGroupChat?: boolean
|
||||
profile?: bsky.profile.AnyProfileView
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const {currentAccount} = useSession()
|
||||
const {t: l} = useLingui()
|
||||
const {convo} = useConvoActive()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const profile = item.relatedProfiles.get(item.message.sender.did)
|
||||
|
||||
const reactionsControl = useDialogControl()
|
||||
const reactionTapRef = useRef(false)
|
||||
|
||||
@@ -277,9 +274,7 @@ let MessageItem = ({
|
||||
return l`You reacted ${reaction.value}`
|
||||
} else {
|
||||
const senderDid = reaction.sender.did
|
||||
const memberSender = convo.members.find(
|
||||
member => member.did === senderDid,
|
||||
)
|
||||
const memberSender = item.relatedProfiles.get(senderDid)
|
||||
if (memberSender) {
|
||||
return l`${createSanitizedDisplayName(memberSender)} reacted ${reaction.value}`
|
||||
}
|
||||
@@ -290,7 +285,13 @@ let MessageItem = ({
|
||||
one: '# person',
|
||||
other: '# people',
|
||||
})} reacted – ${groupedReactions.map(g => g.value).join(' ')}`
|
||||
}, [reactions, groupedReactions, currentAccount?.did, convo.members, l])
|
||||
}, [
|
||||
reactions,
|
||||
groupedReactions,
|
||||
currentAccount?.did,
|
||||
item.relatedProfiles,
|
||||
l,
|
||||
])
|
||||
|
||||
const appliedReactions = (
|
||||
<LayoutAnimationConfig skipEntering skipExiting>
|
||||
@@ -375,7 +376,7 @@ let MessageItem = ({
|
||||
) : null}
|
||||
<ReactionsDialog
|
||||
control={reactionsControl}
|
||||
members={convo.members}
|
||||
relatedProfiles={item.relatedProfiles}
|
||||
message={message}
|
||||
reactions={message.reactions}
|
||||
groupedReactions={groupedReactions}
|
||||
@@ -391,11 +392,13 @@ let MessageItem = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{(hasLargeGapFromPrev || isDateDividerToggled) && (
|
||||
<Animated.View entering={native(FadeIn)} exiting={native(FadeOut)}>
|
||||
<DateDivider date={message.sentAt} />
|
||||
</Animated.View>
|
||||
)}
|
||||
<LayoutAnimationConfig skipExiting skipEntering>
|
||||
{(hasLargeGapFromPrev || isDateDividerToggled) && (
|
||||
<Animated.View entering={native(FadeIn)} exiting={native(FadeOut)}>
|
||||
<DateDivider date={message.sentAt} />
|
||||
</Animated.View>
|
||||
)}
|
||||
</LayoutAnimationConfig>
|
||||
<View style={[messageInset, effectiveFirstInCluster && a.mt_md]}>
|
||||
<View style={[a.relative]}>
|
||||
{showAvatar ? (
|
||||
@@ -434,6 +437,7 @@ let MessageItem = ({
|
||||
hasReactions={hasReactions}
|
||||
isFromSelf={isFromSelf}
|
||||
message={message}
|
||||
senderProfile={profile}
|
||||
onTap={() => {
|
||||
if (reactionTapRef.current) return
|
||||
if (!hasLargeGapFromPrev) {
|
||||
|
||||
@@ -161,11 +161,13 @@ function GroupHeaderReady({
|
||||
})
|
||||
}
|
||||
|
||||
const lockStatus = convo.details.lockStatus
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
heading={
|
||||
<>
|
||||
<AvatarBubbles size="small" profiles={convo.members} />
|
||||
<AvatarBubbles size={40} profiles={convo.members} />
|
||||
<Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}>
|
||||
{convo.details.name}
|
||||
</Text>
|
||||
@@ -175,6 +177,7 @@ function GroupHeaderReady({
|
||||
settings={
|
||||
<Button
|
||||
label={l`Open group chat settings`}
|
||||
disabled={lockStatus === 'locked-permanently'}
|
||||
size="small"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
View,
|
||||
} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {type ChatBskyActorDefs, type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
@@ -33,13 +33,13 @@ type Reaction = {
|
||||
|
||||
export function ReactionsDialog({
|
||||
control,
|
||||
members,
|
||||
relatedProfiles,
|
||||
message,
|
||||
reactions,
|
||||
groupedReactions,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
members: bsky.profile.AnyProfileView[]
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
reactions?: ChatBskyConvoDefs.ReactionView[]
|
||||
groupedReactions?: Reaction[]
|
||||
@@ -100,7 +100,7 @@ export function ReactionsDialog({
|
||||
return 0
|
||||
})
|
||||
.map(reaction => {
|
||||
const sender = members.find(m => m.did === reaction.sender.did)
|
||||
const sender = relatedProfiles.get(reaction.sender.did)
|
||||
if (!sender) return null
|
||||
return (
|
||||
<ReactionRow
|
||||
|
||||
@@ -23,15 +23,15 @@ export type SystemMessageInfo = {
|
||||
|
||||
function getReferredDisplayName(
|
||||
user: ChatBskyConvoDefs.SystemMessageReferredUser,
|
||||
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>,
|
||||
): string | null {
|
||||
const profile = relatedProfiles.find(p => p.did === user.did)
|
||||
const profile = relatedProfiles.get(user.did)
|
||||
return profile ? createSanitizedDisplayName(profile) : null
|
||||
}
|
||||
|
||||
export function getSystemMessageInfo(
|
||||
data: ChatBskyConvoDefs.SystemMessageView['data'],
|
||||
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>,
|
||||
): SystemMessageInfo | null {
|
||||
if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) {
|
||||
const name = getReferredDisplayName(data.member, relatedProfiles)
|
||||
|
||||
@@ -68,13 +68,13 @@ export type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
|
||||
export type ConvoWithDetails = {view: ChatBskyConvoDefs.ConvoView} & (
|
||||
| {
|
||||
kind: 'group'
|
||||
details: ChatBskyConvoDefs.GroupConvo
|
||||
details: $Typed<ChatBskyConvoDefs.GroupConvo>
|
||||
primaryMember: GroupConvoMember // the owner
|
||||
members: Array<GroupConvoMember>
|
||||
}
|
||||
| {
|
||||
kind: 'direct'
|
||||
details: ChatBskyConvoDefs.DirectConvo
|
||||
details: $Typed<ChatBskyConvoDefs.DirectConvo>
|
||||
primaryMember: DirectConvoMember // the other user
|
||||
members: Array<DirectConvoMember>
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ export type ItemProps = ViewStyleProp & {
|
||||
isInvalid?: boolean
|
||||
children: ((props: ItemState) => React.ReactNode) | React.ReactNode
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
highlightRow?: boolean
|
||||
}
|
||||
|
||||
export function useItemContext() {
|
||||
@@ -160,8 +161,11 @@ export function Item({
|
||||
style,
|
||||
type = 'checkbox',
|
||||
label,
|
||||
highlightRow,
|
||||
...rest
|
||||
}: ItemProps) {
|
||||
const t = useTheme()
|
||||
|
||||
const {
|
||||
values: selectedValues,
|
||||
type: groupType,
|
||||
@@ -207,6 +211,12 @@ export function Item({
|
||||
[name, selected, disabled, hovered, pressed, focused, isInvalid],
|
||||
)
|
||||
|
||||
const highlightStyle = highlightRow
|
||||
? selected
|
||||
? [a.rounded_full, a.p_md, {backgroundColor: t.palette.primary_50}]
|
||||
: [a.rounded_full, a.p_md]
|
||||
: null
|
||||
|
||||
return (
|
||||
<ItemContext.Provider value={state}>
|
||||
<Pressable
|
||||
@@ -232,7 +242,7 @@ export function Item({
|
||||
onPressOut={onPressOut}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={[a.flex_row, a.align_center, a.gap_sm, style]}>
|
||||
style={[a.flex_row, a.align_center, a.gap_sm, highlightStyle, style]}>
|
||||
{typeof children === 'function' ? children(state) : children}
|
||||
</Pressable>
|
||||
</ItemContext.Provider>
|
||||
|
||||
@@ -5,6 +5,13 @@ export const EditBig_Stroke1_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M19.667 4.458a1 1 0 1 0 0-2v2Zm25 23a1 1 0 0 0-2 0h2ZM3.912 45.543l.454-.891-.454.89Zm-2.33-2.33.89-.455h0l-.89.454Zm39.173 2.33-.454-.891h0l.454.89Zm2.33-2.33-.89-.455.89.454ZM1.581 6.37l-.89-.454h0l.89.454Zm2.331-2.331-.454-.891.454.89ZM14.333 32.79h-1a1 1 0 0 0 1 1v-1Zm.781-8.781.707.707-.707-.707ZM36.562 2.562l-.707-.707v0l.707.707Zm7.543 0-.707.707v0l.707-.707Zm.457.458.707-.707v0l-.707.707Zm0 7.542.707.707-.707-.707ZM23.114 32.01l.707.707-.707-.707Zm12.02 14.114v-1h-25.6v2h25.6v-1ZM1 37.591h1v-25.6H0v25.6h1ZM9.533 3.458v1h10.134v-2H9.533v1Zm34.134 24h-1V37.59h2V27.457h-1ZM9.533 46.124v-1c-1.51 0-2.582 0-3.421-.07-.828-.067-1.34-.195-1.746-.402l-.454.89-.454.892c.735.374 1.54.537 2.491.614.94.077 2.107.076 3.584.076v-1ZM1 37.591H0c0 1.477 0 2.645.076 3.584.078.951.24 1.756.614 2.491l.891-.454.891-.454c-.207-.406-.335-.918-.403-1.746C2.001 40.173 2 39.101 2 37.591H1Zm2.912 7.952.454-.891a4.33 4.33 0 0 1-1.894-1.894l-.89.454-.892.454a6.33 6.33 0 0 0 2.768 2.768l.454-.891Zm31.221.581v1c1.477 0 2.645.001 3.585-.076.95-.078 1.756-.24 2.49-.614l-.453-.891-.454-.891c-.406.207-.919.335-1.746.403-.84.068-1.912.07-3.422.07v1Zm8.534-8.533h-1c0 1.51-.001 2.582-.07 3.421-.067.828-.196 1.34-.403 1.746l.891.454.891.454c.375-.735.537-1.54.615-2.49.076-.94.076-2.108.076-3.585h-1Zm-2.912 7.952.454.89a6.33 6.33 0 0 0 2.767-2.767l-.89-.454-.892-.454a4.33 4.33 0 0 1-1.893 1.894l.454.89ZM1 11.99h1c0-1.51 0-2.582.07-3.422.067-.827.195-1.34.402-1.745l-.89-.454-.892-.454c-.374.734-.536 1.54-.614 2.49C-.001 9.345 0 10.513 0 11.99h1Zm8.533-8.533v-1c-1.477 0-2.645-.001-3.584.076-.951.077-1.756.24-2.49.614l.453.89.454.892c.406-.207.918-.336 1.746-.403.839-.069 1.911-.07 3.421-.07v-1ZM1.581 6.37l.891.454A4.33 4.33 0 0 1 4.366 4.93l-.454-.891-.454-.891A6.33 6.33 0 0 0 .69 5.916l.891.454Zm12.752 19.525h-1v6.896h2v-6.896h-1Zm0 6.896v1h6.896v-2h-6.896v1Zm.781-8.781.707.707L37.27 3.269l-.707-.707-.707-.707-21.448 21.448.707.707Zm28.99-21.448-.706.707.457.458.707-.707.707-.707-.457-.458-.707.707Zm.458 8-.707-.707-21.448 21.448.707.707.707.707L45.27 11.269l-.707-.707Zm0-7.542-.707.707a4.333 4.333 0 0 1 0 6.128l.707.707.707.707a6.333 6.333 0 0 0 0-8.956l-.707.707Zm-8-.458.707.707a4.333 4.333 0 0 1 6.129 0l.707-.707.707-.707a6.333 6.333 0 0 0-8.957 0l.707.707ZM21.23 32.791v1c.972 0 1.905-.386 2.593-1.074l-.708-.707-.707-.707a1.67 1.67 0 0 1-1.178.488v1Zm-6.896-6.896h1c0-.442.176-.866.489-1.178l-.708-.707-.707-.707a3.67 3.67 0 0 0-1.074 2.592h1Z',
|
||||
})
|
||||
|
||||
/**
|
||||
* @deprecated Use EditBig_Stroke2_Corner2_Rounded
|
||||
*/
|
||||
export const EditBig_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M17.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-9 9A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l9-9ZM10 12.414V14h1.586l8-8L18 4.414l-8 8ZM3 4a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Z',
|
||||
})
|
||||
|
||||
export const EditBig_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||
path: 'M3 16.8V7.2c0-.544-.001-1.011.03-1.395.033-.395.104-.789.297-1.167a3 3 0 0 1 1.31-1.31c.379-.193.772-.265 1.168-.297C6.188 2.999 6.657 3 7.2 3H11a1 1 0 1 1 0 2H7.2c-.576 0-.949 0-1.232.023-.272.022-.373.06-.422.085a1 1 0 0 0-.437.437c-.025.05-.062.15-.085.422C5.001 6.251 5 6.623 5 7.2v9.6c0 .577.001.95.024 1.232.023.272.06.373.085.422a1 1 0 0 0 .437.437c.05.025.15.063.422.085.283.023.656.024 1.232.024h9.6c.576 0 .949-.001 1.232-.024.272-.022.373-.06.422-.085a1 1 0 0 0 .437-.437c.025-.049.062-.15.085-.422.023-.283.024-.655.024-1.232V13a1 1 0 1 1 2 0v3.8c0 .543.001 1.011-.03 1.395-.033.395-.104.788-.297 1.167a3 3 0 0 1-1.31 1.311c-.379.193-.772.264-1.168.296-.383.031-.852.031-1.395.031H7.2c-.543 0-1.012 0-1.395-.031-.396-.032-.789-.103-1.167-.296a3 3 0 0 1-1.31-1.311c-.194-.379-.265-.772-.298-1.167C3 17.81 3 17.343 3 16.8M16.629 2.957a3 3 0 0 1 4.242 0l.172.171a3 3 0 0 1 0 4.243L13 15.414a2 2 0 0 1-1.414.586H9a1 1 0 0 1-1-1v-2.586A2 2 0 0 1 8.586 11zM10 14h1.586l8.043-8.043a1 1 0 0 0 0-1.414l-.172-.172a1 1 0 0 0-1.414 0L10 12.414z',
|
||||
})
|
||||
|
||||
@@ -9,5 +9,5 @@ export const Lock_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||
})
|
||||
|
||||
export const Unlock_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||
path: 'M12 13a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z"/><path fill="#000" fill-rule="evenodd" d="M12 2a5 5 0 0 1 4.843 3.751 1 1 0 0 1-1.938.498A3.002 3.002 0 0 0 9 7v2h8a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5Zm-5 9a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H7Z',
|
||||
path: 'M12 2a5 5 0 0 1 4.843 3.751 1 1 0 0 1-1.938.498A3.002 3.002 0 0 0 9 7v2h8a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5m-5 9a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1zm5 2a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1',
|
||||
})
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
/**
|
||||
* @deprecated Use SquareBehindSquare_Stroke2_Corner2_Rounded
|
||||
*/
|
||||
export const SquareBehindSquare4_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M8 8V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-5v5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h5Zm1 8a1 1 0 0 1-1-1v-5H4v10h10v-4H9Z',
|
||||
})
|
||||
|
||||
export const SquareBehindSquare_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||
path: 'M20 4.25a.25.25 0 0 0-.25-.25h-9.5a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25zM4 19.75c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V16h-3.75A2.25 2.25 0 0 1 8 13.75V10H4.25a.25.25 0 0 0-.25.25zm18-6A2.25 2.25 0 0 1 19.75 16H16v3.75A2.25 2.25 0 0 1 13.75 22h-9.5A2.25 2.25 0 0 1 2 19.75v-9.5A2.25 2.25 0 0 1 4.25 8H8V4.25A2.25 2.25 0 0 1 10.25 2h9.5A2.25 2.25 0 0 1 22 4.25z',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user