Add notifications for badge awards

This commit is contained in:
Ian Philips 2022-10-06 21:16:23 -04:00
parent 4d94f24892
commit 71fac10e44
13 changed files with 223 additions and 54 deletions

View File

@ -4,13 +4,12 @@ export type Badge = {
type: BadgeTypes type: BadgeTypes
createdTime: number createdTime: number
data: { [key: string]: any } data: { [key: string]: any }
name: string name: 'Proven Correct' | 'Streaker' | 'Market Maker'
} }
export type BadgeTypes = 'PROVEN_CORRECT' | 'STREAKER' | 'MARKET_CREATOR' export type BadgeTypes = 'PROVEN_CORRECT' | 'STREAKER' | 'MARKET_CREATOR'
export type ProvenCorrectBadgeData = { export type ProvenCorrectBadgeData = {
name: 'Proven Correct'
type: 'PROVEN_CORRECT' type: 'PROVEN_CORRECT'
data: { data: {
contractSlug: string contractSlug: string
@ -22,7 +21,6 @@ export type ProvenCorrectBadgeData = {
} }
export type MarketCreatorBadgeData = { export type MarketCreatorBadgeData = {
name: 'Market Maker'
type: 'MARKET_CREATOR' type: 'MARKET_CREATOR'
data: { data: {
totalContractsCreated: number totalContractsCreated: number
@ -30,7 +28,6 @@ export type MarketCreatorBadgeData = {
} }
export type StreakerBadgeData = { export type StreakerBadgeData = {
name: 'Streaker'
type: 'STREAKER' type: 'STREAKER'
data: { data: {
totalBettingStreak: number totalBettingStreak: number

View File

@ -79,6 +79,7 @@ export function scoreCommentorsAndBettors(
comments, comments,
(c) => c.betId && -profitById[c.betId] (c) => c.betId && -profitById[c.betId]
)[0]?.id )[0]?.id
const topCommentBetId = commentsById[topCommentId]?.betId
return { return {
topCommentId, topCommentId,
@ -87,5 +88,6 @@ export function scoreCommentorsAndBettors(
profitById, profitById,
commentsById, commentsById,
betsById, betsById,
topCommentBetId,
} }
} }

View File

@ -6,7 +6,12 @@ import {
Notification, Notification,
notification_reason_types, notification_reason_types,
} from '../../common/notification' } from '../../common/notification'
import { User } from '../../common/user' import {
MANIFOLD_AVATAR_URL,
MANIFOLD_USER_NAME,
MANIFOLD_USER_USERNAME,
User,
} from '../../common/user'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { getPrivateUser, getValues } from './utils' import { getPrivateUser, getValues } from './utils'
import { Comment } from '../../common/comment' import { Comment } from '../../common/comment'
@ -30,6 +35,7 @@ import {
import { filterDefined } from '../../common/util/array' import { filterDefined } from '../../common/util/array'
import { getNotificationDestinationsForUser } from '../../common/user-notification-preferences' import { getNotificationDestinationsForUser } from '../../common/user-notification-preferences'
import { ContractFollow } from '../../common/follow' import { ContractFollow } from '../../common/follow'
import { Badge } from 'common/badge'
const firestore = admin.firestore() const firestore = admin.firestore()
type recipients_to_reason_texts = { type recipients_to_reason_texts = {
@ -1087,6 +1093,43 @@ export const createBountyNotification = async (
sourceTitle: contract.question, sourceTitle: contract.question,
} }
return await notificationRef.set(removeUndefinedProps(notification)) return await notificationRef.set(removeUndefinedProps(notification))
}
// maybe TODO: send email notification to comment creator
export const createBadgeAwardedNotification = async (
user: User,
badge: Badge
) => {
const privateUser = await getPrivateUser(user.id)
if (!privateUser) return
const { sendToBrowser } = getNotificationDestinationsForUser(
privateUser,
'badges_awarded'
)
if (!sendToBrowser) return
const notificationRef = firestore
.collection(`/users/${user.id}/notifications`)
.doc()
const notification: Notification = {
id: notificationRef.id,
userId: user.id,
reason: 'badges_awarded',
createdTime: Date.now(),
isSeen: false,
sourceId: badge.type,
sourceType: 'badge',
sourceUpdateType: 'created',
sourceUserName: MANIFOLD_USER_NAME,
sourceUserUsername: MANIFOLD_USER_USERNAME,
sourceUserAvatarUrl: MANIFOLD_AVATAR_URL,
sourceText: `You earned a new ${badge.name} badge!`,
sourceSlug: `/${user.username}?show=badges&badge=${badge.type}`,
sourceTitle: badge.name,
data: {
badge,
},
}
return await notificationRef.set(removeUndefinedProps(notification))
// TODO send email notification
} }

View File

@ -12,6 +12,7 @@ import {
revalidateStaticProps, revalidateStaticProps,
} from './utils' } from './utils'
import { import {
createBadgeAwardedNotification,
createBetFillNotification, createBetFillNotification,
createBettingStreakBonusNotification, createBettingStreakBonusNotification,
createUniqueBettorBonusNotification, createUniqueBettorBonusNotification,
@ -317,6 +318,7 @@ async function handleBettingStreakBadgeAward(
if (newBettingStreak in streakerBadgeRarityThresholds) { if (newBettingStreak in streakerBadgeRarityThresholds) {
const badge = { const badge = {
type: 'STREAKER', type: 'STREAKER',
name: 'Streaker',
data: { data: {
totalBettingStreak: newBettingStreak, totalBettingStreak: newBettingStreak,
}, },
@ -335,5 +337,6 @@ async function handleBettingStreakBadgeAward(
}, },
}, },
}) })
await createBadgeAwardedNotification(user, badge)
} }
} }

View File

@ -1,7 +1,10 @@
import * as functions from 'firebase-functions' import * as functions from 'firebase-functions'
import { getUser, getValues } from './utils' import { getUser, getValues } from './utils'
import { createNewContractNotification } from './create-notification' import {
createBadgeAwardedNotification,
createNewContractNotification,
} from './create-notification'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { parseMentions, richTextToString } from '../../common/util/parse' import { parseMentions, richTextToString } from '../../common/util/parse'
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
@ -49,6 +52,7 @@ async function handleMarketCreatorBadgeAward(contractCreator: User) {
if (contracts.length in marketMakerBadgeRarityThresholds) { if (contracts.length in marketMakerBadgeRarityThresholds) {
const badge = { const badge = {
type: 'MARKET_CREATOR', type: 'MARKET_CREATOR',
name: 'Market Maker',
data: { data: {
totalContractsCreated: contracts.length, totalContractsCreated: contracts.length,
}, },
@ -72,5 +76,6 @@ async function handleMarketCreatorBadgeAward(contractCreator: User) {
}, },
}, },
}) })
await createBadgeAwardedNotification(contractCreator, badge)
} }
} }

