diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx
index 6e0bfef6..e1c0cb97 100644
--- a/web/components/answers/answers-panel.tsx
+++ b/web/components/answers/answers-panel.tsx
@@ -21,9 +21,9 @@ import { Modal } from 'web/components/layout/modal'
import { AnswerBetPanel } from 'web/components/answers/answer-bet-panel'
import { Row } from 'web/components/layout/row'
import { Avatar } from 'web/components/avatar'
-import { UserLink } from 'web/components/user-page'
import { Linkify } from 'web/components/linkify'
import { BuyButton } from 'web/components/yes-no-selector'
+import { UserLink } from 'web/components/user-link'
export function AnswersPanel(props: {
contract: FreeResponseContract | MultipleChoiceContract
diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx
index c3058a45..8d37095e 100644
--- a/web/components/bets-list.tsx
+++ b/web/components/bets-list.tsx
@@ -31,7 +31,6 @@ import {
getContractFromId,
} from 'web/lib/firebase/contracts'
import { Row } from './layout/row'
-import { UserLink } from './user-page'
import { sellBet } from 'web/lib/firebase/api'
import { ConfirmationButton } from './confirmation-button'
import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label'
@@ -59,6 +58,7 @@ import { floatingEqual } from 'common/util/math'
import { filterDefined } from 'common/util/array'
import { Pagination } from './pagination'
import { LimitOrderTable } from './limit-bets'
+import { UserLink } from 'web/components/user-link'
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all'
diff --git a/web/components/charity/feed-items.tsx b/web/components/charity/feed-items.tsx
index 365aa606..b589f34b 100644
--- a/web/components/charity/feed-items.tsx
+++ b/web/components/charity/feed-items.tsx
@@ -1,9 +1,9 @@
import { DonationTxn } from 'common/txn'
import { Avatar } from '../avatar'
import { useUserById } from 'web/hooks/use-user'
-import { UserLink } from '../user-page'
import { manaToUSD } from '../../../common/util/format'
import { RelativeTimestamp } from '../relative-timestamp'
+import { UserLink } from 'web/components/user-link'
export function Donation(props: { txn: DonationTxn }) {
const { txn } = props
diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx
index 280787dd..bffe2ba3 100644
--- a/web/components/comments-list.tsx
+++ b/web/components/comments-list.tsx
@@ -7,12 +7,12 @@ import { SiteLink } from './site-link'
import { Row } from './layout/row'
import { Avatar } from './avatar'
import { RelativeTimestamp } from './relative-timestamp'
-import { UserLink } from './user-page'
import { User } from 'common/user'
import { Col } from './layout/col'
import { Content } from './editor'
import { Pagination } from './pagination'
import { LoadingIndicator } from './loading-indicator'
+import { UserLink } from 'web/components/user-link'
const COMMENTS_PER_PAGE = 50
diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx
index 56407c4d..28f00d94 100644
--- a/web/components/contract/contract-details.tsx
+++ b/web/components/contract/contract-details.tsx
@@ -8,7 +8,6 @@ import {
import { Row } from '../layout/row'
import { formatMoney } from 'common/util/format'
-import { UserLink } from '../user-page'
import { Contract, updateContract } from 'web/lib/firebase/contracts'
import dayjs from 'dayjs'
import { DateTimeTooltip } from '../datetime-tooltip'
@@ -34,6 +33,7 @@ import clsx from 'clsx'
import { contractMetrics } from 'common/contract-details'
import { User } from 'common/user'
import { FeaturedContractBadge } from 'web/components/contract/FeaturedContractBadge'
+import { UserLink } from 'web/components/user-link'
export type ShowTime = 'resolve-date' | 'close-date'
diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx
index 86686f1f..ec08b7c6 100644
--- a/web/components/feed/feed-answer-comment-group.tsx
+++ b/web/components/feed/feed-answer-comment-group.tsx
@@ -5,7 +5,6 @@ import React, { useEffect, useState } from 'react'
import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row'
import { Avatar } from 'web/components/avatar'
-import { UserLink } from 'web/components/user-page'
import { Linkify } from 'web/components/linkify'
import clsx from 'clsx'
import {
@@ -19,6 +18,7 @@ import { groupBy } from 'lodash'
import { User } from 'common/user'
import { useEvent } from 'web/hooks/use-event'
import { CommentTipMap } from 'web/hooks/use-tip-txns'
+import { UserLink } from 'web/components/user-link'
export function FeedAnswerCommentGroup(props: {
contract: any
diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx
index ffa53de3..d6950b37 100644
--- a/web/components/feed/feed-bets.tsx
+++ b/web/components/feed/feed-bets.tsx
@@ -13,11 +13,11 @@ import { RelativeTimestamp } from 'web/components/relative-timestamp'
import React, { Fragment, useEffect } from 'react'
import { uniqBy, partition, sumBy, groupBy } from 'lodash'
import { JoinSpans } from 'web/components/join-spans'
-import { UserLink } from '../user-page'
import { formatNumericProbability } from 'common/pseudo-numeric'
import { SiteLink } from 'web/components/site-link'
import { getChallenge, getChallengeUrl } from 'web/lib/firebase/challenges'
import { Challenge } from 'common/challenge'
+import { UserLink } from 'web/components/user-link'
export function FeedBet(props: {
contract: Contract
diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx
index 0541a7ba..78e971c1 100644
--- a/web/components/feed/feed-comments.tsx
+++ b/web/components/feed/feed-comments.tsx
@@ -10,7 +10,6 @@ import { useRouter } from 'next/router'
import { Row } from 'web/components/layout/row'
import clsx from 'clsx'
import { Avatar } from 'web/components/avatar'
-import { UserLink } from 'web/components/user-page'
import { OutcomeLabel } from 'web/components/outcome-label'
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
import { firebaseLogin } from 'web/lib/firebase/users'
@@ -29,6 +28,7 @@ import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
import { useWindowSize } from 'web/hooks/use-window-size'
import { Content, TextEditor, useTextEditor } from '../editor'
import { Editor } from '@tiptap/react'
+import { UserLink } from 'web/components/user-link'
export function FeedCommentThread(props: {
contract: Contract
diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx
index 62673428..3491771d 100644
--- a/web/components/feed/feed-items.tsx
+++ b/web/components/feed/feed-items.tsx
@@ -17,7 +17,6 @@ import {
import { BinaryResolutionOrChance } from '../contract/contract-card'
import { SiteLink } from '../site-link'
import { Col } from '../layout/col'
-import { UserLink } from '../user-page'
import BetButton from '../bet-button'
import { Avatar } from '../avatar'
import { ActivityItem } from './activity-items'
@@ -38,6 +37,7 @@ import { SignUpPrompt } from '../sign-up-prompt'
import { User } from 'common/user'
import { PlayMoneyDisclaimer } from '../play-money-disclaimer'
import { contractMetrics } from 'common/contract-details'
+import { UserLink } from 'web/components/user-link'
export function FeedItems(props: {
contract: Contract
diff --git a/web/components/feed/feed-liquidity.tsx b/web/components/feed/feed-liquidity.tsx
index 0ed06046..ff94dbae 100644
--- a/web/components/feed/feed-liquidity.tsx
+++ b/web/components/feed/feed-liquidity.tsx
@@ -7,8 +7,8 @@ import clsx from 'clsx'
import { formatMoney } from 'common/util/format'
import { RelativeTimestamp } from 'web/components/relative-timestamp'
import React from 'react'
-import { UserLink } from '../user-page'
import { LiquidityProvision } from 'common/liquidity-provision'
+import { UserLink } from 'web/components/user-link'
export function FeedLiquidity(props: {
liquidity: LiquidityProvision
diff --git a/web/components/filter-select-users.tsx b/web/components/filter-select-users.tsx
index a19ab6af..415a6d57 100644
--- a/web/components/filter-select-users.tsx
+++ b/web/components/filter-select-users.tsx
@@ -6,8 +6,8 @@ import clsx from 'clsx'
import { Menu, Transition } from '@headlessui/react'
import { Avatar } from 'web/components/avatar'
import { Row } from 'web/components/layout/row'
-import { UserLink } from 'web/components/user-page'
import { searchInAny } from 'common/util/parse'
+import { UserLink } from 'web/components/user-link'
export function FilterSelectUsers(props: {
setSelectedUsers: (users: User[]) => void
diff --git a/web/components/follow-list.tsx b/web/components/follow-list.tsx
index c935f73d..65b9ef4a 100644
--- a/web/components/follow-list.tsx
+++ b/web/components/follow-list.tsx
@@ -6,7 +6,7 @@ import { Avatar } from './avatar'
import { FollowButton } from './follow-button'
import { Col } from './layout/col'
import { Row } from './layout/row'
-import { UserLink } from './user-page'
+import { UserLink } from 'web/components/user-link'
export function FollowList(props: { userIds: string[] }) {
const { userIds } = props
diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx
index 781705c2..65261ca3 100644
--- a/web/components/groups/group-chat.tsx
+++ b/web/components/groups/group-chat.tsx
@@ -11,7 +11,6 @@ import { track } from 'web/lib/service/analytics'
import { firebaseLogin } from 'web/lib/firebase/users'
import { useRouter } from 'next/router'
import clsx from 'clsx'
-import { UserLink } from 'web/components/user-page'
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
import { Tipper } from 'web/components/tipper'
@@ -23,6 +22,7 @@ import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications'
import { ChevronDownIcon, UsersIcon } from '@heroicons/react/outline'
import { setNotificationsAsSeen } from 'web/pages/notifications'
import { usePrivateUser } from 'web/hooks/use-user'
+import { UserLink } from 'web/components/user-link'
export function GroupChat(props: {
messages: GroupComment[]
diff --git a/web/components/online-user-list.tsx b/web/components/online-user-list.tsx
index d7f52d56..e5e006ac 100644
--- a/web/components/online-user-list.tsx
+++ b/web/components/online-user-list.tsx
@@ -2,13 +2,13 @@ import clsx from 'clsx'
import { Avatar } from './avatar'
import { Col } from './layout/col'
import { Row } from './layout/row'
-import { UserLink } from './user-page'
import { User } from 'common/user'
import { UserCircleIcon } from '@heroicons/react/solid'
import { useUsers } from 'web/hooks/use-users'
import { partition } from 'lodash'
import { useWindowSize } from 'web/hooks/use-window-size'
import { useState } from 'react'
+import { UserLink } from 'web/components/user-link'
const isOnline = (user?: User) =>
user && user.lastPingTime && user.lastPingTime > Date.now() - 5 * 60 * 1000
diff --git a/web/components/referrals-button.tsx b/web/components/referrals-button.tsx
index fed8fb6b..3cf77cfd 100644
--- a/web/components/referrals-button.tsx
+++ b/web/components/referrals-button.tsx
@@ -7,11 +7,11 @@ import { Modal } from './layout/modal'
import { Tabs } from './layout/tabs'
import { Row } from 'web/components/layout/row'
import { Avatar } from 'web/components/avatar'
-import { UserLink } from 'web/components/user-page'
import { useReferrals } from 'web/hooks/use-referrals'
import { FilterSelectUsers } from 'web/components/filter-select-users'
import { getUser, updateUser } from 'web/lib/firebase/users'
import { TextButton } from 'web/components/text-button'
+import { UserLink } from 'web/components/user-link'
export function ReferralsButton(props: { user: User; currentUser?: User }) {
const { user, currentUser } = props
diff --git a/web/components/user-link.tsx b/web/components/user-link.tsx
new file mode 100644
index 00000000..5eeab1c4
--- /dev/null
+++ b/web/components/user-link.tsx
@@ -0,0 +1,102 @@
+import { linkClass, SiteLink } from 'web/components/site-link'
+import clsx from 'clsx'
+import { Row } from 'web/components/layout/row'
+import { Modal } from 'web/components/layout/modal'
+import { Col } from 'web/components/layout/col'
+import { useState } from 'react'
+import { Avatar } from 'web/components/avatar'
+import { formatMoney } from 'common/util/format'
+
+function shortenName(name: string) {
+ const firstName = name.split(' ')[0]
+ const maxLength = 10
+ const shortName =
+ firstName.length >= 3
+ ? firstName.length < maxLength
+ ? firstName
+ : firstName.substring(0, maxLength - 3) + '...'
+ : name.length > maxLength
+ ? name.substring(0, maxLength) + '...'
+ : name
+ return shortName
+}
+
+export function UserLink(props: {
+ name: string
+ username: string
+ showUsername?: boolean
+ className?: string
+ short?: boolean
+}) {
+ const { name, username, showUsername, className, short } = props
+ const shortName = short ? shortenName(name) : name
+ return (
+
+ {shortName}
+ {showUsername && ` (@${username})`}
+
+ )
+}
+
+export type MultiUserLinkInfo = {
+ name: string
+ username: string
+ avatarUrl: string | undefined
+ amountTipped: number
+}
+
+export function MultiUserTipLink(props: {
+ userInfos: MultiUserLinkInfo[]
+ className?: string
+}) {
+ const { userInfos, className } = props
+ const [open, setOpen] = useState(false)
+ const maxShowCount = 2
+ return (
+ <>
+ {
+ e.stopPropagation()
+ setOpen(true)
+ }}
+ >
+ {userInfos.map((userInfo, index) =>
+ index < maxShowCount ? (
+
+ {shortenName(userInfo.name) +
+ (index < maxShowCount - 1 ? ', ' : '')}
+
+ ) : (
+
+ & {userInfos.length - maxShowCount} more
+
+ )
+ )}
+
+
+
+ Who tipped you
+ {userInfos.map((userInfo) => (
+
+
+ +{formatMoney(userInfo.amountTipped)}
+
+
+
+
+ ))}
+
+
+ >
+ )
+}
diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx
index 56a041f1..544a4131 100644
--- a/web/components/user-page.tsx
+++ b/web/components/user-page.tsx
@@ -32,35 +32,6 @@ import { BettingStreakModal } from 'web/components/profile/betting-streak-modal'
import { REFERRAL_AMOUNT } from 'common/economy'
import { LoansModal } from './profile/loans-modal'
-export function UserLink(props: {
- name: string
- username: string
- showUsername?: boolean
- className?: string
- short?: boolean
-}) {
- const { name, username, showUsername, className, short } = props
- const firstName = name.split(' ')[0]
- const maxLength = 10
- const shortName =
- firstName.length >= 3
- ? firstName.length < maxLength
- ? firstName
- : firstName.substring(0, maxLength - 3) + '...'
- : name.length > maxLength
- ? name.substring(0, maxLength) + '...'
- : name
- return (
-
- {short ? shortName : name}
- {showUsername && ` (@${username})`}
-
- )
-}
-
export function UserPage(props: { user: User }) {
const { user } = props
const router = useRouter()
diff --git a/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx b/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx
index f15c5809..6b8152d6 100644
--- a/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx
+++ b/web/pages/challenges/[username]/[contractSlug]/[challengeSlug].tsx
@@ -21,7 +21,6 @@ import { Page } from 'web/components/page'
import { useUser, useUserById } from 'web/hooks/use-user'
import { AcceptChallengeButton } from 'web/components/challenges/accept-challenge-button'
import { Avatar } from 'web/components/avatar'
-import { UserLink } from 'web/components/user-page'
import { BinaryOutcomeLabel } from 'web/components/outcome-label'
import { formatMoney } from 'common/util/format'
import { LoadingIndicator } from 'web/components/loading-indicator'
@@ -33,6 +32,7 @@ import { useSaveReferral } from 'web/hooks/use-save-referral'
import { BinaryContract } from 'common/contract'
import { Title } from 'web/components/title'
import { getOpenGraphProps } from 'common/contract-details'
+import { UserLink } from 'web/components/user-link'
export const getStaticProps = fromPropz(getStaticPropz)
diff --git a/web/pages/challenges/index.tsx b/web/pages/challenges/index.tsx
index ad4136f0..11d0f9ab 100644
--- a/web/pages/challenges/index.tsx
+++ b/web/pages/challenges/index.tsx
@@ -19,7 +19,6 @@ import {
import { Challenge, CHALLENGES_ENABLED } from 'common/challenge'
import { Tabs } from 'web/components/layout/tabs'
import { SiteLink } from 'web/components/site-link'
-import { UserLink } from 'web/components/user-page'
import { Avatar } from 'web/components/avatar'
import Router from 'next/router'
import { contractPathWithoutContract } from 'web/lib/firebase/contracts'
@@ -30,6 +29,7 @@ import toast from 'react-hot-toast'
import { Modal } from 'web/components/layout/modal'
import { QRCode } from 'web/components/qr-code'
import { CreateChallengeModal } from 'web/components/challenges/create-challenge-modal'
+import { UserLink } from 'web/components/user-link'
dayjs.extend(customParseFormat)
const columnClass = 'sm:px-5 px-2 py-3.5 max-w-[100px] truncate'
diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx
index 6ce3e7c3..d14288cb 100644
--- a/web/pages/group/[...slugs]/index.tsx
+++ b/web/pages/group/[...slugs]/index.tsx
@@ -12,7 +12,6 @@ import {
updateGroup,
} from 'web/lib/firebase/groups'
import { Row } from 'web/components/layout/row'
-import { UserLink } from 'web/components/user-page'
import { firebaseLogin, getUser, User } from 'web/lib/firebase/users'
import { Col } from 'web/components/layout/col'
import { useUser } from 'web/hooks/use-user'
@@ -45,6 +44,7 @@ import { Button } from 'web/components/button'
import { listAllCommentsOnGroup } from 'web/lib/firebase/comments'
import { GroupComment } from 'common/comment'
import { REFERRAL_AMOUNT } from 'common/economy'
+import { UserLink } from 'web/components/user-link'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx
index 521742b2..aaf1374c 100644
--- a/web/pages/groups.tsx
+++ b/web/pages/groups.tsx
@@ -16,9 +16,9 @@ import { SiteLink } from 'web/components/site-link'
import clsx from 'clsx'
import { Avatar } from 'web/components/avatar'
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
-import { UserLink } from 'web/components/user-page'
import { searchInAny } from 'common/util/parse'
import { SEO } from 'web/components/SEO'
+import { UserLink } from 'web/components/user-link'
export async function getStaticProps() {
const groups = await listAllGroups().catch((_) => [])
diff --git a/web/pages/links.tsx b/web/pages/links.tsx
index 6f57dc14..4c4a0be1 100644
--- a/web/pages/links.tsx
+++ b/web/pages/links.tsx
@@ -18,7 +18,6 @@ import { ManalinkTxn } from 'common/txn'
import { User } from 'common/user'
import { Avatar } from 'web/components/avatar'
import { RelativeTimestamp } from 'web/components/relative-timestamp'
-import { UserLink } from 'web/components/user-page'
import { CreateLinksButton } from 'web/components/manalinks/create-links-button'
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
@@ -27,6 +26,7 @@ import { Pagination } from 'web/components/pagination'
import { Manalink } from 'common/manalink'
import { SiteLink } from 'web/components/site-link'
import { REFERRAL_AMOUNT } from 'common/economy'
+import { UserLink } from 'web/components/user-link'
const LINKS_PER_PAGE = 24
diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx
index 6948716b..93ac9e5d 100644
--- a/web/pages/notifications.tsx
+++ b/web/pages/notifications.tsx
@@ -7,7 +7,6 @@ import { Page } from 'web/components/page'
import { Title } from 'web/components/title'
import { doc, updateDoc } from 'firebase/firestore'
import { db } from 'web/lib/firebase/init'
-import { UserLink } from 'web/components/user-page'
import {
MANIFOLD_AVATAR_URL,
MANIFOLD_USERNAME,
@@ -35,7 +34,7 @@ import {
BETTING_STREAK_BONUS_AMOUNT,
UNIQUE_BETTOR_BONUS_AMOUNT,
} from 'common/economy'
-import { groupBy, sum, uniq } from 'lodash'
+import { groupBy, sum, uniqBy } from 'lodash'
import { track } from '@amplitude/analytics-browser'
import { Pagination } from 'web/components/pagination'
import { useWindowSize } from 'web/hooks/use-window-size'
@@ -45,9 +44,13 @@ import { SiteLink } from 'web/components/site-link'
import { NotificationSettings } from 'web/components/NotificationSettings'
import { SEO } from 'web/components/SEO'
import { useUser } from 'web/hooks/use-user'
+import {
+ MultiUserTipLink,
+ MultiUserLinkInfo,
+ UserLink,
+} from 'web/components/user-link'
export const NOTIFICATIONS_PER_PAGE = 30
-const MULTIPLE_USERS_KEY = 'multipleUsers'
const HIGHLIGHT_CLASS = 'bg-indigo-50'
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
@@ -253,10 +256,22 @@ function IncomeNotificationGroupItem(props: {
notification.sourceText &&
(sum = parseInt(notification.sourceText) + sum)
)
- const uniqueUsers = uniq(
+ const uniqueUsers = uniqBy(
notificationsForSourceTitle.map((notification) => {
- return notification.sourceUserUsername
- })
+ let thisSum = 0
+ notificationsForSourceTitle
+ .filter(
+ (n) => n.sourceUserUsername === notification.sourceUserUsername
+ )
+ .forEach((n) => (thisSum = parseInt(n.sourceText) + thisSum))
+ return {
+ username: notification.sourceUserUsername,
+ name: notification.sourceUserName,
+ avatarUrl: notification.sourceUserAvatarUrl,
+ amountTipped: thisSum,
+ } as MultiUserLinkInfo
+ }),
+ (n) => n.username
)
const newNotification = {
@@ -264,7 +279,7 @@ function IncomeNotificationGroupItem(props: {
sourceText: sum.toString(),
sourceUserUsername:
uniqueUsers.length > 1
- ? MULTIPLE_USERS_KEY
+ ? JSON.stringify(uniqueUsers)
: notificationsForSourceTitle[0].sourceType,
}
newNotifications.push(newNotification)
@@ -402,9 +417,8 @@ function IncomeNotificationItem(props: {
} else if (sourceType === 'loan' && sourceText) {
reasonText = `of your invested bets returned as a`
// TODO: support just 'like' notification without a tip
- // TODO: show who tip-liked your market
} else if (sourceType === 'tip_and_like' && sourceText) {
- reasonText = `in likes on`
+ reasonText = !simple ? `liked` : `in likes on`
}
const streakInDays =
@@ -513,9 +527,12 @@ function IncomeNotificationItem(props: {
{incomeNotificationLabel()}
- {sourceType === 'tip' &&
- (sourceUserUsername === MULTIPLE_USERS_KEY ? (
- Multiple users
+ {(sourceType === 'tip' || sourceType === 'tip_and_like') &&
+ (sourceUserUsername?.includes(',') ? (
+
) : (