Add message and star button to profile cards in people page

This commit is contained in:
MartinBraquet
2026-05-12 14:33:04 +02:00
parent e960bff2b0
commit 443f08b558
10 changed files with 138 additions and 46 deletions

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.compassconnections.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 107
versionName "1.24.0"
versionCode 108
versionName "1.25.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View File

@@ -1,6 +1,6 @@
{
"name": "@compass/api",
"version": "1.38.2",
"version": "1.39.0",
"private": true,
"description": "Backend API endpoints",
"main": "src/serve.ts",

View File

@@ -4,9 +4,9 @@ import {Profile} from 'common/profiles/profile'
import {User} from 'common/user'
import {findKey} from 'lodash'
import {useRouter} from 'next/router'
import {useEffect, useState} from 'react'
import React, {useEffect, useState} from 'react'
import {BiEnvelope} from 'react-icons/bi'
import {Button} from 'web/components/buttons/button'
import {Button, buttonClass} from 'web/components/buttons/button'
import {CommentInputTextArea} from 'web/components/comments/comment-input'
import {Col} from 'web/components/layout/col'
import {Modal, MODAL_CLASS} from 'web/components/layout/modal'
@@ -15,26 +15,41 @@ import {useTextEditor} from 'web/components/widgets/editor'
import {Tooltip} from 'web/components/widgets/tooltip'
import {useFirebaseUser} from 'web/hooks/use-firebase-user'
import {useSortedPrivateMessageMemberships} from 'web/hooks/use-private-messages'
import {usePrivateUser} from 'web/hooks/use-user'
import {usePrivateUser, useUser} from 'web/hooks/use-user'
import {api} from 'web/lib/api'
import {firebaseLogin} from 'web/lib/firebase/users'
import {useT} from 'web/lib/locale'
export const SendMessageButton = (props: {
toUser: User
currentUser: User | undefined | null
profile: Profile
includeLabel?: boolean
circleButton?: boolean
text?: string
tooltipText?: string
className?: string
disabled?: boolean
accentIfMessaged?: boolean
onPointerDown?: () => void
size?: string
}) => {
const {toUser, currentUser, profile, includeLabel, circleButton, text, tooltipText, disabled} =
props
const {
toUser,
profile,
includeLabel,
circleButton,
text,
tooltipText,
disabled,
onPointerDown,
className,
size = 'h-6 w-6',
accentIfMessaged,
} = props
const firebaseUser = useFirebaseUser()
const router = useRouter()
const privateUser = usePrivateUser()
const currentUser = useUser()
const channelMemberships = useSortedPrivateMessageMemberships(currentUser?.id)
const {memberIdsByChannelId} = channelMemberships
const t = useT()
@@ -43,17 +58,19 @@ export const SendMessageButton = (props: {
const [error, setError] = useState('')
const [submitting, setSubmitting] = useState(false)
const previousDirectMessageChannel = findKey(
memberIdsByChannelId,
(dm) => dm.includes(toUser.id) && dm.length === 1,
)
const previousChannelId =
previousDirectMessageChannel !== undefined ? previousDirectMessageChannel : undefined
const putAccent = previousChannelId !== undefined && accentIfMessaged
const messageButtonClicked = async () => {
if (disabled) return
if (!currentUser) return firebaseLogin()
const previousDirectMessageChannel = findKey(
memberIdsByChannelId,
(dm) => dm.includes(toUser.id) && dm.length === 1,
)
const previousChannelId =
previousDirectMessageChannel !== undefined ? previousDirectMessageChannel : undefined
if (previousChannelId) router.push(`/messages/${previousChannelId}`)
else setOpenComposeModal(true)
}
@@ -138,12 +155,23 @@ export const SendMessageButton = (props: {
return (
<>
<Tooltip text={tooltipText || t('send_message.button_label', 'Message')} noTap>
<Tooltip
text={
tooltipText ||
(putAccent ? t('send_message.', 'Follow Up') : t('send_message.button_label', 'Message'))
}
noTap
>
{text ? (
<Button
className={clsx('h-fit gap-1', disabled && 'opacity-50 cursor-not-allowed')}
color={'primary'}
onClick={messageButtonClicked}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
onPointerDown?.()
messageButtonClicked()
}}
onPointerDown={onPointerDown}
disabled={disabled}
>
{text}
@@ -154,7 +182,12 @@ export const SendMessageButton = (props: {
'h-7 w-7 rounded-full transition-colors',
disabled ? 'bg-gray-400 cursor-not-allowed' : 'bg-primary-900 hover:bg-primary-600',
)}
onClick={messageButtonClicked}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
onPointerDown?.()
messageButtonClicked()
}}
onPointerDown={onPointerDown}
disabled={disabled}
>
<BiEnvelope
@@ -163,14 +196,27 @@ export const SendMessageButton = (props: {
</button>
) : (
<button
onClick={messageButtonClicked}
onPointerDown={onPointerDown}
disabled={disabled}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
onPointerDown?.()
messageButtonClicked()
}}
className={clsx(
'border-canvas-300 flex items-center gap-1.5 rounded-lg border px-2 py-2 text-sm text-primary-700 transition-colors hover:border-primary-400 hover:bg-primary-50',
'relative border border-canvas-200',
buttonClass('xs', 'none'),
disabled && 'opacity-50 cursor-not-allowed',
className,
putAccent
? 'bg-green-200 border-green-500 !text-green-500'
: 'bg-canvas-50 border-canvas-300 text-ink-500 hover:border-primary-400 hover:bg-primary-50',
)}
>
<BiEnvelope className={clsx('h-5 w-5', includeLabel && 'mr-2')} />{' '}
{putAccent && (
<span className="absolute top-[3px] right-[3px] w-[7px] h-[7px] rounded-full bg-green-500 border-[1.5px] border-canvas-50" />
)}
<BiEnvelope className={clsx(size, includeLabel && 'mr-2')} />{' '}
{includeLabel && <>{t('send_message.button_label', 'Message')}</>}
</button>
)}

View File

@@ -23,12 +23,14 @@ import {PiMagnifyingGlassBold} from 'react-icons/pi'
import GenderIcon from 'web/components/gender-icon'
import {IconWithInfo} from 'web/components/icons'
import {Row} from 'web/components/layout/row'
import {SendMessageButton} from 'web/components/messaging/send-message-button'
import {ProfileLocation} from 'web/components/profile/profile-location'
import {getSeekingText} from 'web/components/profile-about'
import {CompatibleBadge} from 'web/components/widgets/compatible-badge'
import {Content} from 'web/components/widgets/editor'
import HideProfileButton from 'web/components/widgets/hide-profile-button'
import {CompassLoadingIndicator} from 'web/components/widgets/loading-indicator'
import {StarButton} from 'web/components/widgets/star-button'
import {LoadMoreUntilNotVisible} from 'web/components/widgets/visibility-observer'
import {useChoicesContext} from 'web/hooks/use-choices'
import {isDark, useTheme} from 'web/hooks/use-theme'
@@ -144,7 +146,16 @@ function ProfilePreview(props: {
onUndoHidden?: (userId: string) => void
displayOptions?: Partial<DisplayOptions>
}) {
const {profile, compatibilityScore, onHide, isHidden, onUndoHidden, displayOptions} = props
const {
profile,
compatibilityScore,
onHide,
isHidden,
onUndoHidden,
displayOptions,
hasStar,
refreshStars,
} = props
const {
showPhotos,
@@ -168,7 +179,6 @@ function ProfilePreview(props: {
const {user} = profile
const choicesIdsToLabels = useChoicesContext()
const t = useT()
// const currentUser = useUser()
const [isLoading, setIsLoading] = useState(false)
const [showRing, setShowRing] = useState(false)
@@ -346,18 +356,21 @@ function ProfilePreview(props: {
<Col className={clsx('relative w-full rounded-xl transition-all text-sm')}>
<Row
className={clsx(
'absolute top-2 right-2 items-start justify-end px-2 pb-3 z-10',
'absolute top-2 right-2 items-start justify-end px-2 pb-3 z-10 gap-1',
isPhotoRendered && (cardSize === 'large' ? 'mr-0 sm:mr-60' : 'mr-40'),
)}
>
{compatibilityScore && (
<CompatibleBadge compatibility={compatibilityScore} className={'pt-1'} />
<CompatibleBadge
compatibility={compatibilityScore}
className={clsx('pt-1 text-xs', cardSize !== 'large' && 'hidden sm:flex')}
/>
)}
{onHide && (
<HideProfileButton
hiddenUserId={profile.user_id}
onHidden={onHide}
className="ml-2"
className="ml-1"
stopPropagation
eyeOff
onPointerDown={() => {
@@ -365,6 +378,32 @@ function ProfilePreview(props: {
}}
/>
)}
{hasStar !== undefined && (
<StarButton
targetProfile={profile}
isStarred={hasStar}
refresh={refreshStars}
size={'h-4 w-4'}
className="h-7 w-7 !rounded-lg !p-1 hover:border-primary-400 hover:bg-primary-50"
onPointerDown={() => {
hideButtonClickedRef.current = true
}}
/>
)}
{user && (
<div className={clsx(cardSize !== 'large' && 'hidden sm:flex')}>
<SendMessageButton
toUser={user}
profile={profile}
size={'h-4 w-4'}
className={clsx('!p-1 w-7 h-7')}
accentIfMessaged
onPointerDown={() => {
hideButtonClickedRef.current = true
}}
/>
</div>
)}
</Row>
<div className={clsx('flex lg:flex-row h-full lg:justify-between', cardClass)}>

View File

@@ -89,7 +89,6 @@ export function ConnectActions(props: {profile: Profile; user: User}) {
{profile.allow_direct_messaging || matches.length > 0 ? (
<SendMessageButton
toUser={user}
currentUser={currentUser}
profile={profile}
text={t('messaging.send_thoughtful_message', 'Send them a thoughtful message')}
/>

View File

@@ -328,7 +328,6 @@ export function ProfileHeaderActions(props: {
{currentUser && showMessageButton && (
<SendMessageButton
toUser={user}
currentUser={currentUser}
profile={profile}
tooltipText={tooltipText}
disabled={!profile.allow_direct_messaging}

View File

@@ -77,7 +77,7 @@ export function HideProfileButton(props: HideProfileButtonProps) {
>
<button
className={clsx(
'border border-canvas-200 rounded-md p-1 hover:bg-canvas-200 focus:outline-none',
'relative inline-flex items-center justify-center border bg-canvas-50 border-canvas-300 text-ink-300 hover:border-primary-400 hover:bg-primary-50 rounded-lg h-7 w-7',
className,
)}
disabled={submitting}
@@ -91,9 +91,9 @@ export function HideProfileButton(props: HideProfileButtonProps) {
}
>
{hidden || eyeOff ? (
<EyeSlashIcon className={clsx('h-4 w-4 guidance', iconClassName)} />
<EyeSlashIcon className={clsx('h-4 w-4 text-ink-500', iconClassName)} />
) : (
<EyeIcon className={clsx('h-4 w-4 guidance', iconClassName)} />
<EyeIcon className={clsx('h-4 w-4 text-ink-500', iconClassName)} />
)}
</button>
</Tooltip>

View File

@@ -14,8 +14,10 @@ export const StarButton = (props: {
refresh: () => Promise<void>
hideTooltip?: boolean
className?: string
size?: string
onPointerDown?: () => void
}) => {
const {targetProfile, refresh, hideTooltip, className} = props
const {targetProfile, refresh, hideTooltip, className, size = 'w-6 h-6', onPointerDown} = props
const targetId = targetProfile.user_id
const [isStarred, setIsStarred] = useState(props.isStarred)
const t = useT()
@@ -41,18 +43,21 @@ export const StarButton = (props: {
const button = (
<button
className={clsx(buttonClass('xs', 'none'), 'text-ink-500 group !rounded-full', className)}
className={clsx(
'border border-canvas-200',
buttonClass('xs', 'none'),
isStarred
? 'bg-primary-50 border-primary-200 text-primary-600'
: 'bg-canvas-50 border-canvas-300 text-ink-500 hover:border-primary-400 hover:bg-primary-50',
className,
)}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
star()
}}
onPointerDown={onPointerDown}
>
<StarIcon
className={clsx(
'h-7 w-7 transition-colors group-hover:fill-yellow-400/70',
isStarred && 'fill-yellow-400 stroke-yellow-500 dark:stroke-yellow-600',
)}
/>
<StarIcon className={clsx(size, isStarred && 'fill-primary-500')} />
</button>
)

View File

@@ -123,7 +123,7 @@ export function Tooltip(props: {
as="div"
ref={refs.setFloating as any}
style={{position: strategy, top: y ?? 0, left: x ?? 0}}
className="text-ink-1000 bg-canvas-50 z-20 w-max max-w-xs whitespace-normal rounded-lg px-2 py-1 text-center text-sm font-medium border border-canvas-100 shadow shadow-canvas-100"
className="text-ink-1000 bg-primary-100 z-20 w-max max-w-xs whitespace-normal rounded-lg px-2 py-1 text-center text-sm font-medium border border-primary-300 shadow shadow-canvas-100"
suppressHydrationWarning={suppressHydrationWarning}
{...getFloatingProps()}
>

View File

@@ -106,6 +106,7 @@
--color-ink-600: 120 108 92;
/* Green - Accents */
--color-green-200: #f0faf3;
--color-green-500: 107 143 113; /* Accent - Sage Green (#6B8F71) */
/* Canvas - Backgrounds & Surfaces */
@@ -182,7 +183,7 @@
/*--color-green-500: 34 197 94; !* standard green *!*/
--color-green-400: 74 222 128;
--color-green-300: 110 231 183; /* vibrant but not neon */
--color-green-200: 167 243 208; /* gentle mid-light tone */
/*--color-green-200: 167 243 208; !* gentle mid-light tone *!*/
--color-green-100: 209 260 229; /* soft and airy */
--color-green-50: 240 263 245; /* subtle, barely tinted background */
@@ -222,8 +223,11 @@
--color-ink-500: 176 160 140; /* Muted Text - shifted to warm tan */
--color-ink-600: 156 140 120;
/* Green - Sage looks great on dark brown */
--color-green-500: 125 160 131; /* Lightened Sage */
/* Button background — barely-there dark green surface */
--color-green-200: 32 54 38;
/* Icon, dot, text — readable sage on that dark surface */
--color-green-500: 125 160 131;
/* High Contrast */
--color-ink-1000: 255 255 255; /* white */
@@ -298,7 +302,7 @@
/*--color-green-500: 34 197 94; !* standard green *!*/
--color-green-400: 22 163 74;
--color-green-300: 21 128 61;
--color-green-200: 22 101 52;
/*--color-green-200: 22 101 52;*/
--color-green-100: 20 83 45;
--color-green-50: 5 46 22; /* darkest green */