From e969540c72dab78770df9941e3af12adc0941106 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Wed, 6 Jul 2022 15:06:41 -0600 Subject: [PATCH 01/24] Slight notifications refactor --- web/pages/notifications.tsx | 726 ++++++++++++++++++------------------ 1 file changed, 363 insertions(+), 363 deletions(-) diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 54dbdd09..382505e2 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -166,20 +166,6 @@ export default function Notifications() { ) } -export const setNotificationsAsSeen = (notifications: Notification[]) => { - notifications.forEach((notification) => { - if (!notification.isSeen) - updateDoc( - doc(db, `users/${notification.userId}/notifications/`, notification.id), - { - isSeen: true, - viewTime: new Date(), - } - ) - }) - return notifications -} - function IncomeNotificationGroupItem(props: { notificationGroup: NotificationGroup className?: string @@ -556,6 +542,369 @@ function NotificationGroupItem(props: { ) } +function NotificationItem(props: { + notification: Notification + justSummary?: boolean + hideTitle?: boolean +}) { + const { notification, justSummary, hideTitle } = props + const { + sourceType, + sourceId, + sourceUserName, + sourceUserAvatarUrl, + sourceUpdateType, + reasonText, + reason, + sourceUserUsername, + createdTime, + sourceText, + sourceContractCreatorUsername, + sourceContractSlug, + } = notification + + const [defaultNotificationText, setDefaultNotificationText] = + useState('') + + useEffect(() => { + if (sourceText) { + setDefaultNotificationText(sourceText) + } else if (reasonText) { + // Handle arbitrary notifications with reason text here. + setDefaultNotificationText(reasonText) + } + }, [reasonText, sourceText]) + + const [highlighted] = useState(!notification.isSeen) + + useEffect(() => { + setNotificationsAsSeen([notification]) + }, [notification]) + + if (justSummary) { + return ( + +
+
+ +
+ + {sourceType && + reason && + getReasonForShowingNotification(notification, true, true)} + +
+ +
+
+
+
+
+ ) + } + + return ( +
+ + + +
+
+ {sourceUpdateType != 'closed' && ( + + )} + {sourceType && reason && ( +
+ + {getReasonForShowingNotification(notification, false, true)} + + {!hideTitle && ( + + )} +
+ )} + {sourceId && + sourceContractSlug && + sourceContractCreatorUsername ? ( + + ) : ( + + )} +
+
+
+
+ +
+ +
+ +
+ ) +} + +export const setNotificationsAsSeen = (notifications: Notification[]) => { + notifications.forEach((notification) => { + if (!notification.isSeen) + updateDoc( + doc(db, `users/${notification.userId}/notifications/`, notification.id), + { + isSeen: true, + viewTime: new Date(), + } + ) + }) + return notifications +} + +function NotificationLink(props: { notification: Notification }) { + const { notification } = props + const { + sourceType, + sourceContractTitle, + sourceContractCreatorUsername, + sourceContractSlug, + sourceSlug, + sourceTitle, + } = notification + return ( + + {sourceContractTitle || sourceTitle} + + ) +} + +function getSourceUrl(notification: Notification) { + const { + sourceType, + sourceId, + sourceUserUsername, + sourceContractCreatorUsername, + sourceContractSlug, + sourceSlug, + } = notification + if (sourceType === 'follow') return `/${sourceUserUsername}` + if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}` + if ( + sourceContractCreatorUsername && + sourceContractSlug && + sourceType === 'user' + ) + return `/${sourceContractCreatorUsername}/${sourceContractSlug}` + if (sourceType === 'tip') + return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}` + if (sourceContractCreatorUsername && sourceContractSlug) + return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent( + sourceId ?? '', + sourceType + )}` +} + +function getSourceIdForLinkComponent( + sourceId: string, + sourceType?: notification_source_types +) { + switch (sourceType) { + case 'answer': + return `answer-${sourceId}` + case 'comment': + return sourceId + case 'contract': + return '' + default: + return sourceId + } +} + +function NotificationTextLabel(props: { + defaultText: string + contract?: Contract | null + notification: Notification + className?: string + justSummary?: boolean +}) { + const { contract, className, defaultText, notification, justSummary } = props + const { sourceUpdateType, sourceType, sourceText, sourceContractTitle } = + notification + if (sourceType === 'contract') { + if (justSummary) + return {contract?.question || sourceContractTitle} + if (!sourceText) return
+ // Resolved contracts + if (sourceType === 'contract' && sourceUpdateType === 'resolved') { + { + if (sourceText === 'YES' || sourceText == 'NO') { + return + } + if (sourceText.includes('%')) + return ( + + ) + if (sourceText === 'CANCEL') return + if (sourceText === 'MKT' || sourceText === 'PROB') return + } + } + // Close date will be a number - it looks better without it + if (sourceUpdateType === 'closed') { + return
+ } + // Updated contracts + // Description will be in default text + if (parseInt(sourceText) > 0) { + return ( + + Updated close time: {new Date(parseInt(sourceText)).toLocaleString()} + + ) + } + } else if (sourceType === 'user' && sourceText) { + return ( + + As a thank you, we sent you{' '} + + {formatMoney(parseInt(sourceText))} + + ! + + ) + } else if (sourceType === 'liquidity' && sourceText) { + return ( + {formatMoney(parseInt(sourceText))} + ) + } else if ((sourceType === 'bonus' || sourceType === 'tip') && sourceText) { + return ( + + {'+' + formatMoney(parseInt(sourceText))} + + ) + } + // return default text + return ( +
+ +
+ ) +} + +function getReasonForShowingNotification( + notification: Notification, + simple?: boolean, + replaceOn?: boolean +) { + const { sourceType, sourceUpdateType, reason, sourceSlug } = notification + let reasonText: string + switch (sourceType) { + case 'comment': + if (reason === 'reply_to_users_answer') + reasonText = !simple ? 'replied to you on' : 'replied' + else if (reason === 'tagged_user') + reasonText = !simple ? 'tagged you on' : 'tagged you' + else if (reason === 'reply_to_users_comment') + reasonText = !simple ? 'replied to you on' : 'replied' + else if (reason === 'on_users_contract') + reasonText = !simple ? `commented on your question` : 'commented' + else if (reason === 'on_contract_with_users_comment') + reasonText = `commented on` + else if (reason === 'on_contract_with_users_answer') + reasonText = `commented on` + else if (reason === 'on_contract_with_users_shares_in') + reasonText = `commented on` + else reasonText = `commented on` + break + case 'contract': + if (reason === 'you_follow_user') reasonText = 'asked' + else if (sourceUpdateType === 'resolved') reasonText = `resolved` + else if (sourceUpdateType === 'closed') + reasonText = `Please resolve your question` + else reasonText = `updated` + break + case 'answer': + if (reason === 'on_users_contract') reasonText = `answered your question ` + else if (reason === 'on_contract_with_users_comment') + reasonText = `answered` + else if (reason === 'on_contract_with_users_answer') + reasonText = `answered` + else if (reason === 'on_contract_with_users_shares_in') + reasonText = `answered` + else reasonText = `answered` + break + case 'follow': + reasonText = 'followed you' + break + case 'liquidity': + reasonText = 'added liquidity to your question' + break + case 'group': + reasonText = 'added you to the group' + break + case 'user': + if (sourceSlug && reason === 'user_joined_to_bet_on_your_market') + reasonText = 'joined to bet on your market' + else if (sourceSlug) reasonText = 'joined because you shared' + else reasonText = 'joined because of you' + break + default: + reasonText = '' + } + + return ( + + {replaceOn ? reasonText.replace(' on', '') : reasonText} + + ) +} + // TODO: where should we put referral bonus notifications? function NotificationSettings() { const user = useUser() @@ -751,352 +1100,3 @@ function NotificationSettings() {
) } - -function NotificationLink(props: { notification: Notification }) { - const { notification } = props - const { - sourceType, - sourceContractTitle, - sourceContractCreatorUsername, - sourceContractSlug, - sourceSlug, - sourceTitle, - } = notification - return ( - - {sourceContractTitle || sourceTitle} - - ) -} - -function getSourceUrl(notification: Notification) { - const { - sourceType, - sourceId, - sourceUserUsername, - sourceContractCreatorUsername, - sourceContractSlug, - sourceSlug, - } = notification - if (sourceType === 'follow') return `/${sourceUserUsername}` - if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}` - if ( - sourceContractCreatorUsername && - sourceContractSlug && - sourceType === 'user' - ) - return `/${sourceContractCreatorUsername}/${sourceContractSlug}` - if (sourceType === 'tip') - return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}` - if (sourceContractCreatorUsername && sourceContractSlug) - return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent( - sourceId ?? '', - sourceType - )}` -} - -function getSourceIdForLinkComponent( - sourceId: string, - sourceType?: notification_source_types -) { - switch (sourceType) { - case 'answer': - return `answer-${sourceId}` - case 'comment': - return sourceId - case 'contract': - return '' - default: - return sourceId - } -} - -function NotificationItem(props: { - notification: Notification - justSummary?: boolean - hideTitle?: boolean -}) { - const { notification, justSummary, hideTitle } = props - const { - sourceType, - sourceId, - sourceUserName, - sourceUserAvatarUrl, - sourceUpdateType, - reasonText, - reason, - sourceUserUsername, - createdTime, - sourceText, - sourceContractCreatorUsername, - sourceContractSlug, - } = notification - - const [defaultNotificationText, setDefaultNotificationText] = - useState('') - - useEffect(() => { - if (sourceText) { - setDefaultNotificationText(sourceText) - } else if (reasonText) { - // Handle arbitrary notifications with reason text here. - setDefaultNotificationText(reasonText) - } - }, [reasonText, sourceText]) - - const [highlighted] = useState(!notification.isSeen) - - useEffect(() => { - setNotificationsAsSeen([notification]) - }, [notification]) - - if (justSummary) { - return ( - -
-
- -
- - {sourceType && - reason && - getReasonForShowingNotification(notification, true, true)} - -
- -
-
-
-
-
- ) - } - - return ( -
- - - -
-
- {sourceUpdateType != 'closed' && ( - - )} - {sourceType && reason && ( -
- - {getReasonForShowingNotification(notification, false, true)} - - {!hideTitle && ( - - )} -
- )} - {sourceId && - sourceContractSlug && - sourceContractCreatorUsername ? ( - - ) : ( - - )} -
-
-
-
- -
- -
- -
- ) -} - -function NotificationTextLabel(props: { - defaultText: string - contract?: Contract | null - notification: Notification - className?: string - justSummary?: boolean -}) { - const { contract, className, defaultText, notification, justSummary } = props - const { sourceUpdateType, sourceType, sourceText, sourceContractTitle } = - notification - if (sourceType === 'contract') { - if (justSummary) - return {contract?.question || sourceContractTitle} - if (!sourceText) return
- // Resolved contracts - if (sourceType === 'contract' && sourceUpdateType === 'resolved') { - { - if (sourceText === 'YES' || sourceText == 'NO') { - return - } - if (sourceText.includes('%')) - return ( - - ) - if (sourceText === 'CANCEL') return - if (sourceText === 'MKT' || sourceText === 'PROB') return - } - } - // Close date will be a number - it looks better without it - if (sourceUpdateType === 'closed') { - return
- } - // Updated contracts - // Description will be in default text - if (parseInt(sourceText) > 0) { - return ( - - Updated close time: {new Date(parseInt(sourceText)).toLocaleString()} - - ) - } - } else if (sourceType === 'user' && sourceText) { - return ( - - As a thank you, we sent you{' '} - - {formatMoney(parseInt(sourceText))} - - ! - - ) - } else if (sourceType === 'liquidity' && sourceText) { - return ( - {formatMoney(parseInt(sourceText))} - ) - } else if ((sourceType === 'bonus' || sourceType === 'tip') && sourceText) { - return ( - - {'+' + formatMoney(parseInt(sourceText))} - - ) - } - // return default text - return ( -
- -
- ) -} - -function getReasonForShowingNotification( - notification: Notification, - simple?: boolean, - replaceOn?: boolean -) { - const { sourceType, sourceUpdateType, reason, sourceSlug } = notification - let reasonText: string - switch (sourceType) { - case 'comment': - if (reason === 'reply_to_users_answer') - reasonText = !simple ? 'replied to you on' : 'replied' - else if (reason === 'tagged_user') - reasonText = !simple ? 'tagged you on' : 'tagged you' - else if (reason === 'reply_to_users_comment') - reasonText = !simple ? 'replied to you on' : 'replied' - else if (reason === 'on_users_contract') - reasonText = !simple ? `commented on your question` : 'commented' - else if (reason === 'on_contract_with_users_comment') - reasonText = `commented on` - else if (reason === 'on_contract_with_users_answer') - reasonText = `commented on` - else if (reason === 'on_contract_with_users_shares_in') - reasonText = `commented on` - else reasonText = `commented on` - break - case 'contract': - if (reason === 'you_follow_user') reasonText = 'asked' - else if (sourceUpdateType === 'resolved') reasonText = `resolved` - else if (sourceUpdateType === 'closed') - reasonText = `Please resolve your question` - else reasonText = `updated` - break - case 'answer': - if (reason === 'on_users_contract') reasonText = `answered your question ` - else if (reason === 'on_contract_with_users_comment') - reasonText = `answered` - else if (reason === 'on_contract_with_users_answer') - reasonText = `answered` - else if (reason === 'on_contract_with_users_shares_in') - reasonText = `answered` - else reasonText = `answered` - break - case 'follow': - reasonText = 'followed you' - break - case 'liquidity': - reasonText = 'added liquidity to your question' - break - case 'group': - reasonText = 'added you to the group' - break - case 'user': - if (sourceSlug && reason === 'user_joined_to_bet_on_your_market') - reasonText = 'joined to bet on your market' - else if (sourceSlug) reasonText = 'joined because you shared' - else reasonText = 'joined because of you' - break - default: - reasonText = '' - } - - return ( - - {replaceOn ? reasonText.replace(' on', '') : reasonText} - - ) -} From 2591655269ecc300f1b77779a460b882d4f89d56 Mon Sep 17 00:00:00 2001 From: ahalekelly Date: Wed, 6 Jul 2022 14:41:13 -0700 Subject: [PATCH 02/24] Fix docs edit link (#624) * Fix docs edit link * Update github links --- docs/docusaurus.config.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 85129d87..0cf5a8f2 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -26,8 +26,7 @@ const config = { docs: { routeBasePath: '/', sidebarPath: require.resolve('./sidebars.js'), - // Please change this to your repo. - editUrl: 'https://github.com/manifoldmarkets/manifold/tree/main/docs/docs', + editUrl: 'https://github.com/manifoldmarkets/manifold/tree/main/docs', remarkPlugins: [math], rehypePlugins: [katex], }, @@ -72,7 +71,7 @@ const config = { label: 'Docs', }, { - href: 'https://github.com/manifoldmarkets/docs', + href: 'https://github.com/manifoldmarkets/manifold/tree/main/docs/docs', label: 'GitHub', position: 'right', }, @@ -116,7 +115,7 @@ const config = { }, { label: 'GitHub', - href: 'https://github.com/manifoldmarkets/docs', + href: 'https://github.com/manifoldmarkets/manifold/', }, ], }, From a23c744c3e1eeda0ada57b779c607555114161aa Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Wed, 6 Jul 2022 17:24:53 -0600 Subject: [PATCH 03/24] Small groups UX changes --- web/components/create-question-button.tsx | 14 +- web/components/groups/group-chat.tsx | 3 +- web/lib/firebase/groups.ts | 2 +- web/pages/group/[...slugs]/index.tsx | 154 ++++++++++++++-------- web/pages/notifications.tsx | 4 +- 5 files changed, 113 insertions(+), 64 deletions(-) diff --git a/web/components/create-question-button.tsx b/web/components/create-question-button.tsx index 564beb83..a9161ac6 100644 --- a/web/components/create-question-button.tsx +++ b/web/components/create-question-button.tsx @@ -3,6 +3,8 @@ import clsx from 'clsx' import { firebaseLogin, User } from 'web/lib/firebase/users' import React from 'react' +export const createButtonStyle = + 'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0 h-11' export const CreateQuestionButton = (props: { user: User | null | undefined overrideText?: string @@ -12,20 +14,20 @@ export const CreateQuestionButton = (props: { const gradient = 'from-indigo-500 to-blue-500 hover:from-indigo-700 hover:to-blue-700' - const buttonStyle = - 'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0' - const { user, overrideText, className, query } = props return ( -
+
{user ? ( - ) : ( - )} diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 114a9003..13028313 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -36,6 +36,7 @@ export function GroupChat(props: { const [inputRef, setInputRef] = useState(null) const [groupedMessages, setGroupedMessages] = useState([]) const router = useRouter() + const isMember = user && group.memberIds.includes(user?.id) useMemo(() => { // Group messages with createdTime within 2 minutes of each other. @@ -120,7 +121,7 @@ export function GroupChat(props: { ))} {messages.length === 0 && (
- No messages yet. Why not{' '} + No messages yet. Why not{isMember ? ` ` : ' join and '}
), href: groupPath(group.slug, 'questions'), }, + { + title: 'Leaderboards', + content: leaderboard, + href: groupPath(group.slug, 'rankings'), + }, { title: 'About', content: aboutTab, @@ -309,13 +319,16 @@ function JoinOrCreateButton(props: { isMember: boolean }) { const { group, user, isMember } = props - return isMember ? ( - + return user && isMember ? ( + + + + ) : group.anyoneCanJoin ? ( ) : null @@ -389,11 +402,51 @@ function GroupOverview(props: { )} + + + ) } +function SearchBar(props: { setQuery: (query: string) => void }) { + const { setQuery } = props + const debouncedQuery = debounce(setQuery, 50) + return ( +
+ + debouncedQuery(e.target.value)} + placeholder="Find a member" + className="input input-bordered mb-4 w-full pl-12" + /> +
+ ) +} + +function GroupMemberSearch(props: { group: Group }) { + const [query, setQuery] = useState('') + const members = useMembers(props.group) + + // TODO use find-active-contracts to sort by? + const matches = sortBy(members, [(member) => member.name]).filter( + (m) => + checkAgainstQuery(query, m.name) || checkAgainstQuery(query, m.username) + ) + return ( +
+ + + {matches.length > 0 && ( + m.id)} /> + )} + +
+ ) +} + export function GroupMembersList(props: { group: Group }) { const { group } = props const members = useMembers(group) @@ -449,32 +502,24 @@ function GroupLeaderboards(props: { }) { const { traderScores, creatorScores, members, topTraders, topCreators } = props - const [includeOutsiders, setIncludeOutsiders] = useState(false) // Consider hiding M$0 + // If it's just one member (curator), show all bettors, otherwise just show members return ( - - Include all users - - -
- {!includeOutsiders ? ( + {members.length > 1 ? ( <> traderScores[user.id] ?? 0} - title="🏅 Top bettors" + title="🏅 Bettor rankings" header="Profit" /> creatorScores[user.id] ?? 0} - title="🏅 Top creators" + title="🏅 Creator rankings" header="Market volume" /> @@ -543,16 +588,17 @@ function AddContractButton(props: { group: Group; user: User }) {
- +
- +
) } diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 382505e2..08ef9bb8 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -347,10 +347,10 @@ function IncomeNotificationItem(props: { let reasonText = '' if (sourceType === 'bonus' && sourceText) { reasonText = !simple - ? `bonus for ${ + ? `Bonus for ${ parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT } unique bettors` - : ' bonus for unique bettors on' + : 'bonus on' } else if (sourceType === 'tip') { reasonText = !simple ? `tipped you` : `in tips on` } From 93b29000152ed4434853252673f4459e36f69ad8 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Thu, 7 Jul 2022 06:53:14 -0600 Subject: [PATCH 04/24] Groups UX on mobile --- web/components/create-question-button.tsx | 2 + web/components/groups/group-chat.tsx | 2 +- web/components/layout/tabs.tsx | 5 ++- web/pages/group/[...slugs]/index.tsx | 51 ++++++++++++++++++----- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/web/components/create-question-button.tsx b/web/components/create-question-button.tsx index a9161ac6..b8b5dcf3 100644 --- a/web/components/create-question-button.tsx +++ b/web/components/create-question-button.tsx @@ -2,6 +2,8 @@ import Link from 'next/link' import clsx from 'clsx' import { firebaseLogin, User } from 'web/lib/firebase/users' import React from 'react' +import { PlusIcon } from '@heroicons/react/outline' +import { Row } from 'web/components/layout/row' export const createButtonStyle = 'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0 h-11' diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index 13028313..1298065d 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -97,7 +97,7 @@ export function GroupChat(props: { } return ( - + void + className?: string }) { - const { tabs, defaultIndex, labelClassName, onClick } = props + const { tabs, defaultIndex, labelClassName, onClick, className } = props const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0) const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case return ( <> -
+