View File

@ -1,6 +1,9 @@
import * as functions from 'firebase-functions' import * as functions from 'firebase-functions'
import { getUser, getValues } from './utils' import { getUser, getValues } from './utils'
import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification' import {
createBadgeAwardedNotification,
createCommentOrAnswerOrUpdatedContractNotification,
} from './create-notification'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { Bet } from '../../common/bet' import { Bet } from '../../common/bet'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
@ -50,18 +53,19 @@ async function handleResolvedContract(contract: Contract) {
firestore.collection(`contracts/${contract.id}/comments`) firestore.collection(`contracts/${contract.id}/comments`)
) )
const { topCommentId, profitById, commentsById, betsById } = const { topCommentId, profitById, commentsById, betsById, topCommentBetId } =
scoreCommentorsAndBettors(contract, bets, comments) scoreCommentorsAndBettors(contract, bets, comments)
if (topCommentId && profitById[topCommentId] > 0) { if (topCommentBetId && profitById[topCommentBetId] > 0) {
// award proven correct badge to user // award proven correct badge to user
const comment = commentsById[topCommentId] const comment = commentsById[topCommentId]
const bet = betsById[topCommentId] const bet = betsById[topCommentBetId]
const user = await getUser(comment.userId) const user = await getUser(comment.userId)
if (!user) return if (!user) return
const newProvenCorrectBadge = { const newProvenCorrectBadge = {
createdTime: Date.now(), createdTime: Date.now(),
type: 'PROVEN_CORRECT', type: 'PROVEN_CORRECT',
name: 'Proven Correct',
data: { data: {
contractSlug: contract.slug, contractSlug: contract.slug,
contractCreatorUsername: contract.creatorUsername, contractCreatorUsername: contract.creatorUsername,
@ -87,6 +91,7 @@ async function handleResolvedContract(contract: Contract) {
}, },
}, },
}) })
await createBadgeAwardedNotification(user, newProvenCorrectBadge)
} }
} }

View File

@ -17,7 +17,7 @@ async function main() {
.update({ .update({
notificationPreferences: { notificationPreferences: {
...privateUser.notificationPreferences, ...privateUser.notificationPreferences,
badges_awarded: [], badges_awarded: ['browser'],
}, },
}) })
}) })

View File

@ -3,8 +3,7 @@ import { resolvedPayout } from 'common/calculate'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { groupBy, mapValues, sumBy, sortBy } from 'lodash' import { groupBy, mapValues, sumBy } from 'lodash'
import { CommentTipMap } from 'web/hooks/use-tip-txns'
import { FeedBet } from '../feed/feed-bets' import { FeedBet } from '../feed/feed-bets'
import { FeedComment } from '../feed/feed-comments' import { FeedComment } from '../feed/feed-comments'
import { Spacer } from '../layout/spacer' import { Spacer } from '../layout/spacer'
@ -57,29 +56,34 @@ export function ContractTopTrades(props: {
contract: Contract contract: Contract
bets: Bet[] bets: Bet[]
comments: ContractComment[] comments: ContractComment[]
tips: CommentTipMap
}) { }) {
const { contract, bets, comments, tips } = props const { contract, bets, comments } = props
const { topBetId, topBettor, profitById, betsById } = const {
scoreCommentorsAndBettors(contract, bets, comments) topBetId,
topBettor,
// And also the comment with the highest profit profitById,
const topComment = sortBy(comments, (c) => c.betId && -profitById[c.betId])[0] betsById,
topCommentId,
commentsById,
topCommentBetId,
} = scoreCommentorsAndBettors(contract, bets, comments)
return ( return (
<div className="mt-12 max-w-sm"> <div className="mt-12 max-w-sm">
{topComment && profitById[topComment.id] > 0 && ( {topCommentBetId && profitById[topCommentBetId] > 0 && (
<> <>
<Title text="💬 Proven correct" className="!mt-0" /> <Title text="💬 Proven correct" className="!mt-0" />
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
<FeedComment contract={contract} comment={topComment} /> <FeedComment
contract={contract}
comment={commentsById[topCommentId]}
/>
</div> </div>
<Spacer h={16} /> <Spacer h={16} />
</> </>
)} )}
{/* If they're the same, only show the comment; otherwise show both */} {/* If they're the same, only show the comment; otherwise show both */}
{topBettor && topBetId !== topComment?.betId && profitById[topBetId] > 0 && ( {topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && (
<> <>
<Title text="💸 Best bet" className="!mt-0" /> <Title text="💸 Best bet" className="!mt-0" />
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">

View File

@ -131,6 +131,7 @@ export function NotificationSettings(props: {
'betting_streaks', 'betting_streaks',
'referral_bonuses', 'referral_bonuses',
'unique_bettors_on_your_contract', 'unique_bettors_on_your_contract',
'badges_awarded',
], ],
} }
const otherBalances: SectionData = { const otherBalances: SectionData = {

View File

@ -151,7 +151,7 @@ function StreakerBadgeItem(props: { badge: StreakerBadge; rarity: rarities }) {
const { badge, rarity } = props const { badge, rarity } = props
const { totalBettingStreak } = badge.data const { totalBettingStreak } = badge.data
return ( return (
<Col className={'text-center'}> <Col className={'cursor-default text-center'}>
<Medal rarity={rarity} /> <Medal rarity={rarity} />
<Tooltip <Tooltip
text={`Make predictions ${totalBettingStreak} day text={`Make predictions ${totalBettingStreak} day
@ -179,7 +179,7 @@ function MarketCreatorBadgeItem(props: {
const { badge, rarity } = props const { badge, rarity } = props
const { totalContractsCreated } = badge.data const { totalContractsCreated } = badge.data
return ( return (
<Col className={'text-center'}> <Col className={'cursor-default text-center'}>
<Medal rarity={rarity} /> <Medal rarity={rarity} />
<Tooltip <Tooltip
text={`Make ${totalContractsCreated} market${ text={`Make ${totalContractsCreated} market${

View File

@ -270,7 +270,11 @@ export function ContractPageContent(
<> <>
<div className="grid grid-cols-1 sm:grid-cols-2"> <div className="grid grid-cols-1 sm:grid-cols-2">
<ContractLeaderboard contract={contract} bets={bets} /> <ContractLeaderboard contract={contract} bets={bets} />
<ContractTopTrades contract={contract} bets={bets} /> <ContractTopTrades
contract={contract}
bets={bets}
comments={comments}
/>
</div> </div>
<Spacer h={12} /> <Spacer h={12} />
</> </>

View File

@ -13,11 +13,7 @@ import { Page } from 'web/components/page'
import { Title } from 'web/components/title' import { Title } from 'web/components/title'
import { doc, updateDoc } from 'firebase/firestore' import { doc, updateDoc } from 'firebase/firestore'
import { db } from 'web/lib/firebase/init' import { db } from 'web/lib/firebase/init'
import { import { MANIFOLD_AVATAR_URL, PAST_BETS, PrivateUser } from 'common/user'
MANIFOLD_AVATAR_URL,
MANIFOLD_USER_USERNAME,
PrivateUser,
} from 'common/user'
import clsx from 'clsx' import clsx from 'clsx'
import { RelativeTimestamp } from 'web/components/relative-timestamp' import { RelativeTimestamp } from 'web/components/relative-timestamp'
import { Linkify } from 'web/components/linkify' import { Linkify } from 'web/components/linkify'
@ -736,6 +732,24 @@ function NotificationItem(props: {
justSummary={justSummary} justSummary={justSummary}
/> />
) )
} else if (sourceType === 'badge') {
return (
<BadgeNotification
notification={notification}
isChildOfGroup={isChildOfGroup}
highlighted={highlighted}
justSummary={justSummary}
/>
)
} else if (sourceType === 'contract' && sourceUpdateType === 'closed') {
return (
<MarketClosedNotification
notification={notification}
isChildOfGroup={isChildOfGroup}
highlighted={highlighted}
justSummary={justSummary}
/>
)
} }
// TODO Add new notification components here // TODO Add new notification components here
@ -809,9 +823,16 @@ function NotificationFrame(props: {
subtitle: string subtitle: string
children: React.ReactNode children: React.ReactNode
isChildOfGroup?: boolean isChildOfGroup?: boolean
showUserName?: boolean
}) { }) {
const { notification, isChildOfGroup, highlighted, subtitle, children } = const {
props notification,
isChildOfGroup,
highlighted,
subtitle,
children,
showUserName,
} = props
const { const {
sourceType, sourceType,
sourceUserName, sourceUserName,
@ -822,7 +843,7 @@ function NotificationFrame(props: {
sourceUserUsername, sourceUserUsername,
sourceText, sourceText,
} = notification } = notification
const questionNeedsResolution = sourceUpdateType == 'closed'
const { width } = useWindowSize() const { width } = useWindowSize()
const isMobile = (width ?? 0) < 600 const isMobile = (width ?? 0) < 600
return ( return (
@ -852,18 +873,10 @@ function NotificationFrame(props: {
/> />
<Row className={'items-center text-gray-500 sm:justify-start'}> <Row className={'items-center text-gray-500 sm:justify-start'}>
<Avatar <Avatar
avatarUrl={ avatarUrl={sourceUserAvatarUrl}
questionNeedsResolution
? MANIFOLD_AVATAR_URL
: sourceUserAvatarUrl
}
size={'sm'} size={'sm'}
className={'z-10 mr-2'} className={'z-10 mr-2'}
username={ username={sourceUserUsername}
questionNeedsResolution
? MANIFOLD_USER_USERNAME
: sourceUserUsername
}
/> />
<div className={'flex w-full flex-row pl-1 sm:pl-0'}> <div className={'flex w-full flex-row pl-1 sm:pl-0'}>
<div <div
@ -872,12 +885,14 @@ function NotificationFrame(props: {
} }
> >
<div> <div>
{showUserName && (
<UserLink <UserLink
name={sourceUserName || ''} name={sourceUserName || ''}
username={sourceUserUsername || ''} username={sourceUserUsername || ''}
className={'relative mr-1 flex-shrink-0'} className={'relative mr-1 flex-shrink-0'}
short={isMobile} short={isMobile}
/> />
)}
{subtitle} {subtitle}
{isChildOfGroup ? ( {isChildOfGroup ? (
<RelativeTimestamp time={notification.createdTime} /> <RelativeTimestamp time={notification.createdTime} />
@ -965,6 +980,64 @@ function BetFillNotification(props: {
</NotificationFrame> </NotificationFrame>
) )
} }
function MarketClosedNotification(props: {
notification: Notification
highlighted: boolean
justSummary: boolean
isChildOfGroup?: boolean
}) {
const { notification, isChildOfGroup, highlighted, justSummary } = props
notification.sourceUserAvatarUrl = MANIFOLD_AVATAR_URL
return (
<NotificationFrame
notification={notification}
isChildOfGroup={isChildOfGroup}
highlighted={highlighted}
subtitle={'Please resolve'}
>
<Row>
<span>
{`Your market has closed. Please resolve it to pay out ${PAST_BETS}.`}
</span>
</Row>
</NotificationFrame>
)
}
function BadgeNotification(props: {
notification: Notification
highlighted: boolean
justSummary: boolean
isChildOfGroup?: boolean
}) {
const { notification, isChildOfGroup, highlighted, justSummary } = props
const { sourceText } = notification
const subtitle = 'You earned a new badge!'
notification.sourceUserAvatarUrl = '/award.svg'
if (justSummary) {
return (
<NotificationSummaryFrame notification={notification} subtitle={subtitle}>
<Row className={'line-clamp-1'}>
<span>{sourceText} 🎉</span>
</Row>
</NotificationSummaryFrame>
)
}
return (
<NotificationFrame
notification={notification}
isChildOfGroup={isChildOfGroup}
highlighted={highlighted}
subtitle={subtitle}
showUserName={false}
>
<Row>
<span>{sourceText} 🎉</span>
</Row>
</NotificationFrame>
)
}
function ContractResolvedNotification(props: { function ContractResolvedNotification(props: {
notification: Notification notification: Notification
@ -1136,6 +1209,11 @@ function getSourceUrl(notification: Notification) {
sourceId ?? '', sourceId ?? '',
sourceType sourceType
)}` )}`
else if (sourceSlug)
return `/${sourceSlug}#${getSourceIdForLinkComponent(
sourceId ?? '',
sourceType
)}`
} }
function getSourceIdForLinkComponent( function getSourceIdForLinkComponent(
@ -1235,7 +1313,6 @@ function getReasonForShowingNotification(
reasonText = justSummary ? 'asked the question' : 'asked' reasonText = justSummary ? 'asked the question' : 'asked'
else if (sourceUpdateType === 'resolved') else if (sourceUpdateType === 'resolved')
reasonText = justSummary ? `resolved the question` : `resolved` reasonText = justSummary ? `resolved the question` : `resolved`
else if (sourceUpdateType === 'closed') reasonText = `Please resolve`
else reasonText = justSummary ? 'updated the question' : `updated` else reasonText = justSummary ? 'updated the question' : `updated`
break break
case 'answer': case 'answer':

28
web/public/award.svg Normal file
View File

@ -0,0 +1,28 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<polyline fill="#92d3f5" stroke="#92d3f5" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="54.9988,4.0221 43,16.0208 36,16.0208 30.9584,10.9792 37.9207,4.0169 54.9988,4.0169"/>
<polyline fill="#ea5a47" stroke="#ea5a47" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="23.9831,4.0039 36,16.0208 29,16.0208 16.9675,3.9883 23.9831,3.9883"/>
<polyline fill="#fcea2b" stroke="none" points="28,22.4271 28,17 44,17 44,22.4271"/>
<circle cx="36" cy="45.0208" r="23" fill="#fcea2b" stroke="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<polygon fill="#f1b31c" stroke="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="35.9861,28 30.8575,38.4014 19.3815,40.0733 27.6891,48.1652 25.7329,59.5961 35.9958,54.1957 46.2628,59.5885 44.2981,48.159 52.5996,40.061 41.1225,38.3976"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<circle cx="36" cy="45.0208" r="23" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-width="2"/>
<circle cx="36" cy="45.0208" r="23" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<circle cx="36" cy="45.0208" r="23" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-width="2"/>
<line x1="29" x2="29" y1="19" y2="16.0208" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<line x1="43" x2="43" y1="19" y2="16.0208" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<line x1="29" x2="43" y1="16.0208" y2="16.0208" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<line x1="25.9896" x2="16.9675" y1="13.0104" y2="3.9883" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<line x1="31.9896" x2="23.9831" y1="12.0104" y2="4.0039" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<line x1="34" x2="37.9207" y1="8" y2="4.0169" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<line x1="46" x2="54.9988" y1="13" y2="4.0221" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<line x1="16.9675" x2="23.9831" y1="3.9883" y2="3.9883" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<line x1="37.9207" x2="54.9988" y1="4.0169" y2="4.0169" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<circle cx="36" cy="45.0208" r="23" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-width="2"/>
<polygon fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="35.9861,28 30.8575,38.4014 19.3815,40.0733 27.6891,48.1652 25.7329,59.5961 35.9958,54.1957 46.2628,59.5885 44.2981,48.159 52.5996,40.061 41.1225,38.3976"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB