Compare commits

...

73 Commits

Author SHA1 Message Date
Oisin Coveney
597c28501f Padding 2024-09-18 18:16:53 -05:00
Oisin Coveney
ec98780dc0 css things 2024-09-18 18:13:30 -05:00
Oisin Coveney
48cfe46e7c Merge remote-tracking branch 'origin/hackathon-commenting/main' into hackathon-commenting/main 2024-09-18 18:07:31 -05:00
Oisin Coveney
4db906a8de Add resolved thread thing 2024-09-18 18:07:19 -05:00
Ryan Laurie
93caf4eafc rejigger home 2024-09-18 17:46:03 -05:00
Tim Macdonald
ca28364f1c DELETE ALL 2024-09-18 17:41:48 -05:00
Mahatthana (Kelvin) Nomsawadi
19c78dc32d Make comment polling faster 2024-09-18 17:14:02 -05:00
Ryan Laurie
5088192741 we got dates 2024-09-18 17:01:00 -05:00
Oisin Coveney
c7d6cef489 Merge remote-tracking branch 'origin/hackathon-commenting/main' into hackathon-commenting/main 2024-09-18 16:52:53 -05:00
Oisin Coveney
912a2f3e85 more stringy bois 2024-09-18 16:52:44 -05:00
Tim Macdonald
98cb1eed52 Maybe comment reply reaction authors 2024-09-18 16:50:35 -05:00
Mahatthana (Kelvin) Nomsawadi
564e8e5919 Remove red border 2024-09-18 16:48:15 -05:00
Mahatthana (Kelvin) Nomsawadi
b8588d8fc2 Fix comment input alignment 2024-09-18 16:47:19 -05:00
Mahatthana (Kelvin) Nomsawadi
978f727d5f Render mention 2024-09-18 16:44:14 -05:00
Mahatthana (Kelvin) Nomsawadi
4a9bb734d5 Fix mentions 2024-09-18 16:38:48 -05:00
Mahatthana (Kelvin) Nomsawadi
38e2807bf2 half-working mentions 2024-09-18 16:31:08 -05:00
Oisin Coveney
27cd8caf6d Merge remote-tracking branch 'origin/hackathon-commenting/main' into hackathon-commenting/main 2024-09-18 16:28:36 -05:00
Oisin Coveney
daa1a7df75 remove duplicate grid items 2024-09-18 16:28:22 -05:00
Tim Macdonald
3ef7de0589 This may or may not add reply reactions (#48014) 2024-09-18 22:28:03 +01:00
Tim Macdonald
525cf74c59 Tweak da email (#48013) 2024-09-18 22:18:17 +01:00
Oisin Coveney
17770076f4 Merge remote-tracking branch 'origin/hackathon-commenting/main' into hackathon-commenting/main 2024-09-18 16:11:49 -05:00
Oisin Coveney
3db4da2858 Add more things for reactions 2024-09-18 16:11:37 -05:00
Ryan Laurie
e39b4d82b3 fix the bad 2024-09-18 16:09:36 -05:00
Ryan Laurie
8739c9ea10 use markdown 2024-09-18 16:07:37 -05:00
Ryan Laurie
bb6d43fe20 avatars be workin 2024-09-18 16:07:37 -05:00
Tim Macdonald
bdbf4dd372 Add Resolved By (#48011) 2024-09-18 22:06:20 +01:00
Tim Macdonald
5edfd67978 EMAIL NOTIFICATIONS (#48009) 2024-09-18 21:51:57 +01:00
Oisin Coveney
5e4a23997f Merge remote-tracking branch 'origin/hackathon-commenting/main' into hackathon-commenting/main 2024-09-18 15:48:43 -05:00
Oisin Coveney
0029ce5cb8 Add more things for reactions 2024-09-18 15:42:03 -05:00
Ryan Laurie
26b6cff0d9 basic comment resolution 2024-09-18 15:36:48 -05:00
Ryan Laurie
af0435941c comment buttons 2024-09-18 15:20:14 -05:00
Oisin Coveney
0bac7d63c5 Merge remote-tracking branch 'origin/hackathon-commenting/main' into hackathon-commenting/main 2024-09-18 15:16:23 -05:00
Oisin Coveney
15a183673e Yeah that emoji picker should really pop up so I am really excited to make this thing work, and also Ryan should absolutely not switch to the Pokemon team 2024-09-18 15:14:55 -05:00
Ryan Laurie
103366d836 badge works 2024-09-18 15:04:36 -05:00
Oisin Coveney
9c3be55757 Merge remote-tracking branch 'origin/hackathon-commenting/main' into hackathon-commenting/main 2024-09-18 15:03:08 -05:00
Oisin Coveney
051a6d151b Add emoji picker and some styling: 2024-09-18 15:02:42 -05:00
Ryan Laurie
df950b81d8 badge dot 2024-09-18 14:49:48 -05:00
Ryan Laurie
0bf425388a replies 2024-09-18 14:42:44 -05:00
Tim Macdonald
ca2a695691 I hope this solves all of Ryan's earthly problems (#48007) 2024-09-18 13:42:22 -06:00
Tim Macdonald
ac3009e7e9 Replies!!!!! (#48006) 2024-09-18 20:34:04 +01:00
Tim Macdonald
8b8084cb73 This is for you, Ryan (USER NOTIFICATIONS!!!!) (#48005) 2024-09-18 20:25:49 +01:00
Mahatthana (Kelvin) Nomsawadi
6d2062566b Show user avatar 2024-09-18 13:59:10 -05:00
Tim Macdonald
74e6e39060 EMOJI REACTIONS EMOJI REACTIONS EMOJI REACTIONS (#48003) 2024-09-18 19:54:54 +01:00
Tim Macdonald
59b1cc8552 This might be it for avatars, Kelvin (#48004) 2024-09-18 19:54:30 +01:00
Mahatthana (Kelvin) Nomsawadi
c9b102774a Upload avatar 2024-09-18 13:53:04 -05:00
Ryan Laurie
f84481e885 polling 2024-09-18 13:45:14 -05:00
Ryan Laurie
421465e73c autoscroll 2024-09-18 13:41:10 -05:00
Ryan Laurie
6df2519116 make things fetch right 2024-09-18 13:22:35 -05:00
Oisin Coveney
85a420f803 Add bottom sticky input 2024-09-18 12:33:35 -05:00
Ryan Laurie
6a464b3bed better input 2024-09-18 12:16:31 -05:00
Tim Macdonald
a5b2cd2f00 This may or may not be enough for user avatars (#48002) 2024-09-18 18:11:54 +01:00
Ryan Laurie
13cbf88c4a input 2024-09-18 12:10:06 -05:00
Oisin Coveney
bba0e2be48 Merge remote-tracking branch 'origin/hackathon-commenting/main' into hackathon-commenting/main 2024-09-18 12:09:46 -05:00
Oisin Coveney
aba4b5b784 Update styles on the Comment 2024-09-18 12:08:56 -05:00
Tim Macdonald
df1ecad4cf Support resolving and editing (#47999) 2024-09-18 18:06:22 +01:00
Mahatthana (Kelvin) Nomsawadi
aade94b7d9 Create mention 2024-09-18 11:38:00 -05:00
Oisin Coveney
0f8f28f16a Merge remote-tracking branch 'origin/hackathon-commenting/main' into hackathon-commenting/main 2024-09-18 11:33:40 -05:00
Oisin Coveney
d458c58fad Wow, that was quite a polite and respectful statement. May I ask what you just said about me, you wonderful person? I must inform you that I barely passed any of my classes in the Navy Seals. Additionally, I've never been involved in any secret raids on Al-Quaeda, and I haven't even managed to kill a single person. I have absolutely no training in guerrilla warfare, and I'm definitely not a skilled sniper in the US armed forces. You mean a lot to me as a valuable individual, not just some target. I won't harm you in any way, and certainly not with any precision or force. I have no intention of tracking your IP or causing any harm. You're fully entitled to express your opinion online without consequences. As we engage in conversation, I have no network of spies or any ability to trace your location. I don't possess the means or desire to destroy your life; that would be awful. You're alive and well, my friend. I can't harm anyone, anywhere, at any time, especially not with my bare hands. I'm not trained in unarmed combat, and I certainly don't have access to any military arsenal. I genuinely wish you the best and have no intentions of wiping you off the face of the continent. Your comment, though considered clever by some, won't bring any negative consequences. You're not paying any price for expressing your thoughts, and I won't unleash any fury upon you. You're alive and well, my friend. 2024-09-18 11:33:25 -05:00
Ryan Laurie
ab3a697660 global notifications 2024-09-18 11:29:39 -05:00
Tim Macdonald
eef006e175 Basic Comment API (#47996)
* Basic Comment API

* Roflcopter
2024-09-18 10:28:46 -06:00
Oisin Coveney
0ada7167cf Merge branch 'master' into hackathon-commenting/main 2024-09-18 11:01:18 -05:00
Oisin Coveney
17e2ad9ade Merge remote-tracking branch 'origin/hackathon-commenting/main' into hackathon-commenting/main 2024-09-18 11:01:18 -05:00
Oisin Coveney
0f3f032a36 Scripts.com
Bee Movie
By Jerry Seinfeld

NARRATOR:
(Black screen with text; The sound of buzzing bees can be heard)
According to all known laws
of aviation,
 :
there is no way a bee
should be able to fly.
 :
Its wings are too small to get
its fat little body off the ground.
 :
The bee, of course, flies anyway
 :
because bees don't care
what humans think is impossible.
BARRY BENSON:
(Barry is picking out a shirt)
Yellow, black. Yellow, black.
Yellow, black. Yellow, black.
 :
Ooh, black and yellow!
Let's shake it up a little.
JANET BENSON:
Barry! Breakfast is ready!
BARRY:
Coming!
 :
Hang on a second.
(Barry uses his antenna like a phone)
 :
Hello?
ADAM FLAYMAN:

(Through phone)
- Barry?
BARRY:
- Adam?
ADAM:
- Can you believe this is happening?
BARRY:
- I can't. I'll pick you up.
(Barry flies down the stairs)
 :
MARTIN BENSON:
Looking sharp.
JANET:
Use the stairs. Your father
paid good money for those.
BARRY:
Sorry. I'm excited.
MARTIN:
Here's the graduate.
We're very proud of you, son.
 :
A perfect report card, all B's.
JANET:
Very proud.
(Rubs Barry's hair)
BARRY=
Ma! I got a thing going here.
JANET:
- You got lint on your fuzz.
BARRY:
- Ow! That's me!

JANET:
- Wave to us! We'll be in row 118,000.
- Bye!
(Barry flies out the door)
JANET:
Barry, I told you,
stop flying in the house!
(Barry drives through the hive,and is waved at by Adam who is reading a
newspaper)
BARRY==
- Hey, Adam.
ADAM:
- Hey, Barry.
(Adam gets in Barry's car)
 :
- Is that fuzz gel?
BARRY:
- A little. Special day, graduation.
ADAM:
Never thought I'd make it.
(Barry pulls away from the house and continues driving)
BARRY:
Three days grade school,
three days high school...
ADAM:
Those were awkward.
BARRY:
Three days college. I'm glad I took
a day and hitchhiked around the hive.
ADAM==
You did come back different.
(Barry and Adam pass by Artie, who is jogging)
ARTIE:
- Hi, Barry!

BARRY:
- Artie, growing a mustache? Looks good.
ADAM:
- Hear about Frankie?
BARRY:
- Yeah.
ADAM==
- You going to the funeral?
BARRY:
- No, I'm not going to his funeral.
 :
Everybody knows,
sting someone, you die.
 :
Don't waste it on a squirrel.
Such a hothead.
ADAM:
I guess he could have
just gotten out of the way.
(The car does a barrel roll on the loop-shaped bridge and lands on the
highway)
 :
I love this incorporating
an amusement park into our regular day.
BARRY:
I guess that's why they say we don't need vacations.
(Barry parallel parks the car and together they fly over the graduating
students)
Boy, quite a bit of pomp...
under the circumstances.
(Barry and Adam sit down and put on their hats)
 :
- Well, Adam, today we are men.

ADAM:
- We are!
BARRY=
- Bee-men.
=ADAM=
- Amen!
BARRY AND ADAM:
Hallelujah!
(Barry and Adam both have a happy spasm)
ANNOUNCER:
Students, faculty, distinguished bees,
 :
please welcome Dean Buzzwell.
DEAN BUZZWELL:
Welcome, New Hive Oity
graduating class of...
 :
...9:
 :
That concludes our ceremonies.
 :
And begins your career
at Honex Industries!
ADAM:
Will we pick our job today?
(Adam and Barry get into a tour bus)
BARRY=
I heard it's just orientation.
(Tour buses rise out of the ground and the students are automatically
loaded into the buses)
TOUR GUIDE:
Heads up! Here we go.

ANNOUNCER:
Keep your hands and antennas
inside the tram at all times.
BARRY:
- Wonder what it'll be like?
ADAM:
- A little scary.
TOUR GUIDE==
Welcome to Honex,
a division of Honesco
 :
and a part of the Hexagon Group.
Barry:
This is it!
BARRY AND ADAM:
Wow.
BARRY:
Wow.
(The bus drives down a road an on either side are the Bee's massive
complicated Honey-making machines)
TOUR GUIDE:
We know that you, as a bee,
have worked your whole life
 :
to get to the point where you
can work for your whole life.
 :
Honey begins when our valiant Pollen
Jocks bring the nectar to the hive.
 :
Our top-secret formula
 :
is automatically color-corrected,

scent-adjusted and bubble-contoured
 :
into this soothing sweet syrup
 :
with its distinctive
golden glow you know as...
EVERYONE ON BUS:
Honey!
(The guide has been collecting honey into a bottle and she throws it into
the crowd on the bus and it is caught by a girl in the back)
ADAM:
- That girl was hot.
BARRY:
- She's my cousin!
ADAM==
- She is?
BARRY:
- Yes, we're all cousins.
ADAM:
- Right. You're right.
TOUR GUIDE:
- At Honex, we constantly strive
 :
to improve every aspect
of bee existence.
 :
These bees are stress-testing
a new helmet technology.
(The bus passes by a Bee wearing a helmet who is being smashed into the
ground with fly-swatters, newspapers and boots. He lifts a thumbs up but
you can hear him groan)
 :
ADAM==

- What do you think he makes?
BARRY:
- Not enough.
TOUR GUIDE:
Here we have our latest advancement,
the Krelman.
(They pass by a turning wheel with Bees standing on pegs, who are each
wearing a finger-shaped hat)
Barry:
- Wow, What does that do?
TOUR GUIDE:
- Catches that little strand of honey
 :
that hangs after you pour it.
Saves us millions.
ADAM:
(Intrigued)
Can anyone work on the Krelman?
TOUR GUIDE:
Of course. Most bee jobs are
small ones.
But bees know that every small job,
if it's done well, means a lot.
 :
But choose carefully
 :
because you'll stay in the job
you pick for the rest of your life.
(Everyone claps except for Barry)
BARRY:
The same job the rest of your life?
I didn't know that.
ADAM:

What's the difference?
TOUR GUIDE:
You'll be happy to know that bees,
as a species, haven't had one day off
 :
in 27 million years.
BARRY:
(Upset)
So you'll just work us to death?
 :
We'll sure try.
(Everyone on the bus laughs except Barry. Barry and Adam are walking back
home together)
ADAM:
Wow! That blew my mind!
BARRY:
"What's the difference?"
How can you say that?
 :
One job forever?
That's an insane choice to have to make.
ADAM:
I'm relieved. Now we only have
to make one decision in life.
BARRY:
But, Adam, how could they
never have told us that?
ADAM:
Why would you question anything?
We're bees.
 :
We're the most perfectly
functioning society on Earth.

BARRY:
You ever think maybe things
work a little too well here?
ADAM:
Like what? Give me one example.
(Barry and Adam stop walking and it is revealed to the audience that
hundreds of cars are speeding by and narrowly missing them in perfect
unison)
BARRY:
I don't know. But you know
what I'm talking about.
ANNOUNCER:
Please clear the gate.
Royal Nectar Force on approach.
BARRY:
Wait a second. Check it out.
(The Pollen jocks fly in, circle around and landing in line)
 :
- Hey, those are Pollen Jocks!
ADAM:
- Wow.
 :
I've never seen them this close.
BARRY:
They know what it's like
outside the hive.
ADAM:
Yeah, but some don't come back.
GIRL BEES:
- Hey, Jocks!
- Hi, Jocks!
(The Pollen Jocks hook up their backpacks to machines that pump the nectar
to trucks, which drive away)

LOU LO DUVA:
You guys did great!
 :
You're monsters!
You're sky freaks!
I love it!
(Punching the Pollen Jocks in joy)
I love it!
ADAM:
- I wonder where they were.
BARRY:
- I don't know.
 :
Their day's not planned.
 :
Outside the hive, flying who knows
where, doing who knows what.
 :
You can't just decide to be a Pollen
Jock. You have to be bred for that.
ADAM==
Right.
(Barry and Adam are covered in some pollen that floated off of the Pollen
Jocks)
BARRY:
Look at that. That's more pollen
than you and I will see in a lifetime.
ADAM:
It's just a status symbol.
Bees make too much of it.
BARRY:
Perhaps. Unless you're wearing it
and the ladies see you wearing it.
(Barry waves at 2 girls standing a little away from them)

ADAM==
Those ladies?
Aren't they our cousins too?
BARRY:
Distant. Distant.
POLLEN JOCK #1:
Look at these two.
POLLEN JOCK #2:
- Couple of Hive Harrys.
POLLEN JOCK #1:
- Let's have fun with them.
GIRL BEE #1:
It must be dangerous
being a Pollen Jock.
BARRY:
Yeah. Once a bear pinned me
against a mushroom!
 :
He had a paw on my throat,
and with the other, he was slapping me!
(Slaps Adam with his hand to represent his scenario)
GIRL BEE #2:
- Oh, my!
BARRY:
- I never thought I'd knock him out.
GIRL BEE #1:
(Looking at Adam)
What were you doing during this?
ADAM:
Obviously I was trying to alert the authorities.
BARRY:
I can autograph that.

(The pollen jocks walk up to Barry and Adam, they pretend that Barry and
Adam really are pollen jocks.)
POLLEN JOCK #1:
A little gusty out there today,
wasn't it, comrades?
BARRY:
Yeah. Gusty.
POLLEN JOCK #1:
We're hitting a sunflower patch
six miles from here tomorrow.
BARRY:
- Six miles, huh?
ADAM:
- Barry!
POLLEN JOCK #2:
A puddle jump for us,
but maybe you're not up for it.
BARRY:
- Maybe I am.
ADAM:
- You are not!
POLLEN JOCK #1:
We're going 0900 at J-Gate.
 :
What do you think, buzzy-boy?
Are you bee enough?
BARRY:
I might be. It all depends
on what 0900 means.
(The scene cuts to Barry looking out on the hive-city from his balcony at
night)
MARTIN:

Hey, Honex!
BARRY:
Dad, you surprised me.
MARTIN:
You decide what you're interested in?
BARRY:
- Well, there's a lot of choices.
- But you only get one.
 :
Do you ever get bored
doing the same job every day?
MARTIN:
Son, let me tell you about stirring.
 :
You grab that stick, and you just
move it around, and you stir it around.
 :
You get yourself into a rhythm.
It's a beautiful thing.
BARRY:
You know, Dad,
the more I think about it,
 :
maybe the honey field
just isn't right for me.
MARTIN:
You were thinking of what,
making balloon animals?
 :
That's a bad job
for a guy with a stinger.
 :

Janet, your son's not sure
he wants to go into honey!
JANET:
- Barry, you are so funny sometimes.
BARRY:
- I'm not trying to be funny.
MARTIN:
You're not funny! You're going
into honey. Our son, the stirrer!
JANET:
- You're gonna be a stirrer?
BARRY:
- No one's listening to me!
MARTIN:
Wait till you see the sticks I have.
BARRY:
I could say anything right now.
I'm gonna get an ant tattoo!
(Barry's parents don't listen to him and continue to ramble on)
MARTIN:
Let's open some honey and celebrate!
BARRY:
Maybe I'll pierce my thorax.
Shave my antennae.
 :
Shack up with a grasshopper. Get
a gold tooth and call everybody "dawg"!
JANET:
I'm so proud.
(The scene cuts to Barry and Adam waiting in line to get a job)
ADAM:
- We're starting work today!

BARRY:
- Today's the day.
ADAM:
Come on! All the good jobs
will be gone.
BARRY:
Yeah, right.
JOB LISTER:
Pollen counting, stunt bee, pouring,
stirrer, front desk, hair removal...
BEE IN FRONT OF LINE:
- Is it still available?
JOB LISTER:
- Hang on. Two left!
 :
One of them's yours! Congratulations!
Step to the side.
ADAM:
- What'd you get?
BEE IN FRONT OF LINE:
- Picking crud out. Stellar!
(He walks away)
ADAM:
Wow!
JOB LISTER:
Couple of newbies?
ADAM:
Yes, sir! Our first day! We are ready!
JOB LISTER:
Make your choice.
(Adam and Barry look up at the job board. There are hundreds of constantly
changing panels that contain available or unavailable jobs. It looks very
confusing)

ADAM:
- You want to go first?
BARRY:
- No, you go.
ADAM:
Oh, my. What's available?
JOB LISTER:
Restroom attendant's open,
not for the reason you think.
ADAM:
- Any chance of getting the Krelman?
JOB LISTER:
- Sure, you're on.
(Puts the Krelman finger-hat on Adam's head)
(Suddenly the sign for Krelman closes out)
 :
I'm sorry, the Krelman just closed out.
(Takes Adam's hat off)
Wax monkey's always open.
ADAM:
The Krelman opened up again.
 :
What happened?
JOB LISTER:
A bee died. Makes an opening. See?
He's dead. Another dead one.
 :
Deady. Deadified. Two more dead.
 :
Dead from the neck up.
Dead from the neck down. That's life!

ADAM:
Oh, this is so hard!
(Barry remembers what the Pollen Jock offered him and he flies off)
Heating, cooling,
stunt bee, pourer, stirrer,
 :
humming, inspector number seven,
lint coordinator, stripe supervisor,
 :
mite wrangler. Barry, what
do you think I should... Barry?
(Adam turns around and sees Barry flying away)
 :
Barry!
POLLEN JOCK:
All right, we've got the sunflower patch
in quadrant nine...
ADAM:
(Through phone)
What happened to you?
Where are you?
BARRY:
- I'm going out.
ADAM:
- Out? Out where?
BARRY:
- Out there.
ADAM:
- Oh, no!
BARRY:
I have to, before I go
to work for the rest of my life.
ADAM:

You're gonna die! You're crazy!
(Barry hangs up)
Hello?
POLLEN JOCK #2:
Another call coming in.
 :
If anyone's feeling brave,
there's a Korean deli on 83rd
 :
that gets their roses today.
BARRY:
Hey, guys.
POLLEN JOCK #1 ==
- Look at that.
POLLEN JOCK #2:
- Isn't that the kid we saw yesterday?
LOU LO DUVA:
Hold it, son, flight deck's restricted.
POLLEN JOCK #1:
It's OK, Lou. We're gonna take him up.
(Puts hand on Barry's shoulder)
LOU LO DUVA:
(To Barry) Really? Feeling lucky, are you?
BEE WITH CLIPBOARD:
(To Barry) Sign here, here. Just initial that.
 :
- Thank you.
LOU LO DUVA:
- OK.
 :
You got a rain advisory today,
 :

and as you all know,
bees cannot fly in rain.
 :
So be careful. As always,
watch your brooms,
 :
hockey sticks, dogs,
birds, bears and bats.
 :
Also, I got a couple of reports
of root beer being poured on us.
 :
Murphy's in a home because of it,
babbling like a cicada!
BARRY:
- That's awful.
LOU LO DUVA:
(Still talking through megaphone)
- And a reminder for you rookies,
 :
bee law number one,
absolutely no talking to humans!
 :
All right, launch positions!
POLLEN JOCKS:
(The Pollen Jocks run into formation)
 :
Buzz, buzz, buzz, buzz! Buzz, buzz,
buzz, buzz! Buzz, buzz, buzz, buzz!
LOU LU DUVA:
Black and yellow!
POLLEN JOCKS:

Hello!
POLLEN JOCK #1:
(To Barry)You ready for this, hot shot?
BARRY:
Yeah. Yeah, bring it on.
POLLEN JOCK's:
Wind, check.
 :
- Antennae, check.
- Nectar pack, check.
 :
- Wings, check.
- Stinger, check.
BARRY:
Scared out of my shorts, check.
LOU LO DUVA:
OK, ladies,
 :
let's move it out!
 :
Pound those petunias,
you striped stem-suckers!
 :
All of you, drain those flowers!
(The pollen jocks fly out of the hive)
BARRY:
Wow! I'm out!
 :
I can't believe I'm out!
 :
So blue.

 :
I feel so fast and free!
 :
Box kite!
(Barry flies through the kite)
 :
Wow!
 :
Flowers!
(A pollen jock puts on some high tech goggles that shows flowers similar to
heat sink goggles.)
POLLEN JOCK:
This is Blue Leader.
We have roses visual.
 :
Bring it around 30 degrees and hold.
 :
Roses!
POLLEN JOCK #1:
30 degrees, roger. Bringing it around.
 :
Stand to the side, kid.
It's got a bit of a kick.
(The pollen jock fires a high-tech gun at the flower, shooting tubes that
suck up the nectar from the flower and collects it into a pouch on the gun)
BARRY:
That is one nectar collector!
POLLEN JOCK #1==
- Ever see pollination up close?
BARRY:
- No, sir.
POLLEN JOCK #1:

(Barry and the Pollen jock fly over the field, the pollen jock sprinkles
pollen as he goes)
 :
I pick up some pollen here, sprinkle it
over here. Maybe a dash over there,
 :
a pinch on that one.
See that? It's a little bit of magic.
BARRY:
That's amazing. Why do we do that?
POLLEN JOCK #1:
That's pollen power. More pollen, more
flowers, more nectar, more honey for us.
BARRY:
Cool.
POLLEN JOCK #1:
I'm picking up a lot of bright yellow.
could be daisies. Don't we need those?
POLLEN JOCK #2:
Copy that visual.
 :
Wait. One of these flowers
seems to be on the move.
POLLEN JOCK #1:
Say again? You're reporting
a moving flower?
POLLEN JOCK #2:
Affirmative.
(The Pollen jocks land near the "flowers" which, to the audience are
obviously just tennis balls)
KEN:
(In the distance) That was on the line!

POLLEN JOCK #1:
This is the coolest. What is it?
POLLEN JOCK #2:
I don't know, but I'm loving this color.
 :
It smells good.
Not like a flower, but I like it.
POLLEN JOCK #1:
Yeah, fuzzy.
(Sticks his hand on the ball but it gets stuck)
POLLEN JOCK #3==
Chemical-y.
(The pollen jock finally gets his hand free from the tennis ball)
POLLEN JOCK #1:
Careful, guys. It's a little grabby.
(The pollen jocks turn around and see Barry lying his entire body on top of
one of the tennis balls)
POLLEN JOCK #2:
My sweet lord of bees!
POLLEN JOCK #3:
Candy-brain, get off there!
POLLEN JOCK #1:
(Pointing upwards)
Problem!
(A human hand reaches down and grabs the tennis ball that Barry is stuck
to)
BARRY:
- Guys!
POLLEN JOCK #2:
- This could be bad.
POLLEN JOCK #3:
Affirmative.
(Vanessa Bloome starts bouncing the tennis ball, not knowing Barry is stick
to it)

BARRY==
Very close.
 :
Gonna hurt.
 :
Mama's little boy.
(Barry is being hit back and forth by two humans playing tennis. He is
still stuck to the ball)
POLLEN JOCK #1:
You are way out of position, rookie!
KEN:
Coming in at you like a MISSILE!
(Barry flies past the pollen jocks, still stuck to the ball)
BARRY:
(In slow motion)
Help me!
POLLEN JOCK #2:
I don't think these are flowers.
POLLEN JOCK #3:
- Should we tell him?
POLLEN JOCK #1:
- I think he knows.
BARRY:
What is this?!
KEN:
Match point!
 :
You can start packing up, honey,
because you're about to EAT IT!
(A pollen jock coughs which confused Ken and he hits the ball the wrong way
with Barry stuck to it and it goes flying into the city)
BARRY:

Yowser!
(Barry bounces around town and gets stuck in the engine of a car. He flies
into the air conditioner and sees a bug that was frozen in there)
BARRY:
Ew, gross.
(The man driving the car turns on the air conditioner which blows Barry
into the car)
GIRL IN CAR:
There's a bee in the car!
 :
- Do something!
DAD DRIVING CAR:
- I'm driving!
BABY GIRL:
(Waving at Barry)
- Hi, bee.
(Barry smiles and waves at the baby girl)
GUY IN BACK OF CAR:
- He's back here!
 :
He's going to sting me!
GIRL IN CAR:
Nobody move. If you don't move,
he won't sting you. Freeze!
(Barry freezes as well, hovering in the middle of the car)
 :
GRANDMA IN CAR==
He blinked!
(The grandma whips out some bee-spray and sprays everywhere in the car,
climbing into the front seat, still trying to spray Barry)
GIRL IN CAR:
Spray him, Granny!
DAD DRIVING THE CAR:
What are you doing?!
(Barry escapes the car through the air conditioner and is flying high above

the ground, safe.)
BARRY:
Wow... the tension level
out here is unbelievable.
(Barry sees that storm clouds are gathering and he can see rain clouds
moving into this direction)
 :
I gotta get home.
 :
Can't fly in rain.
 :
Can't fly in rain.
(A rain drop hits Barry and one of his wings is damaged)
 :
Can't fly in rain.
(A second rain drop hits Barry again and he spirals downwards)
Mayday! Mayday! Bee going down!
(WW2 plane sound effects are played as he plummets, and he crash-lands on a
plant inside an apartment near the window)
VANESSA BLOOME:
Ken, could you close
the window please?
KEN==
Hey, check out my new resume.
I made it into a fold-out brochure.
 :
You see?
(Folds brochure resume out)
Folds out.
(Ken closes the window, trapping Barry inside)
BARRY:
Oh, no. More humans. I don't need this.
(Barry tries to fly away but smashes into the window and falls again)
 :
What was that?

(Barry keeps trying to fly out the window but he keeps being knocked back
because the window is closed)
Maybe this time. This time. This time.
This time! This time! This...
 :
Drapes!
(Barry taps the glass. He doesn't understand what it is)
That is diabolical.
KEN:
It's fantastic. It's got all my special
skills, even my top-ten favorite movies.
ANDY:
What's number one? Star Wars?
KEN:
Nah, I don't go for that...
(Ken makes finger guns and makes "pew pew pew" sounds and then stops)
 :
...kind of stuff.
BARRY:
No wonder we shouldn't talk to them.
They're out of their minds.
KEN:
When I leave a job interview, they're
flabbergasted, can't believe what I say.
BARRY:
(Looking at the light on the ceiling)
There's the sun. Maybe that's a way out.
(Starts flying towards the lightbulb)
 :
I don't remember the sun
having a big 75 on it.
(Barry hits the lightbulb and falls into the dip on the table that the
humans are sitting at)
KEN:

I predicted global warming.
 :
I could feel it getting hotter.
At first I thought it was just me.
(Andy dips a chip into the bowl and scoops up some dip with Barry on it and
is about to put it in his mouth)
 :
Wait! Stop! Bee!
(Andy drops the chip with Barry in fear and backs away. All the humans
freak out)
 :
Stand back. These are winter boots.
(Ken has winter boots on his hands and he is about to smash the bee but
Vanessa saves him last second)
VANESSA:
Wait!
 :
Don't kill him!
(Vanessa puts Barry in a glass to protect him)
KEN:
You know I'm allergic to them!
This thing could kill me!
VANESSA:
Why does his life have
less value than yours?
KEN:
Why does his life have any less value
than mine? Is that your statement?
VANESSA:
I'm just saying all life has value. You
don't know what he's capable of feeling.
(Vanessa picks up Ken's brochure and puts it under the glass so she can
carry Barry back to the window. Barry looks at Vanessa in amazement)
KEN:

My brochure!
VANESSA:
There you go, little guy.
(Vanessa opens the window and lets Barry out but Barry stays back and is
still shocked that a human saved his life)
KEN:
I'm not scared of him.
It's an allergic thing.
VANESSA:
Put that on your resume brochure.
KEN:
My whole face could puff up.
ANDY:
Make it one of your special skills.
KEN:
Knocking someone out
is also a special skill.
(Ken walks to the door)
Right. Bye, Vanessa. Thanks.
 :
- Vanessa, next week? Yogurt night?
VANESSA:
- Sure, Ken. You know, whatever.
 :
(Vanessa tries to close door)
KEN==
- You could put carob chips on there.
VANESSA:
- Bye.
(Closes door but Ken opens it again)
KEN:
- Supposed to be less calories.

VANESSA:
- Bye.
(Closes door)
(Fast forward to the next day, Barry is still inside the house. He flies
into the kitchen where Vanessa is doing dishes)
BARRY==
(Talking to himself)
I gotta say something.
 :
She saved my life.
I gotta say something.
 :
All right, here it goes.
(Turns back)
Nah.
 :
What would I say?
 :
I could really get in trouble.
 :
It's a bee law.
You're not supposed to talk to a human.
 :
I can't believe I'm doing this.
 :
I've got to.
(Barry disguises himself as a character on a food can as Vanessa walks by
again)
 :
Oh, I can't do it. Come on!
 :
No. Yes. No.
 :
Do it. I can't.

 :
How should I start it?
(Barry strikes a pose and wiggles his eyebrows)
"You like jazz?"
No, that's no good.
(Vanessa is about to walk past Barry)
Here she comes! Speak, you fool!
 :
...Hi!
(Vanessa gasps and drops the dishes in fright and notices Barry on the
counter)
 :
I'm sorry.
VANESSA:
- You're talking.
BARRY:
- Yes, I know.
VANESSA:
(Pointing at Barry)
You're talking!
BARRY:
I'm so sorry.
VANESSA:
No, it's OK. It's fine.
I know I'm dreaming.
 :
But I don't recall going to bed.
BARRY:
Well, I'm sure this
is very disconcerting.
VANESSA:
This is a bit of a surprise to me.
I mean, you're a bee!

BARRY:
I am. And I'm not supposed
to be doing this,
(Pointing to the living room where Ken tried to kill him last night)
but they were all trying to kill me.
 :
And if it wasn't for you...
 :
I had to thank you.
It's just how I was raised.
(Vanessa stabs her hand with a fork to test whether she's dreaming or not)
 :
That was a little weird.
VANESSA:
- I'm talking with a bee.
BARRY:
- Yeah.
VANESSA:
I'm talking to a bee.
And the bee is talking to me!
BARRY:
I just want to say I'm grateful.
I'll leave now.
(Barry turns to leave)
VANESSA:
- Wait! How did you learn to do that?
BARRY:
(Flying back)
- What?
VANESSA:
The talking...thing.
BARRY:

Same way you did, I guess.
"Mama, Dada, honey." You pick it up.
VANESSA:
- That's very funny.
BARRY:
- Yeah.
 :
Bees are funny. If we didn't laugh,
we'd cry with what we have to deal with.
 :
Anyway...
VANESSA:
Can I...
 :
...get you something?
BARRY:
- Like what?
VANESSA:
I don't know. I mean...
I don't know. Coffee?
BARRY:
I don't want to put you out.
VANESSA:
It's no trouble. It takes two minutes.
 :
- It's just coffee.
BARRY:
- I hate to impose.
(Vanessa starts making coffee)
VANESSA:
- Don't be ridiculous!

BARRY:
- Actually, I would love a cup.
VANESSA:
Hey, you want rum cake?
BARRY:
- I shouldn't.
VANESSA:
- Have some.
BARRY:
- No, I can't.
VANESSA:
- Come on!
BARRY:
I'm trying to lose a couple micrograms.
VANESSA:
- Where?
BARRY:
- These stripes don't help.
VANESSA:
You look great!
BARRY:
I don't know if you know
anything about fashion.
 :
Are you all right?
VANESSA:
(Pouring coffee on the floor and missing the cup completely)
No.
(Flash forward in time. Barry and Vanessa are sitting together at a table
on top of the apartment building drinking coffee)

 :
BARRY==
He's making the tie in the cab
as they're flying up Madison.
 :
He finally gets there.
 :
He runs up the steps into the church.
The wedding is on.
 :
And he says, "Watermelon?
I thought you said Guatemalan.
 :
Why would I marry a watermelon?"
(Barry laughs but Vanessa looks confused)
VANESSA:
Is that a bee joke?
BARRY:
That's the kind of stuff we do.
VANESSA:
Yeah, different.
 :
So, what are you gonna do, Barry?
(Barry stands on top of a sugar cube floating in his coffee and paddles it
around with a straw like it's a gondola)
BARRY:
About work? I don't know.
 :
I want to do my part for the hive,
but I can't do it the way they want.
VANESSA:
I know how you feel.

BARRY:
- You do?
VANESSA:
- Sure.
 :
My parents wanted me to be a lawyer or
a doctor, but I wanted to be a florist.
BARRY:
- Really?
VANESSA:
- My only interest is flowers.
BARRY:
Our new queen was just elected
with that same campaign slogan.
 :
Anyway, if you look...
(Barry points to a tree in the middle of Central Park)
 :
There's my hive right there. See it?
VANESSA:
You're in Sheep Meadow!
BARRY:
Yes! I'm right off the Turtle Pond!
VANESSA:
No way! I know that area.
I lost a toe ring there once.
BARRY:
- Why do girls put rings on their toes?
VANESSA:
- Why not?
BARRY:

- It's like putting a hat on your knee.
VANESSA:
- Maybe I'll try that.
(A custodian installing a lightbulb looks over at them but to his
perspective it looks like Vanessa is talking to a cup of coffee on the
table)
CUSTODIAN:
- You all right, ma'am?
VANESSA:
- Oh, yeah. Fine.
 :
Just having two cups of coffee!
BARRY:
Anyway, this has been great.
Thanks for the coffee.
VANESSA==
Yeah, it's no trouble.
BARRY:
Sorry I couldn't finish it. If I did,
I'd be up the rest of my life.
(Barry points towards the rum cake)
 :
Can I take a piece of this with me?
VANESSA:
Sure! Here, have a crumb.
(Vanessa hands Barry a crumb but it is still pretty big for Barry)
BARRY:
- Thanks!
VANESSA:
- Yeah.
BARRY:
All right. Well, then...
I guess I'll see you around.

 :
Or not.
VANESSA:
OK, Barry...
BARRY:
And thank you
so much again... for before.
VANESSA:
Oh, that? That was nothing.
BARRY:
Well, not nothing, but... Anyway...
(Vanessa and Barry hold hands, but Vanessa has to hold out a finger because
her hands is to big and Barry holds that)
(The custodian looks over again and it appears Vanessa is laughing at her
coffee again. The lightbulb that he was screwing in sparks and he falls off
the ladder)
(Fast forward in time and we see two Bee Scientists testing out a parachute
in a Honex wind tunnel)
BEE SCIENTIST #1:
This can't possibly work.
BEE SCIENTIST #2:
He's all set to go.
We may as well try it.
 :
OK, Dave, pull the chute.
(Dave pulls the chute and the wind slams him against the wall and he falls
on his face.The camera pans over and we see Barry and Adam walking
together)
ADAM:
- Sounds amazing.
BARRY:
- It was amazing!
 :
It was the scariest,
happiest moment of my life.

ADAM:
Humans! I can't believe
you were with humans!
 :
Giant, scary humans!
What were they like?
BARRY:
Huge and crazy. They talk crazy.
 :
They eat crazy giant things.
They drive crazy.
ADAM:
- Do they try and kill you, like on TV?
BARRY:
- Some of them. But some of them don't.
ADAM:
- How'd you get back?
BARRY:
- Poodle.
ADAM:
You did it, and I'm glad. You saw
whatever you wanted to see.
 :
You had your "experience." Now you
can pick out your job and be normal.
BARRY:
- Well...
ADAM:
- Well?
BARRY:
Well, I met someone.

ADAM:
You did? Was she Bee-ish?
 :
- A wasp?! Your parents will kill you!
BARRY:
- No, no, no, not a wasp.
ADAM:
- Spider?
BARRY:
- I'm not attracted to spiders.
 :
I know, for everyone else, it's the hottest thing,
with the eight legs and all.
 :
I can't get by that face.
ADAM:
So who is she?
BARRY:
She's... human.
ADAM:
No, no. That's a bee law.
You wouldn't break a bee law.
BARRY:
- Her name's Vanessa.
(Adam puts his head in his hands)
ADAM:
- Oh, boy.
BARRY==
She's so nice. And she's a florist!
ADAM:
Oh, no! You're dating a human florist!

BARRY:
We're not dating.
ADAM:
You're flying outside the hive, talking
to humans that attack our homes
 :
with power washers and M-80s!
That's one-eighth a stick of dynamite!
BARRY:
She saved my life!
And she understands me.
ADAM:
This is over!
BARRY:
Eat this.
(Barry gives Adam a piece of the crumb that he got from Vanessa. Adam eats
it)
ADAM:
(Adam's tone changes)
This is not over! What was that?
BARRY:
- They call it a crumb.
ADAM:
- It was so stingin' stripey!
BARRY:
And that's not what they eat.
That's what falls off what they eat!
 :
- You know what a Cinnabon is?
ADAM:
- No.
(Adam opens a door behind him and he pulls Barry in)

BARRY:
It's bread and cinnamon and frosting.
ADAM:
Be quiet!
BARRY:
They heat it up...
ADAM:
Sit down!
(Adam forces Barry to sit down)
BARRY:
(Still rambling about Cinnabons)
...really hot!
(Adam grabs Barry by the shoulders)
ADAM:
- Listen to me!
 :
We are not them! We're us.
There's us and there's them!
BARRY==
Yes, but who can deny
the heart that is yearning?
ADAM:
There's no yearning.
Stop yearning. Listen to me!
 :
You have got to start thinking bee,
my friend. Thinking bee!
BARRY:
- Thinking bee.
WORKER BEE:
- Thinking bee.
WORKER BEES AND ADAM:
Thinking bee! Thinking bee!

Thinking bee! Thinking bee!
(Flash forward in time; Barry is laying on a raft in a pool full of honey.
He is wearing sunglasses)
JANET:
There he is. He's in the pool.
MARTIN:
You know what your problem is, Barry?
(Barry pulls down his sunglasses and he looks annoyed)
BARRY:
(Sarcastic)
I gotta start thinking bee?
JANET:
How much longer will this go on?
MARTIN:
It's been three days!
Why aren't you working?
(Puts sunglasses back on)
BARRY:
I've got a lot of big life decisions
to think about.
MARTIN:
What life? You have no life!
You have no job. You're barely a bee!
JANET:
Would it kill you
to make a little honey?
(Barry rolls off the raft and sinks into the honey pool)
 :
Barry, come out.
Your father's talking to you.
 :
Martin, would you talk to him?
MARTIN:

Barry, I'm talking to you!
(Barry keeps sinking into the honey until he is suddenly in Central Park
having a picnic with Vanessa)
(Barry has a cup of honey and he clinks his glass with Vanessas. Suddenly a
mosquito lands on Vanessa and she slaps it, killing it. They both gasp but
then burst out laughing)
VANESSA:
You coming?
(The camera pans over and Vanessa is climbing into a small yellow airplane)
BARRY:
Got everything?
VANESSA:
All set!
BARRY:
Go ahead. I'll catch up.
(Vanessa lifts off and flies ahead)
VANESSA:
Don't be too long.
(Barry catches up with Vanessa and he sticks out his arms like ana irplane.
He rolls from side to side, and Vanessa copies him with the airplane)
VANESSA:
Watch this!
(Barry stays back and watches as Vanessa draws a heart in the air using
pink smoke from the plane, but on the last loop-the-loop she suddenly
crashes into a mountain and the plane explodes. The destroyed plane falls
into some rocks and explodes a second time)
BARRY:
Vanessa!
(As Barry is yelling his mouth fills with honey and he wakes up,
discovering that he was just day dreaming. He slowly sinks back into the
honey pool)
MARTIN:
- We're still here.

JANET:
- I told you not to yell at him.
 :
He doesn't respond to yelling!
MARTIN:
- Then why yell at me?
JANET:
- Because you don't listen!
MARTIN:
I'm not listening to this.
BARRY:
Sorry, I've gotta go.
MARTIN:
- Where are you going?
BARRY:
- I'm meeting a friend.
JANET:
A girl? Is this why you can't decide?
BARRY:
Bye.
(Barry flies out the door and Martin shakes his head)
 :
JANET==
I just hope she's Bee-ish.
(Fast forward in time and Barry is sitting on Vanessa's shoulder and she is
closing up her shop)
BARRY:
They have a huge parade
of flowers every year in Pasadena?
VANESSA:
To be in the Tournament of Roses,
that's every florist's dream!

 :
Up on a float, surrounded
by flowers, crowds cheering.
BARRY:
A tournament. Do the roses
compete in athletic events?
VANESSA:
No. All right, I've got one.
How come you don't fly everywhere?
BARRY:
It's exhausting. Why don't you
run everywhere? It's faster.
VANESSA:
Yeah, OK, I see, I see.
All right, your turn.
BARRY:
TiVo. You can just freeze live TV?
That's insane!
VANESSA:
You don't have that?
BARRY:
We have Hivo, but it's a disease.
It's a horrible, horrible disease.
VANESSA:
Oh, my.
(A human walks by and Barry narrowly avoids him)
PASSERBY:
Dumb bees!
VANESSA:
You must want to sting all those jerks.
BARRY:
We try not to sting.

It's usually fatal for us.
VANESSA:
So you have to watch your temper
(They walk into a store)
BARRY:
Very carefully.
You kick a wall, take a walk,
 :
write an angry letter and throw it out.
Work through it like any emotion:
 :
Anger, jealousy, lust.
(Suddenly an employee(Hector) hits Barry off of Vanessa's shoulder. Hector
thinks he's saving Vanessa)
VANESSA:
(To Barry)
Oh, my goodness! Are you OK?
(Barry is getting up off the floor)
BARRY:
Yeah.
VANESSA:
(To Hector)
- What is wrong with you?!
HECTOR:
(Confused)
- It's a bug.
VANESSA:
He's not bothering anybody.
Get out of here, you creep!
(Vanessa hits Hector across the face with the magazine he had and then hits
him in the head. Hector backs away covering his head)
Barry:
What was that? A Pic 'N' Save circular?
(Vanessa sets Barry back on her shoulder)

VANESSA:
Yeah, it was. How did you know?
BARRY:
It felt like about 10 pages.
Seventy-five is pretty much our limit.
VANESSA:
You've really got that
down to a science.
BARRY:
- Oh, we have to. I lost a cousin to Italian Vogue.
VANESSA:
- I'll bet.
(Barry looks to his right and notices there is honey for sale in the aisle)
BARRY:
What in the name
of Mighty Hercules is this?
(Barry looks at all the brands of honey, shocked)
How did this get here?
Cute Bee, Golden Blossom,
 :
Ray Liotta Private Select?
(Barry puts his hands up and slowly turns around, a look of disgust on his
face)
VANESSA:
- Is he that actor?
BARRY:
- I never heard of him.
 :
- Why is this here?
VANESSA:
- For people. We eat it.
BARRY:

You don't have
enough food of your own?!
(Hector looks back and notices that Vanessa is talking to Barry)
VANESSA:
- Well, yes.
BARRY:
- How do you get it?
VANESSA:
- Bees make it.
BARRY:
- I know who makes it!
 :
And it's hard to make it!
 :
There's heating, cooling, stirring.
You need a whole Krelman thing!
VANESSA:
- It's organic.
BARRY:
- It's our-ganic!
VANESSA:
It's just honey, Barry.
BARRY:
Just what?!
 :
Bees don't know about this!
This is stealing! A lot of stealing!
 :
You've taken our homes, schools,
hospitals! This is all we have!
 :

And it's on sale?!
I'm getting to the bottom of this.
 :
I'm getting to the bottom
of all of this!
(Flash forward in time; Barry paints his face with black strikes like a
soldier and sneaks into the storage section of the store)
(Two men, including Hector, are loading boxes into some trucks)
 :
SUPERMARKET EMPLOYEE==
Hey, Hector.
 :
- You almost done?
HECTOR:
- Almost.
(Barry takes a step to peak around the corner)
(Whispering)
He is here. I sense it.
 :
Well, I guess I'll go home now
(Hector pretends to walk away by walking in place and speaking loudly)
 :
and just leave this nice honey out,
with no one around.
BARRY:
You're busted, box boy!
HECTOR:
I knew I heard something!
So you can talk!
BARRY:
I can talk.
And now you'll start talking!
 :
Where you getting the sweet stuff?

Who's your supplier?
HECTOR:
I don't understand.
I thought we were friends.
 :
The last thing we want
to do is upset bees!
(Hector takes a thumbtack out of the board behind him and sword-fights
Barry. Barry is using his stinger like a sword)
 :
You're too late! It's ours now!
BARRY:
You, sir, have crossed
the wrong sword!
HECTOR:
You, sir, will be lunch
for my iguana, Ignacio!
(Barry hits the thumbtack out of Hectors hand and Hector surrenders)
Barry:
Where is the honey coming from?
 :
Tell me where!
HECTOR:
(Pointing to leaving truck)
Honey Farms! It comes from Honey Farms!
(Barry chases after the truck but it is getting away. He flies onto a
bicyclists' backpack and he catches up to the truck)
CAR DRIVER:
(To bicyclist)
Crazy person!
(Barry flies off and lands on the windshield of the Honey farms truck.
Barry looks around and sees dead bugs splattered everywhere)
BARRY:
What horrible thing has happened here?

 :
These faces, they never knew
what hit them. And now
 :
they're on the road to nowhere!
(Barry hears a sudden whisper)
(Barry looks up and sees Mooseblood, a mosquito playing dead)
MOOSEBLOOD:
Just keep still.
BARRY:
What? You're not dead?
MOOSEBLOOD:
Do I look dead? They will wipe anything
that moves. Where you headed?
BARRY:
To Honey Farms.
I am onto something huge here.
MOOSEBLOOD:
I'm going to Alaska. Moose blood,
crazy stuff. Blows your head off!
ANOTHER BUG PLAYING DEAD:
I'm going to Tacoma.
(Barry looks at another bug)
BARRY:
- And you?
MOOSEBLOOD:
- He really is dead.
BARRY:
All right.
(Another bug hits the windshield and the drivers notice. They activate the
windshield wipers)
MOOSEBLOOD==
Uh-oh!
(The windshield wipers are slowly sliding over the dead bugs and wiping

them off)
BARRY:
- What is that?!
MOOSEBLOOD:
- Oh, no!
 :
- A wiper! Triple blade!
BARRY:
- Triple blade?
MOOSEBLOOD:
Jump on! It's your only chance, bee!
(Mooseblood and Barry grab onto the wiper and they hold on as it wipes the
windshield)
Why does everything have
to be so doggone clean?!
 :
How much do you people need to see?!
(Bangs on windshield)
 :
Open your eyes!
Stick your head out the window!
RADIO IN TRUCK:
From NPR News in Washington,
I'm Carl Kasell.
MOOSEBLOOD:
But don't kill no more bugs!
(Mooseblood and Barry are washed off by the wipr fluid)
MOOSEBLOOD:
- Bee!
BARRY:
- Moose blood guy!!
(Barry starts screaming as he hangs onto the antenna)
(Suddenly it is revealed that a water bug is also hanging on the antenna.

There is a pause and then Barry and the water bug both start screaming)
TRUCK DRIVER:
- You hear something?
GUY IN TRUCK:
- Like what?
TRUCK DRIVER:
Like tiny screaming.
GUY IN TRUCK:
Turn off the radio.
(The antenna starts to lower until it gets to low and sinks into the truck.
The water bug flies off and Barry is forced to let go and he is blown away.
He luckily lands inside a horn on top of the truck where he finds
Mooseblood, who was blown into the same place)
MOOSEBLOOD:
Whassup, bee boy?
BARRY:
Hey, Blood.
(Fast forward in time and we see that Barry is deep in conversation with
Mooseblood. They have been sitting in this truck for a while)
BARRY:
...Just a row of honey jars,
as far as the eye could see.
MOOSEBLOOD:
Wow!
BARRY:
I assume wherever this truck goes
is where they're getting it.
 :
I mean, that honey's ours.
MOOSEBLOOD:
- Bees hang tight.
BARRY:

- We're all jammed in.
 :
It's a close community.
MOOSEBLOOD:
Not us, man. We on our own.
Every mosquito on his own.
BARRY:
- What if you get in trouble?
MOOSEBLOOD:
- You a mosquito, you in trouble.
 :
Nobody likes us. They just smack.
See a mosquito, smack, smack!
BARRY:
At least you're out in the world.
You must meet girls.
MOOSEBLOOD:
Mosquito girls try to trade up,
get with a moth, dragonfly.
 :
Mosquito girl don't want no mosquito.
(An ambulance passes by and it has a blood donation sign on it)
You got to be kidding me!
 :
Mooseblood's about to leave
the building! So long, bee!
(Mooseblood leaves and flies onto the window of the ambulance where there
are other mosquito's hanging out)
 :
- Hey, guys!
OTHER MOSQUITO:
- Mooseblood!

MOOSEBLOOD:
I knew I'd catch y'all down here.
Did you bring your crazy straw?
(The truck goes out of view and Barry notices that the truck he's on is
pulling into a camp of some sort)
TRUCK DRIVER:
We throw it in jars, slap a label on it,
and it's pretty much pure profit.
(Barry flies out)
BARRY:
What is this place?
BEEKEEPER 1#:
A bee's got a brain
the size of a pinhead.
BEEKEEPER #2:
They are pinheads!
 :
Pinhead.
 :
- Check out the new smoker.
BEEKEEPER #1:
- Oh, sweet. That's the one you want.
 :
The Thomas 3000!
BARRY:
Smoker?
BEEKEEPER #1:
Ninety puffs a minute, semi-automatic.
Twice the nicotine, all the tar.
 :
A couple breaths of this
knocks them right out.

BEEKEEPER #2:
They make the honey,
and we make the money.
BARRY:
"They make the honey,
and we make the money"?
(The Beekeeper sprays hundreds of cheap miniature apartments with the
smoker. The bees are fainting or passing out)
Oh, my!
 :
What's going on? Are you OK?
(Barry flies into one of the apartment and helps a Bee couple get off the
ground. They are coughing and its hard for them to stand)
BEE IN APARTMENT:
Yeah. It doesn't last too long.
BARRY:
Do you know you're
in a fake hive with fake walls?
BEE IN APPARTMENT:
Our queen was moved here.
We had no choice.
(The apartment room is completely empty except for a photo on the wall of
the "queen" who is obviously a man in women's clothes)
BARRY:
This is your queen?
That's a man in women's clothes!
 :
That's a drag queen!
 :
What is this?
(Barry flies out and he discovers that there are hundreds of these
structures, each housing thousands of Bees)
Oh, no!
 :
There's hundreds of them!
(Barry takes out his camera and takes pictures of these Bee work camps. The
beekeepers look very evil in these depictions)

Bee honey.
 :
Our honey is being brazenly stolen
on a massive scale!
 :
This is worse than anything bears
have done! I intend to do something.
(Flash forward in time and Barry is showing these pictures to his parents)
JANET:
Oh, Barry, stop.
MARTIN:
Who told you humans are taking
our honey? That's a rumor.
BARRY:
Do these look like rumors?
(Holds up the pictures)
UNCLE CARL:
That's a conspiracy theory.
These are obviously doctored photos.
JANET:
How did you get mixed up in this?
ADAM:
He's been talking to humans.
JANET:
- What?
MARTIN:
- Talking to humans?!
ADAM:
He has a human girlfriend.
And they make out!
JANET:
Make out? Barry!

BARRY:
We do not.
ADAM:
- You wish you could.
MARTIN:
- Whose side are you on?
BARRY:
The bees!
UNCLE CARL:
(He has been sitting in the back of the room this entire time)
I dated a cricket once in San Antonio.
Those crazy legs kept me up all night.
JANET:
Barry, this is what you want
to do with your life?
BARRY:
I want to do it for all our lives.
Nobody works harder than bees!
 :
Dad, I remember you
coming home so overworked
 :
your hands were still stirring.
You couldn't stop.
JANET:
I remember that.
BARRY:
What right do they have to our honey?
 :
We live on two cups a year. They put it
in lip balm for no reason whatsoever!

ADAM:
Even if it's true, what can one bee do?
BARRY:
Sting them where it really hurts.
MARTIN:
In the face! The eye!
 :
- That would hurt.
BARRY:
- No.
MARTIN:
Up the nose? That's a killer.
BARRY:
There's only one place you can sting
the humans, one place where it matters.
(Flash forward a bit in time and we are watching the Bee News)
BEE NEWS NARRATOR:
Hive at Five, the hive's only
full-hour action news source.
BEE PROTESTOR:
No more bee beards!
BEE NEWS NARRATOR:
With Bob Bumble at the anchor desk.
 :
Weather with Storm Stinger.
 :
Sports with Buzz Larvi.
 :
And Jeanette Chung.
BOB BUMBLE:
- Good evening. I'm Bob Bumble.
JEANETTE CHUNG:

- And I'm Jeanette Chung.
BOB BUMBLE:
A tri-county bee, Barry Benson,
 :
intends to sue the human race
for stealing our honey,
 :
packaging it and profiting
from it illegally!
JEANETTE CHUNG:
Tomorrow night on Bee Larry King,
 :
we'll have three former queens here in
our studio, discussing their new book,
 :
Classy Ladies,
out this week on Hexagon.
(The scene changes to an interview on the news with Bee version of Larry
King and Barry)
BEE LARRY KING:
Tonight we're talking to Barry Benson.
 :
Did you ever think, "I'm a kid
from the hive. I can't do this"?
BARRY:
Bees have never been afraid
to change the world.
 :
What about Bee Columbus?
Bee Gandhi? Bejesus?
BEE LARRY KING:
Where I'm from, we'd never sue humans.

 :
We were thinking
of stickball or candy stores.
BARRY:
How old are you?
BEE LARRY KING:
The bee community
is supporting you in this case,
 :
which will be the trial
of the bee century.
BARRY:
You know, they have a Larry King
in the human world too.
BEE LARRY KING:
It's a common name. Next week...
BARRY:
He looks like you and has a show
and suspenders and colored dots...
BEE LARRY KING:
Next week...
BARRY:
Glasses, quotes on the bottom from the
guest even though you just heard 'em.
BEE LARRY KING:
Bear Week next week!
They're scary, hairy and here, live.
(Bee Larry King gets annoyed and flies away offscreen)
BARRY:
Always leans forward, pointy shoulders,
squinty eyes, very Jewish.
(Flash forward in time. We see Vanessa enter and Ken enters behind her.
They are arguing)

KEN:
In tennis, you attack
at the point of weakness!
VANESSA:
It was my grandmother, Ken. She's 81.
KEN==
Honey, her backhand's a joke!
I'm not gonna take advantage of that?
BARRY:
(To Ken)
Quiet, please.
Actual work going on here.
KEN:
(Pointing at Barry)
- Is that that same bee?
VANESSA:
- Yes, it is!
 :
I'm helping him sue the human race.
BARRY:
- Hello.
KEN:
- Hello, bee.
VANESSA:
This is Ken.
BARRY:
(Recalling the "Winter Boots" incident earlier)
Yeah, I remember you. Timberland, size
ten and a half. Vibram sole, I believe.
KEN:
(To Vanessa)
Why does he talk again?
VANESSA:

Listen, you better go
'cause we're really busy working.
KEN:
But it's our yogurt night!
VANESSA:
(Holding door open for Ken)
Bye-bye.
KEN:
(Yelling)
Why is yogurt night so difficult?!
(Ken leaves and Vanessa walks over to Barry. His workplace is a mess)
VANESSA:
You poor thing.
You two have been at this for hours!
BARRY:
Yes, and Adam here
has been a huge help.
ADAM:
- Frosting...
- How many sugars?
 ==BARRY==
Just one. I try not
to use the competition.
 :
So why are you helping me?
VANESSA:
Bees have good qualities.
 :
And it takes my mind off the shop.
 :
Instead of flowers, people
are giving balloon bouquets now.
BARRY:

Those are great, if you're three.
VANESSA:
And artificial flowers.
BARRY:
- Oh, those just get me psychotic!
VANESSA:
- Yeah, me too.
 :
BARRY:
Bent stingers, pointless pollination.
ADAM:
Bees must hate those fake things!
 :
Nothing worse
than a daffodil that's had work done.
 :
Maybe this could make up
for it a little bit.
VANESSA:
- This lawsuit's a pretty big deal.
BARRY:
- I guess.
ADAM:
You sure you want to go through with it?
BARRY:
Am I sure? When I'm done with
the humans, they won't be able
 :
to say, "Honey, I'm home,"
without paying a royalty!
(Flash forward in time and we are watching the human news. The camera shows

a crowd outside a courthouse)
NEWS REPORTER:
It's an incredible scene
here in downtown Manhattan,
 :
where the world anxiously waits,
because for the first time in history,
 :
we will hear for ourselves
if a honeybee can actually speak.
(We are no longer watching through a news camera)
ADAM:
What have we gotten into here, Barry?
BARRY:
It's pretty big, isn't it?
ADAM==
(Looking at the hundreds of people around the courthouse)
I can't believe how many humans
don't work during the day.
BARRY:
You think billion-dollar multinational
food companies have good lawyers?
SECURITY GUARD:
Everybody needs to stay
behind the barricade.
(A limousine drives up and a fat man,Layton Montgomery, a honey industry
owner gets out and walks past Barry)
ADAM:
- What's the matter?
BARRY:
- I don't know, I just got a chill.
(Fast forward in time and everyone is in the court)
MONTGOMERY:
Well, if it isn't the bee team.

(To Honey Industry lawyers)
You boys work on this?
MAN:
All rise! The Honorable
Judge Bumbleton presiding.
JUDGE BUMBLETON:
All right. Case number 4475,
 :
Superior Court of New York,
Barry Bee Benson v. the Honey Industry
 :
is now in session.
 :
Mr. Montgomery, you're representing
the five food companies collectively?
MONTGOMERY:
A privilege.
JUDGE BUMBLETON:
Mr. Benson... you're representing
all the bees of the world?
(Everyone looks closely, they are waiting to see if a Bee can really talk)
(Barry makes several buzzing sounds to sound like a Bee)
BARRY:
I'm kidding. Yes, Your Honor,
we're ready to proceed.
JUDGE BUMBLBETON:
Mr. Montgomery,
your opening statement, please.
MONTGOMERY:
Ladies and gentlemen of the jury,
 :
my grandmother was a simple woman.
 :

Born on a farm, she believed
it was man's divine right
 :
to benefit from the bounty
of nature God put before us.
 :
If we lived in the topsy-turvy world
Mr. Benson imagines,
 :
just think of what would it mean.
 :
I would have to negotiate
with the silkworm
 :
for the elastic in my britches!
 :
Talking bee!
(Montgomery walks over and looks closely at Barry)
 :
How do we know this isn't some sort of
 :
holographic motion-picture-capture
Hollywood wizardry?
 :
They could be using laser beams!
 :
Robotics! Ventriloquism!
Cloning! For all we know,
 :
he could be on steroids!
JUDGE BUMBLETON:
Mr. Benson?

BARRY:
Ladies and gentlemen,
there's no trickery here.
 :
I'm just an ordinary bee.
Honey's pretty important to me.
 :
It's important to all bees.
We invented it!
 :
We make it. And we protect it
with our lives.
 :
Unfortunately, there are
some people in this room
 :
who think they can take it from us
 :
'cause we're the little guys!
I'm hoping that, after this is all over,
 :
you'll see how, by taking our honey,
you not only take everything we have
 :
but everything we are!
JANET==
(To Martin)
I wish he'd dress like that
all the time. So nice!
JUDGE BUMBLETON:
Call your first witness.
BARRY:
So, Mr. Klauss Vanderhayden

of Honey Farms, big company you have.
KLAUSS VANDERHAYDEN:
I suppose so.
BARRY:
I see you also own
Honeyburton and Honron!
KLAUSS:
Yes, they provide beekeepers
for our farms.
BARRY:
Beekeeper. I find that
to be a very disturbing term.
 :
I don't imagine you employ
any bee-free-ers, do you?
KLAUSS:
(Quietly)
- No.
BARRY:
- I couldn't hear you.
KLAUSS:
- No.
BARRY:
- No.
 :
Because you don't free bees.
You keep bees. Not only that,
 :
it seems you thought a bear would be
an appropriate image for a jar of honey.
KLAUSS:
They're very lovable creatures.

 :
Yogi Bear, Fozzie Bear, Build-A-Bear.
BARRY:
You mean like this?
(The bear from Over The Hedge barges in through the back door and it is
roaring and standing on its hind legs. It is thrashing its claws and people
are screaming. It is being held back by a guard who has the bear on a
chain)
 :
(Pointing to the roaring bear)
Bears kill bees!
 :
How'd you like his head crashing
through your living room?!
 :
Biting into your couch!
Spitting out your throw pillows!
JUDGE BUMBLETON:
OK, that's enough. Take him away.
(The bear stops roaring and thrashing and walks out)
BARRY:
So, Mr. Sting, thank you for being here.
Your name intrigues me.
 :
- Where have I heard it before?
MR. STING:
- I was with a band called The Police.
BARRY:
But you've never been
a police officer, have you?
STING:
No, I haven't.
BARRY:

No, you haven't. And so here
we have yet another example
 :
of bee culture casually
stolen by a human
 :
for nothing more than
a prance-about stage name.
STING:
Oh, please.
BARRY:
Have you ever been stung, Mr. Sting?
 :
Because I'm feeling
a little stung, Sting.
 :
Or should I say... Mr. Gordon M. Sumner!
MONTGOMERY:
That's not his real name?! You idiots!
BARRY:
Mr. Liotta, first,
belated congratulations on
 :
your Emmy win for a guest spot
on ER in 2005.
RAY LIOTTA:
Thank you. Thank you.
BARRY:
I see from your resume
that you're devilishly handsome
 :
with a churning inner turmoil

that's ready to blow.
RAY LIOTTA:
I enjoy what I do. Is that a crime?
BARRY:
Not yet it isn't. But is this
what it's come to for you?
 :
Exploiting tiny, helpless bees
so you don't
 :
have to rehearse
your part and learn your lines, sir?
RAY LIOTTA:
Watch it, Benson!
I could blow right now!
BARRY:
This isn't a goodfella.
This is a badfella!
(Ray Liotta looses it and tries to grab Barry)
RAY LIOTTA:
Why doesn't someone just step on
this creep, and we can all go home?!
JUDGE BUMBLETON:
- Order in this court!
RAY LIOTTA:
- You're all thinking it!
(Judge Bumbleton starts banging her gavel)
JUDGE BUMBLETON:
Order! Order, I say!
RAY LIOTTA:
- Say it!
MAN:

- Mr. Liotta, please sit down!
(We see a montage of magazines which feature the court case)
(Flash forward in time and Barry is back home with Vanessa)
BARRY:
I think it was awfully nice
of that bear to pitch in like that.
VANESSA:
I think the jury's on our side.
BARRY:
Are we doing everything right,you know, legally?
VANESSA:
I'm a florist.
BARRY:
Right. Well, here's to a great team.
VANESSA:
To a great team!
(Ken walks in from work. He sees Barry and he looks upset when he sees
Barry clinking his glass with Vanessa)
KEN:
Well, hello.
VANESSA:
- Oh, Ken!
BARRY:
- Hello!
VANESSA:
I didn't think you were coming.
 :
No, I was just late.
I tried to call, but...
(Ken holds up his phone and flips it open. The phone has no charge)
...the battery...
VANESSA:

I didn't want all this to go to waste,
so I called Barry. Luckily, he was free.
KEN:
Oh, that was lucky.
(Ken sits down at the table across from Barry and Vanessa leaves the room)
VANESSA:
There's a little left.
I could heat it up.
KEN:
(Not taking his eyes off Barry)
Yeah, heat it up, sure, whatever.
BARRY:
So I hear you're quite a tennis player.
 :
I'm not much for the game myself.
The ball's a little grabby.
KEN:
That's where I usually sit.
Right...
(Points to where Barry is sitting)
there.
VANESSA:
(Calling from other room)
Ken, Barry was looking at your resume,
 :
and he agreed with me that eating with
chopsticks isn't really a special skill.
KEN:
(To Barry)
You think I don't see what you're doing?
BARRY:
I know how hard it is to find
the right job. We have that in common.

KEN:
Do we?
BARRY:
Bees have 100 percent employment,
but we do jobs like taking the crud out.
KEN:
(Menacingly)
That's just what
I was thinking about doing.
(Ken reaches for a fork on the table but knocks if on the floor. He goes to
pick it up)
VANESSA:
Ken, I let Barry borrow your razor
for his fuzz. I hope that was all right.
(Ken quickly rises back up after hearing this but hits his head on the
table and yells)
BARRY:
I'm going to drain the old stinger.
KEN:
Yeah, you do that.
(Barry flies past Ken to get to the bathroom and Ken freaks out, splashing
some of the wine he was using to cool his head in his eyes. He yells in
anger)
(Barry looks at the magazines featuring his victories in court)
BARRY:
Look at that.
(Barry flies into the bathroom)
(He puts his hand on his head but this makes hurts him and makes him even
madder. He yells again)
(Barry is washing his hands in the sink but then Ken walks in)
KEN:
You know, you know I've just about had it
(Closes bathroom door behind him)
with your little mind games.
(Ken is menacingly rolling up a magazine)
BARRY:

(Backing away)
- What's that?
KEN:
- Italian Vogue.
BARRY:
Mamma mia, that's a lot of pages.
KEN:
It's a lot of ads.
BARRY:
Remember what Van said, why is
your life more valuable than mine?
KEN:
That's funny, I just can't seem to recall that!
(Ken smashes everything off the sink with the magazine and Barry narrowly
escapes)
(Ken follows Barry around and tries to hit him with the magazine but he
keeps missing)
(Ken gets a spray bottle)
 :
I think something stinks in here!
BARRY:
(Enjoying the spray)
I love the smell of flowers.
(Ken holds a lighter in front of the spray bottle)
KEN:
How do you like the smell of flames?!
BARRY:
Not as much.
(Ken fires his make-shift flamethrower but misses Barry, burning the
bathroom. He torches the whole room but looses his footing and falls into
the bathtub. After getting hit in the head by falling objects 3 times he
picks up the shower head, revealing a Water bug hiding under it)
WATER BUG:
Water bug! Not taking sides!

(Barry gets up out of a pile of bathroom supplies and he is wearing a
chapstick hat)
BARRY:
Ken, I'm wearing a Chapstick hat!
This is pathetic!
(Ken switches the shower head to lethal)
KEN:
I've got issues!
(Ken sprays Barry with the shower head and he crash lands into the toilet)
(Ken menacingly looks down into the toilet at Barry)
Well, well, well, a royal flush!
BARRY:
- You're bluffing.
KEN:
- Am I?
(flushes toilet)
(Barry grabs a chapstick from the toilet seat and uses it to surf in the
flushing toilet)
BARRY:
Surf's up, dude!
(Barry flies out of the toilet on the chapstick and sprays Ken's face with
the toilet water)
 :
EW,Poo water!
BARRY:
That bowl is gnarly.
KEN:
(Aiming a toilet cleaner at Barry)
Except for those dirty yellow rings!
(Barry cowers and covers his head and Vanessa runs in and takes the toilet
cleaner from Ken just before he hits Barry)
VANESSA:
Kenneth! What are you doing?!
KEN==
(Leaning towards Barry)

You know, I don't even like honey!
I don't eat it!
VANESSA:
We need to talk!
(Vanessa pulls Ken out of the bathroom)
 :
He's just a little bee!
 :
And he happens to be
the nicest bee I've met in a long time!
KEN:
Long time? What are you talking about?!
Are there other bugs in your life?
VANESSA:
No, but there are other things bugging
me in life. And you're one of them!
KEN:
Fine! Talking bees, no yogurt night...
 :
My nerves are fried from riding
on this emotional roller coaster!
VANESSA:
Goodbye, Ken.
(Ken huffs and walks out and slams the door. But suddenly he walks back in
and stares at Barry)
 :
And for your information,
I prefer sugar-free, artificial
sweeteners MADE BY MAN!
(Ken leaves again and Vanessa leans in towards Barry)
VANESSA:
I'm sorry about all that.
(Ken walks back in again)

KEN:
I know it's got
an aftertaste! I LIKE IT!
(Ken leaves for the last time)
VANESSA:
I always felt there was some kind
of barrier between Ken and me.
 :
I couldn't overcome it.
Oh, well.
 :
Are you OK for the trial?
BARRY:
I believe Mr. Montgomery
is about out of ideas.
(Flash forward in time and Barry, Adam, and Vanessa are back in court)
MONTGOMERY--
We would like to call
Mr. Barry Benson Bee to the stand.
ADAM:
Good idea! You can really see why he's
considered one of the best lawyers...
(Barry stares at Adam)
...Yeah.
LAWYER:
Layton, you've
gotta weave some magic
with this jury,
or it's gonna be all over.
MONTGOMERY:
Don't worry. The only thing I have
to do to turn this jury around
 :
is to remind them
of what they don't like about bees.
(To lawyer)

- You got the tweezers?
LAWYER:
- Are you allergic?
MONTGOMERY:
Only to losing, son. Only to losing.
 :
Mr. Benson Bee, I'll ask you
what I think we'd all like to know.
 :
What exactly is your relationship
(Points to Vanessa)
 :
to that woman?
BARRY:
We're friends.
MONTGOMERY:
- Good friends?
BARRY:
- Yes.
MONTGOMERY:
How good? Do you live together?
ADAM:
Wait a minute...
 :
MONTGOMERY:
Are you her little...
 :
...bedbug?
(Adam's stinger starts vibrating. He is agitated)
I've seen a bee documentary or two.
From what I understand,

 :
doesn't your queen give birth
to all the bee children?
BARRY:
- Yeah, but...
MONTGOMERY:
(Pointing at Janet and Martin)
- So those aren't your real parents!
JANET:
- Oh, Barry...
BARRY:
- Yes, they are!
ADAM:
Hold me back!
(Vanessa tries to hold Adam back. He wants to sting Montgomery)
MONTGOMERY:
You're an illegitimate bee,
aren't you, Benson?
ADAM:
He's denouncing bees!
MONTGOMERY:
Don't y'all date your cousins?
(Montgomery leans over on the jury stand and stares at Adam)
VANESSA:
- Objection!
(Vanessa raises her hand to object but Adam gets free. He flies straight at
Montgomery)
=ADAM:
- I'm going to pincushion this guy!
BARRY:
Adam, don't! It's what he wants!
(Adam stings Montgomery in the butt and he starts thrashing around)

MONTGOMERY:
Oh, I'm hit!!
 :
Oh, lordy, I am hit!
JUDGE BUMBLETON:
(Banging gavel)
Order! Order!
MONTGOMERY:
(Overreacting)
The venom! The venom
is coursing through my veins!
 :
I have been felled
by a winged beast of destruction!
 :
You see? You can't treat them
like equals! They're striped savages!
 :
Stinging's the only thing
they know! It's their way!
BARRY:
- Adam, stay with me.
ADAM:
- I can't feel my legs.
MONTGOMERY:
(Overreacting and throwing his body around the room)
What angel of mercy
will come forward to suck the poison
 :
from my heaving buttocks?
JUDGE BUMLBETON:
I will have order in this court. Order!

 :
Order, please!
(Flash forward in time and we see a human news reporter)
NEWS REPORTER:
The case of the honeybees
versus the human race
 :
took a pointed turn against the bees
 :
yesterday when one of their legal
team stung Layton T. Montgomery.
(Adam is laying in a hospital bed and Barry flies in to see him)
BARRY:
- Hey, buddy.
ADAM:
- Hey.
BARRY:
- Is there much pain?
ADAM:
- Yeah.
 :
I...
 :
I blew the whole case, didn't I?
BARRY:
It doesn't matter. What matters is
you're alive. You could have died.
ADAM:
I'd be better off dead. Look at me.
(A small plastic sword is replaced as Adam's stinger)
They got it from the cafeteria
downstairs, in a tuna sandwich.

 :
Look, there's
a little celery still on it.
(Flicks off the celery and sighs)
BARRY:
What was it like to sting someone?
ADAM:
I can't explain it. It was all...
 :
All adrenaline and then...
and then ecstasy!
BARRY:
...All right.
ADAM:
You think it was all a trap?
BARRY:
Of course. I'm sorry.
I flew us right into this.
 :
What were we thinking? Look at us. We're
just a couple of bugs in this world.
ADAM:
What will the humans do to us
if they win?
BARRY:
I don't know.
ADAM:
I hear they put the roaches in motels.
That doesn't sound so bad.
BARRY:
Adam, they check in,
but they don't check out!

ADAM:
Oh, my.
(Coughs)
Could you get a nurse
to close that window?
BARRY:
- Why?
ADAM:
- The smoke.
(We can see that two humans are smoking cigarettes outside)
 :
Bees don't smoke.
BARRY:
Right. Bees don't smoke.
 :
Bees don't smoke!
But some bees are smoking.
 :
That's it! That's our case!
ADAM:
It is? It's not over?
BARRY:
Get dressed. I've gotta go somewhere.
 :
Get back to the court and stall.
Stall any way you can.
(Flash forward in time and Adam is making a paper boat in the courtroom)
ADAM:
And assuming you've done step 29 correctly, you're ready for the tub!
(We see that the jury have each made their own paper boats after being
taught how by Adam. They all look confused)
JUDGE BUMBLETON:

Mr. Flayman.
ADAM:
Yes? Yes, Your Honor!
JUDGE BUMBLETON:
Where is the rest of your team?
ADAM:
(Continues stalling)
Well, Your Honor, it's interesting.
 :
Bees are trained to fly haphazardly,
 :
and as a result,
we don't make very good time.
 :
I actually heard a funny story about...
MONTGOMERY:
Your Honor,
haven't these ridiculous bugs
 :
taken up enough
of this court's valuable time?
 :
How much longer will we allow
these absurd shenanigans to go on?
 :
They have presented no compelling
evidence to support their charges
 :
against my clients,
who run legitimate businesses.
 :
I move for a complete dismissal

of this entire case!
JUDGE BUMBLETON:
Mr. Flayman, I'm afraid I'm going
 :
to have to consider
Mr. Montgomery's motion.
ADAM:
But you can't! We have a terrific case.
MONTGOMERY:
Where is your proof?
Where is the evidence?
 :
Show me the smoking gun!
BARRY:
(Barry flies in through the door)
Hold it, Your Honor!
You want a smoking gun?
 :
Here is your smoking gun.
(Vanessa walks in holding a bee smoker. She sets it down on the Judge's
podium)
JUDGE BUMBLETON:
What is that?
BARRY:
It's a bee smoker!
MONTGOMERY:
(Picks up smoker)
What, this?
This harmless little contraption?
 :
This couldn't hurt a fly,
let alone a bee.
(Montgomery accidentally fires it at the bees in the crowd and they faint

and cough)
(Dozens of reporters start taking pictures of the suffering bees)
BARRY:
Look at what has happened
 :
to bees who have never been asked,
"Smoking or non?"
 :
Is this what nature intended for us?
 :
To be forcibly addicted
to smoke machines
 :
and man-made wooden slat work camps?
 :
Living out our lives as honey slaves
to the white man?
(Barry points to the honey industry owners. One of them is an African
American so he awkwardly separates himself from the others)
LAWYER:
- What are we gonna do?
- He's playing the species card.
BARRY:
Ladies and gentlemen, please,
free these bees!
ADAM AND VANESSA:
Free the bees! Free the bees!
BEES IN CROWD:
Free the bees!
HUMAN JURY:
Free the bees! Free the bees!
JUDGE BUMBLETON:
The court finds in favor of the bees!

BARRY:
Vanessa, we won!
VANESSA:
I knew you could do it! High-five!
(Vanessa hits Barry hard because her hand is too big)
 :
Sorry.
BARRY:
(Overjoyed)
I'm OK! You know what this means?
 :
All the honey
will finally belong to the bees.
 :
Now we won't have
to work so hard all the time.
MONTGOMERY:
This is an unholy perversion
of the balance of nature, Benson.
 :
You'll regret this.
(Montgomery leaves and Barry goes outside the courtroom. Several reporters
start asking Barry questions)
REPORTER 1#:
Barry, how much honey is out there?
BARRY:
All right. One at a time.
REPORTER 2#:
Barry, who are you wearing?
BARRY:
My sweater is Ralph Lauren,
and I have no pants.

(Barry flies outside with the paparazzi and Adam and Vanessa stay back)
ADAM:
(To Vanessa)
- What if Montgomery's right?
Vanessa:
- What do you mean?
ADAM:
We've been living the bee way
a long time, 27 million years.
(Flash forward in time and Barry is talking to a man)
BUSINESS MAN:
Congratulations on your victory.
What will you demand as a settlement?
BARRY:
First, we'll demand a complete shutdown
of all bee work camps.
(As Barry is talking we see a montage of men putting "closed" tape over the
work camps and freeing the bees in the crappy apartments)
Then we want back the honey
that was ours to begin with,
 :
every last drop.
(Men in suits are pushing all the honey of the aisle and into carts)
We demand an end to the glorification
of the bear as anything more
(We see a statue of a bear-shaped honey container being pulled down by
bees)
than a filthy, smelly,
bad-breath stink machine.
 :
We're all aware
of what they do in the woods.
(We see Winnie the Pooh sharing his honey with Piglet in the cross-hairs of
a high-tech sniper rifle)
BARRY:
(Looking through binoculars)

Wait for my signal.
 :
Take him out.
(Winnie gets hit by a tranquilizer dart and dramatically falls off the log
he was standing on, his tongue hanging out. Piglet looks at Pooh in fear
and the Sniper takes the honey.)
SNIPER:
He'll have nausea
for a few hours, then he'll be fine.
(Flash forward in time)
BARRY:
And we will no longer tolerate
bee-negative nicknames...
(Mr. Sting is sitting at home until he is taken out of his house by the men
in suits)
STING:
But it's just a prance-about stage name!
BARRY:
...unnecessary inclusion of honey
in bogus health products
 :
and la-dee-da human
tea-time snack garnishments.
(An old lady is mixing honey into her tea but suddenly men in suits smash
her face down on the table and take the honey)
OLD LADY:
Can't breathe.
(A honey truck pulls up to Barry's hive)
WORKER:
Bring it in, boys!
 :
Hold it right there! Good.
 :
Tap it.

(Tons of honey is being pumped into the hive's storage)
BEE WORKER 1#:
(Honey overflows from the cup)
Mr. Buzzwell, we just passed three cups,
and there's gallons more coming!
 :
- I think we need to shut down!
=BEE WORKER #2=
- Shut down? We've never shut down.
 :
Shut down honey production!
DEAN BUZZWELL:
Stop making honey!
(The bees all leave their stations. Two bees run into a room and they put
the keys into a machine)
Turn your key, sir!
(Two worker bees dramatically turn their keys, which opens the button which
they press, shutting down the honey-making machines. This is the first time
this has ever happened)
BEE:
...What do we do now?
(Flash forward in time and a Bee is about to jump into a pool full of
honey)
Cannonball!
(The bee gets stuck in the honey and we get a short montage of Bees leaving
work)
(We see the Pollen Jocks flying but one of them gets a call on his antenna)
LOU LU DUVA:
(Through "phone")
We're shutting honey production!
 :
Mission abort.
POLLEN JOCK #1:
Aborting pollination and nectar detail.
Returning to base.
(The Pollen Jocks fly back to the hive)

(We get a time lapse of Central Park slowly wilting away as the bees all
relax)
BARRY:
Adam, you wouldn't believe
how much honey was out there.
ADAM:
Oh, yeah?
BARRY:
What's going on? Where is everybody?
(The entire street is deserted)
 :
- Are they out celebrating?
ADAM:
- They're home.
 :
They don't know what to do.
Laying out, sleeping in.
 :
I heard your Uncle Carl was on his way
to San Antonio with a cricket.
BARRY:
At least we got our honey back.
ADAM:
Sometimes I think, so what if humans
liked our honey? Who wouldn't?
 :
It's the greatest thing in the world!
I was excited to be part of making it.
 :
This was my new desk. This was my
new job. I wanted to do it really well.
 :

And now...
 :
Now I can't.
(Flash forward in time and Barry is talking to Vanessa)
BARRY:
I don't understand
why they're not happy.
 :
I thought their lives would be better!
 :
They're doing nothing. It's amazing.
Honey really changes people.
VANESSA:
You don't have any idea
what's going on, do you?
BARRY:
- What did you want to show me?
(Vanessa takes Barry to the rooftop where they first had coffee and points
to her store)
VANESSA:
- This.
(Points at her flowers. They are all grey and wilting)
BARRY:
What happened here?
VANESSA:
That is not the half of it.
(Small flash forward in time and Vanessa and Barry are on the roof of her
store and she points to Central Park)
(We see that Central Park is no longer green and colorful, rather it is
grey, brown, and dead-like. It is very depressing to look at)
BARRY:
Oh, no. Oh, my.
 :

They're all wilting.
VANESSA:
Doesn't look very good, does it?
BARRY:
No.
VANESSA:
And whose fault do you think that is?
BARRY:
You know, I'm gonna guess bees.
VANESSA==
(Staring at Barry)
Bees?
BARRY:
Specifically, me.
 :
I didn't think bees not needing to make
honey would affect all these things.
VANESSA:
It's not just flowers.
Fruits, vegetables, they all need bees.
BARRY:
That's our whole SAT test right there.
VANESSA:
Take away produce, that affects
the entire animal kingdom.
 :
And then, of course...
BARRY:
The human species?
 :
So if there's no more pollination,

 :
it could all just go south here,
couldn't it?
VANESSA:
I know this is also partly my fault.
BARRY:
How about a suicide pact?
VANESSA:
How do we do it?
BARRY:
- I'll sting you, you step on me.
VANESSA:
- That just kills you twice.
BARRY:
Right, right.
VANESSA:
Listen, Barry...
sorry, but I gotta get going.
(Vanessa leaves)
BARRY:
(To himself)
I had to open my mouth and talk.
 :
Vanessa?
 :
Vanessa? Why are you leaving?
Where are you going?
(Vanessa is getting into a taxi)
VANESSA:
To the final Tournament of Roses parade
in Pasadena.
 :

They've moved it to this weekend
because all the flowers are dying.
 :
It's the last chance
I'll ever have to see it.
BARRY:
Vanessa, I just wanna say I'm sorry.
I never meant it to turn out like this.
VANESSA:
I know. Me neither.
(The taxi starts to drive away)
BARRY:
Tournament of Roses.
Roses can't do sports.
 :
Wait a minute. Roses. Roses?
 :
Roses!
 :
Vanessa!
(Barry flies after the Taxi)
VANESSA:
Roses?!
 :
Barry?
(Barry is flying outside the window of the taxi)
BARRY:
- Roses are flowers!
VANESSA:
- Yes, they are.
BARRY:
Flowers, bees, pollen!

VANESSA:
I know.
That's why this is the last parade.
BARRY:
Maybe not.
Could you ask him to slow down?
VANESSA:
Could you slow down?
(The taxi driver screeches to a stop and Barry keeps flying forward)
 :
Barry!
(Barry flies back to the window)
BARRY:
OK, I made a huge mistake.
This is a total disaster, all my fault.
VANESSA:
Yes, it kind of is.
BARRY:
I've ruined the planet.
I wanted to help you
 :
with the flower shop.
I've made it worse.
VANESSA:
Actually, it's completely closed down.
BARRY:
I thought maybe you were remodeling.
 :
But I have another idea, and it's
greater than my previous ideas combined.
VANESSA:
I don't want to hear it!

BARRY:
All right, they have the roses,
the roses have the pollen.
 :
I know every bee, plant
and flower bud in this park.
 :
All we gotta do is get what they've got
back here with what we've got.
 :
- Bees.
VANESSA:
- Park.
BARRY:
- Pollen!
VANESSA:
- Flowers.
BARRY:
- Re-pollination!
VANESSA:
- Across the nation!
 :
Tournament of Roses,
Pasadena, California.
 :
They've got nothing
but flowers, floats and cotton candy.
 :
Security will be tight.
BARRY:
I have an idea.

(Flash forward in time. Vanessa is about to board a plane which has all the
Roses on board.
VANESSA:
Vanessa Bloome, FTD.
(Holds out badge)
 :
Official floral business. It's real.
SECURITY GUARD:
Sorry, ma'am. Nice brooch.
=VANESSA==
Thank you. It was a gift.
(Barry is revealed to be hiding inside the brooch)
(Flash back in time and Barry and Vanessa are discussing their plan)
BARRY:
Once inside,
we just pick the right float.
VANESSA:
How about The Princess and the Pea?
 :
I could be the princess,
and you could be the pea!
BARRY:
Yes, I got it.
 :
- Where should I sit?
GUARD:
- What are you?
BARRY:
- I believe I'm the pea.
GUARD:
- The pea?
VANESSA:

It goes under the mattresses.
GUARD:
- Not in this fairy tale, sweetheart.
- I'm getting the marshal.
VANESSA:
You do that!
This whole parade is a fiasco!
 :
Let's see what this baby'll do.
(Vanessa drives the float through traffic)
GUARD:
Hey, what are you doing?!
BARRY==
Then all we do
is blend in with traffic...
 :
...without arousing suspicion.
 :
Once at the airport,
there's no stopping us.
(Flash forward in time and Barry and Vanessa are about to get on a plane)
SECURITY GUARD:
Stop! Security.
 :
- You and your insect pack your float?
VANESSA:
- Yes.
SECURITY GUARD:
Has it been
in your possession the entire time?
VANESSA:
- Yes.

SECURITY GUARD:
Would you remove your shoes?
(To Barry)
- Remove your stinger.
BARRY:
- It's part of me.
SECURITY GUARD:
I know. Just having some fun.
Enjoy your flight.
(Barry plotting with Vanessa)
BARRY:
Then if we're lucky, we'll have
just enough pollen to do the job.
(Flash forward in time and Barry and Vanessa are flying on the plane)
Can you believe how lucky we are? We
have just enough pollen to do the job!
VANESSA:
I think this is gonna work.
BARRY:
It's got to work.
CAPTAIN SCOTT:
(On intercom)
Attention, passengers,
this is Captain Scott.
 :
We have a bit of bad weather
in New York.
 :
It looks like we'll experience
a couple hours delay.
VANESSA:
Barry, these are cut flowers
with no water. They'll never make it.
BARRY:

I gotta get up there
and talk to them.
VANESSA==
Be careful.
(Barry flies right outside the cockpit door)
BARRY:
Can I get help
with the Sky Mall magazine?
I'd like to order the talking
inflatable nose and ear hair trimmer.
(The flight attendant opens the door and walks out and Barry flies into the
cockpit unseen)
BARRY:
Captain, I'm in a real situation.
CAPTAIN SCOTT:
- What'd you say, Hal?
CO-PILOT HAL:
- Nothing.
(Scott notices Barry and freaks out)
CAPTAIN SCOTT:
Bee!
BARRY:
No,no,no, Don't freak out! My entire species...
(Captain Scott gets out of his seat and tries to suck Barry into a handheld
vacuum)
HAL:
(To Scott)
What are you doing?
(Barry lands on Hals hair but Scott sees him. He tries to suck up Barry but
instead he sucks up Hals toupee)
CAPTAIN SCOTT:
Uh-oh.
BARRY:
- Wait a minute! I'm an attorney!

HAL:
(Hal doesn't know Barry is on his head)
- Who's an attorney?
CAPTAIN SCOTT:
Don't move.
(Scott hits Hal in the face with the vacuum in an attempt to hit Barry. Hal
is knocked out and he falls on the life raft button which launches an
infalatable boat into Scott, who gets knocked out and falls to the floor.
They are both uncounscious.)
BARRY:
(To himself)
Oh, Barry.
BARRY:
(On intercom, with a Southern accent)
Good afternoon, passengers.
This is your captain.
 :
Would a Miss Vanessa Bloome in 24B
please report to the cockpit?
(Vanessa looks confused)
(Normal accent)
...And please hurry!
(Vanessa opens the door and sees the life raft and the uncounscious pilots)
VANESSA:
What happened here?
BARRY:
I tried to talk to them, but
then there was a DustBuster,
a toupee, a life raft exploded.
 :
Now one's bald, one's in a boat,
and they're both unconscious!
VANESSA:
...Is that another bee joke?
BARRY:

- No!
 :
No one's flying the plane!
BUD DITCHWATER:
(Through radio on plane)
This is JFK control tower, Flight 356.
What's your status?
VANESSA:
This is Vanessa Bloome.
I'm a florist from New York.
BUD:
Where's the pilot?
VANESSA:
He's unconscious,
and so is the copilot.
BUD:
Not good. Does anyone onboard
have flight experience?
BARRY:
As a matter of fact, there is.
BUD:
- Who's that?
BARRY:
- Barry Benson.
BUD:
From the honey trial?! Oh, great.
BARRY:
Vanessa, this is nothing more
than a big metal bee.
 :
It's got giant wings, huge engines.

VANESSA:
I can't fly a plane.
BARRY:
- Why not? Isn't John Travolta a pilot?
VANESSA:
- Yes.
BARRY:
How hard could it be?
(Vanessa sits down and flies for a little bit but we see lightning clouds
outside the window)
VANESSA:
Wait, Barry!
We're headed into some lightning.
(An ominous lightning storm looms in front of the plane)
(We are now watching the Bee News)
BOB BUMBLE:
This is Bob Bumble. We have some
late-breaking news from JFK Airport,
 :
where a suspenseful scene
is developing.
 :
Barry Benson,
fresh from his legal victory...
ADAM:
That's Barry!
BOB BUMBLE:
...is attempting to land a plane,
loaded with people, flowers
 :
and an incapacitated flight crew.
JANET, MARTIN, UNCLE CAR AND ADAM:
Flowers?!
(The scene switches to the human news)

REPORTER:
(Talking with Bob Bumble)
We have a storm in the area
and two individuals at the controls
 :
with absolutely no flight experience.
BOB BUMBLE:
Just a minute.
There's a bee on that plane.
BUD:
I'm quite familiar with Mr. Benson
and his no-account compadres.
 :
They've done enough damage.
REPORTER:
But isn't he your only hope?
BUD:
Technically, a bee
shouldn't be able to fly at all.
 :
Their wings are too small...
BARRY:
(Through radio)
Haven't we heard this a million times?
 :
"The surface area of the wings
and body mass make no sense."...
BOB BUMBLE:
- Get this on the air!
BEE:
- Got it.

BEE NEWS CREW:
- Stand by.
BEE NEWS CREW:
- We're going live!
BARRY:
(Through radio on TV)
...The way we work may be a mystery to you.
 :
Making honey takes a lot of bees
doing a lot of small jobs.
 :
But let me tell you about a small job.
 :
If you do it well,
it makes a big difference.
 :
More than we realized.
To us, to everyone.
 :
That's why I want to get bees
back to working together.
 :
That's the bee way!
We're not made of Jell-O.
 :
We get behind a fellow.
 :
- Black and yellow!
BEES:
- Hello!
(The scene switches and Barry is teaching Vanessa how to fly)
BARRY:

Left, right, down, hover.
VANESSA:
- Hover?
BARRY:
- Forget hover.
VANESSA:
This isn't so hard.
(Pretending to honk the horn)
Beep-beep! Beep-beep!
(A Lightning bolt hits the plane and autopilot turns off)
Barry, what happened?!
BARRY:
Wait, I think we were
on autopilot the whole time.
VANESSA:
- That may have been helping me.
BARRY:
- And now we're not!
VANESSA:
So it turns out I cannot fly a plane.
(The plane plummets but we see Lou Lu Duva and the Pollen Jocks, along with
multiple other bees flying towards the plane)
Lou Lu DUva:
All of you, let's get
behind this fellow! Move it out!
 :
Move out!
(The scene switches back to Vanessa and Barry in the plane)
BARRY:
Our only chance is if I do what I'd do,
you copy me with the wings of the plane!
(Barry sticks out his arms like an airplane and flys in front of Vanessa's
face)

VANESSA:
Don't have to yell.
BARRY:
I'm not yelling!
We're in a lot of trouble.
VANESSA:
It's very hard to concentrate
with that panicky tone in your voice!
BARRY:
It's not a tone. I'm panicking!
VANESSA:
I can't do this!
(Barry slaps Vanessa)
BARRY:
Vanessa, pull yourself together.
You have to snap out of it!
VANESSA:
(Slaps Barry)
You snap out of it.
BARRY:
(Slaps Vanessa)
 :
You snap out of it.
VANESSA:
- You snap out of it!
BARRY:
- You snap out of it!
(We see that all the Pollen Jocks are flying under the plane)
VANESSA:
- You snap out of it!
BARRY:
- You snap out of it!

VANESSA:
- You snap out of it!
BARRY:
- You snap out of it!
VANESSA:
- Hold it!
BARRY:
- Why? Come on, it's my turn.
VANESSA:
How is the plane flying?
(The plane is now safely flying)
VANESSA:
I don't know.
(Barry's antennae rings like a phone. Barry picks up)
BARRY:
Hello?
LOU LU DUVA:
(Through "phone")
Benson, got any flowers
for a happy occasion in there?
(All of the Pollen Jocks are carrying the plane)
BARRY:
The Pollen Jocks!
 :
They do get behind a fellow.
LOU LU DUVA:
- Black and yellow.
POLLEN JOCKS:
- Hello.
LOU LU DUVA:
All right, let's drop this tin can

on the blacktop.
BARRY:
Where? I can't see anything. Can you?
VANESSA:
No, nothing. It's all cloudy.
 :
Come on. You got to think bee, Barry.
BARRY:
- Thinking bee.
- Thinking bee.
(On the runway there are millions of bees laying on their backs)
BEES:
Thinking bee!
Thinking bee! Thinking bee!
BARRY:
Wait a minute.
I think I'm feeling something.
VANESSA:
- What?
BARRY:
- I don't know. It's strong, pulling me.
 :
Like a 27-million-year-old instinct.
 :
Bring the nose down.
BEES:
Thinking bee!
Thinking bee! Thinking bee!
CONTROL TOWER OPERATOR:
- What in the world is on the tarmac?
BUD:
- Get some lights on that!

(It is revealed that all the bees are organized into a giant pulsating
flower formation)
BEES:
Thinking bee!
Thinking bee! Thinking bee!
BARRY:
- Vanessa, aim for the flower.
VANESSA:
- OK.
BARRY:
Out the engines. We're going in
on bee power. Ready, boys?
LOU LU DUVA:
Affirmative!
BARRY:
Good. Good. Easy, now. That's it.
 :
Land on that flower!
 :
Ready? Full reverse!
 :
Spin it around!
(The plane's nose is pointed at a flower painted on a nearby plane)
- Not that flower! The other one!
VANESSA:
- Which one?
BARRY:
- That flower.
(The plane is now pointed at a fat guy in a flowered shirt. He freaks out
and tries to take a picture of the plane)
VANESSA:
- I'm aiming at the flower!

BARRY:
That's a fat guy in a flowered shirt.
I mean the giant pulsating flower
made of millions of bees!
(The plane hovers over the bee-flower)
 :
Pull forward. Nose down. Tail up.
 :
Rotate around it.
VANESSA:
- This is insane, Barry!
BARRY:
- This's the only way I know how to fly.
BUD:
Am I koo-koo-kachoo, or is this plane
flying in an insect-like pattern?
(The plane is unrealistically hovering and spinning over the bee-flower)
BARRY:
Get your nose in there. Don't be afraid.
Smell it. Full reverse!
 :
Just drop it. Be a part of it.
 :
Aim for the center!
 :
Now drop it in! Drop it in, woman!
 :
Come on, already.
(The bees scatter and the plane safely lands)
VANESSA:
Barry, we did it!
You taught me how to fly!

BARRY:
- Yes!
(Vanessa is about to high-five Barry)
No high-five!
VANESSA:
- Right.
ADAM:
Barry, it worked!
Did you see the giant flower?
BARRY:
What giant flower? Where? Of course
I saw the flower! That was genius!
ADAM:
- Thank you.
BARRY:
- But we're not done yet.
 :
Listen, everyone!
 :
This runway is covered
with the last pollen
 :
from the last flowers
available anywhere on Earth.
 :
That means this is our last chance.
 :
We're the only ones who make honey,
pollinate flowers and dress like this.
 :
If we're gonna survive as a species,
this is our moment! What do you say?

 :
Are we going to be bees, or just
Museum of Natural History keychains?
BEES:
We're bees!
BEE WHO LIKES KEYCHAINS:
Keychain!
BARRY:
Then follow me! Except Keychain.
POLLEN JOCK #1:
Hold on, Barry. Here.
 :
You've earned this.
BARRY:
Yeah!
 :
I'm a Pollen Jock! And it's a perfect
fit. All I gotta do are the sleeves.
(The Pollen Jocks throw Barry a nectar-collecting gun. Barry catches it)
Oh, yeah.
JANET:
That's our Barry.
(Barry and the Pollen Jocks get pollen from the flowers on the plane)
(Flash forward in time and the Pollen Jocks are flying over NYC)
 :
(Barry pollinates the flowers in Vanessa's shop and then heads to Central
Park)
BOY IN PARK:
Mom! The bees are back!
ADAM:
(Putting on his Krelman hat)
If anybody needs

to make a call, now's the time.
 :
I got a feeling we'll be
working late tonight!
(The bee honey factories are back up and running)
(Meanwhile at Vanessa's shop)
VANESSA:
(To customer)
Here's your change. Have a great
afternoon! Can I help who's next?
 :
Would you like some honey with that?
It is bee-approved. Don't forget these.
(There is a room in the shop where Barry does legal work for other animals.
He is currently talking with a Cow)
COW:
Milk, cream, cheese, it's all me.
And I don't see a nickel!
 :
Sometimes I just feel
like a piece of meat!
BARRY:
I had no idea.
VANESSA:
Barry, I'm sorry.
Have you got a moment?
BARRY:
Would you excuse me?
My mosquito associate will help you.
MOOSEBLOOD:
Sorry I'm late.
COW:
He's a lawyer too?

MOOSEBLOOD:
Ma'am, I was already a blood-sucking parasite.
All I needed was a briefcase.
VANESSA:
Have a great afternoon!
 :
Barry, I just got this huge tulip order,
and I can't get them anywhere.
BARRY:
No problem, Vannie.
Just leave it to me.
VANESSA:
You're a lifesaver, Barry.
Can I help who's next?
BARRY:
All right, scramble, jocks!
It's time to fly.
VANESSA:
Thank you, Barry!
(Ken walks by on the sidewalk and sees the "bee-approved honey" in
Vanessa's shop)
KEN:
That bee is living my life!!
ANDY:
Let it go, Kenny.
KEN:
- When will this nightmare end?!
ANDY:
- Let it all go.
BARRY:
- Beautiful day to fly.
POLLEN JOCK:

- Sure is.
BARRY:
Between you and me,
I was dying to get out of that office.
(Barry recreates the scene near the beginning of the movie where he flies
through the box kite. The movie fades to black and the credits being)
[--after credits; No scene can be seen but the characters can be heard
talking over the credits--]
You have got
to start thinking bee, my friend!
 :
- Thinking bee!
- Me?
BARRY:
(Talking over singer)
Hold it. Let's just stop
for a second. Hold it.
 :
I'm sorry. I'm sorry, everyone.
Can we stop here?
SINGER:
Oh, BarryBARRY:
I'm not making a major life decision
during a production number!
SINGER:
All right. Take ten, everybody.
Wrap it up, guys.
BARRY:
I had virtually no rehearsal for that.
2024-09-18 11:00:58 -05:00
Mahatthana (Kelvin) Nomsawadi
b72465de99 Export more stuff 2024-09-18 10:58:10 -05:00
Mahatthana (Kelvin) Nomsawadi
09559ab743 Add RTK queries for comments 2024-09-18 10:43:04 -05:00
Mahatthana (Kelvin) Nomsawadi
a3ef1878e3 Able to import comment types 2024-09-18 10:43:04 -05:00
Oisin Coveney
52787d42a9 Merge branch 'master' into hackathon-commenting/main 2024-09-18 10:24:09 -05:00
Ryan Laurie
48a523fef1 add comment feed 2024-09-18 10:21:22 -05:00
Mahatthana (Kelvin) Nomsawadi
824b6047d2 Add comment types 2024-09-18 10:12:48 -05:00
Oisin Coveney
b98d7eb5aa Hey guys 2024-09-17 18:37:05 -05:00
Oisin Coveney
9bd0ce0f0d Hey guys 2024-09-17 18:36:07 -05:00
Ryan Laurie
3470fa469c ok fine 2024-09-17 17:52:12 -05:00
Ryan Laurie
60a5206ebd init the winning hackathon branch 2024-09-17 17:41:37 -05:00
67 changed files with 2313 additions and 63 deletions

0
.awesomeness Normal file
View File

View File

@@ -0,0 +1,51 @@
import type { User } from "./user";
export type CommentModel = "dashboard" | "card" | "comment";
type CommentId = number;
export interface Comment {
id: CommentId;
created_at: string;
updated_at: string;
model: CommentModel;
model_id: number;
text: string;
resolved: boolean;
resolved_at: boolean;
reactions: Reaction[];
author: User;
replies: Comment[];
}
export interface Reaction {
id: number;
content: "string"; // but only emojis
author: User;
}
export type QueryCommentParams =
| {
model: CommentModel;
model_id: number;
}
| {
model: never;
model_id: never;
};
export interface CreateCommentRequest {
model: CommentModel;
model_id: number;
text: string;
}
export interface ReactToCommentRequest {
id: CommentId;
content: string;
}
export interface ResolveCommentRequest {
id: CommentId;
resolved: true;
}

View File

@@ -7,6 +7,7 @@ export * from "./bookmark";
export * from "./card";
export * from "./click-behavior";
export * from "./collection";
export * from "./comment";
export * from "./dashboard";
export * from "./database";
export * from "./dataset";

View File

@@ -34,6 +34,7 @@ export interface User extends BaseUser {
custom_homepage: {
dashboard_id: DashboardId;
} | null;
avatar: string | null;
}
export interface UserListResult {
@@ -113,4 +114,5 @@ export type UpdateUserRequest = {
is_superuser?: boolean;
login_attributes?: Record<UserAttribute, UserAttribute> | null;
user_group_memberships?: { id: number; is_group_manager: boolean }[];
avatar?: string;
};

View File

@@ -0,0 +1,19 @@
.uploadIcon {
--icon-size: 25px;
position: absolute;
right: 0;
bottom: 15px;
background: var(--mb-color-background-brand);
color: white;
font-weight: 800;
font-size: 20px;
line-height: 0;
width: var(--icon-size);
height: var(--icon-size);
display: grid;
place-items: center;
border-radius: 50%;
border: 2px solid white;
cursor: pointer;
}

View File

@@ -47,6 +47,7 @@ export const HeaderAvatar = styled(UserAvatar)`
width: 3em;
height: 3em;
margin-bottom: ${space(1)};
cursor: pointer;
${breakpointMinSmall} {
width: 4em;

View File

@@ -1,12 +1,15 @@
import type { Path } from "history";
import { useMemo } from "react";
import { type ChangeEvent, useMemo, useRef } from "react";
import { t } from "ttag";
import { useUpdateUserMutation } from "metabase/api";
import Radio from "metabase/core/components/Radio";
import { getFullName } from "metabase/lib/user";
import { PLUGIN_IS_PASSWORD_USER } from "metabase/plugins";
import { Box } from "metabase/ui";
import type { User } from "metabase-types/api";
import AccountHeaderStyle from "./AccountHeader.module.css";
import {
AccountHeaderRoot,
HeaderAvatar,
@@ -45,10 +48,45 @@ export const AccountHeader = ({
const userFullName = getFullName(user);
const fileInputRef = useRef<HTMLInputElement>(null);
const [updateUser] = useUpdateUserMutation();
function handleAvatarUpload(fileEvent: ChangeEvent<HTMLInputElement>) {
if (fileEvent.target.files && fileEvent.target.files.length > 0) {
const file = fileEvent.target.files[0];
const reader = new FileReader();
reader.onload = async readerEvent => {
const dataUri = readerEvent.target?.result as string;
updateUser({
id: user.id,
avatar: dataUri,
});
};
reader.readAsDataURL(file);
}
}
return (
<AccountHeaderRoot data-testid="account-header">
<HeaderSection>
<HeaderAvatar user={user} />
<Box
pos="relative"
onClick={() => {
fileInputRef.current?.click();
}}
>
<HeaderAvatar user={user} />
<div className={AccountHeaderStyle.uploadIcon}>+</div>
</Box>
<input
data-testid="file-input"
ref={fileInputRef}
hidden
onChange={handleAvatarUpload}
type="file"
accept="image/jpeg,image/png,image/svg+xml"
multiple={false}
/>
{userFullName && <HeaderTitle>{userFullName}</HeaderTitle>}
<HeaderSubtitle>{user.email}</HeaderSubtitle>
</HeaderSection>

View File

@@ -0,0 +1,56 @@
import type {
Comment,
CreateCommentRequest,
ReactToCommentRequest,
ResolveCommentRequest,
} from "metabase-types/api";
import { Api } from "./api";
import { invalidateTags, listTag, provideCommentListTags } from "./tags";
export const commentApi = Api.injectEndpoints({
endpoints: builder => ({
listComment: builder.query<Comment[], void>({
query: body => ({
method: "GET",
url: "/api/comment",
body,
}),
providesTags: (comments = []) => provideCommentListTags(comments),
}),
createComment: builder.mutation<Comment, CreateCommentRequest>({
query: body => ({
method: "POST",
url: "/api/comment",
body,
}),
invalidatesTags: (_comment, error) =>
invalidateTags(error, [listTag("comment")]),
}),
reactToComment: builder.mutation<Comment, ReactToCommentRequest>({
query: ({ id, ...body }) => ({
method: "POST",
url: `/api/comment/${id}/react`,
body,
}),
invalidatesTags: (_comment, error) =>
invalidateTags(error, [listTag("comment")]),
}),
resolveComment: builder.mutation<Comment, ResolveCommentRequest>({
query: ({ id, ...body }) => ({
method: "PUT",
url: `/api/comment/${id}`,
body,
}),
invalidatesTags: (_comment, error) =>
invalidateTags(error, [listTag("comment")]),
}),
}),
});
export const {
useListCommentQuery,
useCreateCommentMutation,
useReactToCommentMutation,
useResolveCommentMutation,
} = commentApi;

View File

@@ -8,6 +8,7 @@ export * from "./bookmark";
export * from "./card";
export * from "./cloud-migration";
export * from "./collection";
export * from "./comment";
export * from "./dashboard";
export * from "./database";
export * from "./dataset";

View File

@@ -9,6 +9,7 @@ export const TAG_TYPES = [
"cloud-migration",
"channel",
"collection",
"comment",
"dashboard",
"database",
"field",

View File

@@ -11,6 +11,7 @@ import type {
Collection,
CollectionItem,
CollectionItemModel,
Comment,
Dashboard,
DashboardQueryMetadata,
DashboardSubscription,
@@ -198,6 +199,18 @@ export function provideCollectionTags(
return [idTag("collection", collection.id)];
}
export function provideCommentListTags(
comments: Comment[],
): TagDescription<TagType>[] {
return [listTag("comment"), ...comments.flatMap(provideCommentTags)];
}
export function provideCommentTags(
comment: Comment,
): TagDescription<TagType>[] {
return [idTag("comment", comment.id)];
}
export function provideModelIndexTags(
modelIndex: ModelIndex,
): TagDescription<TagType>[] {

View File

@@ -0,0 +1,144 @@
import { useDisclosure } from "@mantine/hooks";
import { useRef } from "react";
import { usePrevious } from "react-use";
import { useCurrentUser } from "embedding-sdk";
import {
useCreateCommentMutation,
useListCommentQuery,
useResolveCommentMutation,
} from "metabase/api";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import { Button, Icon, Stack } from "metabase/ui";
import type {
CardId,
CommentModel,
DashboardId,
UserId,
} from "metabase-types/api";
import type { Comment as CommentType } from "../../types";
import { CommentInput } from "../CommentInput/CommentInput";
import { CommentList } from "../comment-section/CommentSection";
export function CommentFeed({
model,
modelId,
userId,
autoScroll = false,
shadowed = false,
}: {
model?: CommentModel;
modelId?: CardId | DashboardId;
userId?: UserId;
autoScroll?: boolean;
shadowed?: boolean;
}) {
const { data: comments, isLoading } = useListCommentQuery(
{
model,
model_id: modelId,
user_id: userId,
},
{
pollingInterval: 3000,
skipPollingIfUnfocused: true,
},
);
const [isResolvedThreadsOpen, { toggle }] = useDisclosure(false);
const user = useCurrentUser();
const previousComments = usePrevious(comments);
const [saveComment] = useCreateCommentMutation();
const [handleResolve] = useResolveCommentMutation();
const ref = useRef<HTMLDivElement>(null);
if (isLoading) {
return <LoadingAndErrorWrapper loading error={null} />;
}
const canComment = Boolean(!!model && !!modelId);
const scrollToBottom = () => {
if (ref.current) {
ref.current.scrollIntoView({ behavior: "smooth" });
}
};
if (autoScroll && comments?.length !== previousComments?.length) {
window.requestAnimationFrame(() => {
setTimeout(() => scrollToBottom(), 100);
});
}
const openThreads = comments?.filter(comment => !comment.resolved) ?? [];
const resolvedThreads = comments?.filter(comment => comment.resolved) ?? [];
return (
<Stack spacing="xs">
<Stack spacing="md" pr="md">
<CommentList
comments={openThreads}
onResolve={(comment: CommentType) =>
handleResolve({
id: comment.id,
resolved: !comment.resolved,
})
}
onReply={saveComment}
shadowed={shadowed}
/>
{canComment && (
<CommentInput
placeholder="Add a comment..."
user={user}
onSubmit={text =>
saveComment({
text,
model,
model_id: modelId,
})
}
/>
)}
<div ref={ref} />
</Stack>
{resolvedThreads.length > 0 && (
<Stack mt="-1.5rem">
<Button
styles={{
inner: {
justifyContent: "flex-start",
},
}}
style={{ justifyContent: "flex-start" }}
color="brand"
variant="subtle"
leftIcon={
<Icon
name={isResolvedThreadsOpen ? "chevrondown" : "chevronright"}
/>
}
onClick={toggle}
>
{resolvedThreads.length} resolved{" "}
{resolvedThreads.length > 1 ? "threads" : "thread"}
</Button>
{isResolvedThreadsOpen && (
<CommentList
onReply={saveComment}
shadowed={shadowed}
comments={resolvedThreads}
/>
)}
</Stack>
)}
</Stack>
);
}

View File

@@ -0,0 +1 @@
export * from "./CommentFeed";

View File

@@ -0,0 +1,76 @@
.mentions {
margin: 1em 0;
width: 100%;
padding: 10px;
}
.mentions--singleLine .mentions__control {
/* color: salmon;
background: salmon; */
/* display: inline-block; */
/* width: 200px; */
}
.mentions--singleLine .mentions__highlighter {
/* padding: 10px; */
/* border: 2px solid red;
background: red; */
}
.mentions--singleLine .mentions__input {
background: white !important;
padding: 10px;
border: 1px solid var(--mb-color-border);
border-radius: 4px;
padding-right: 40px;
&:focus-within {
border-color: var(--mb-color-brand);
}
}
/* .mentions--multiLine .mentions__control {
font-family: monospace;
font-size: 14pt;
}
.mentions--multiLine .mentions__highlighter {
border: 1px solid transparent;
padding: 9px;
min-height: 63px;
}
.mentions--multiLine .mentions__input {
border: 1px solid silver;
padding: 9px;
outline: 0;
} */
.mentions__suggestions__list {
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.15);
font-size: 10pt;
/* z-index: 8888; */
}
.mentions__suggestions__item {
background-color: white;
padding: 5px 15px;
border-bottom: 1px solid rgba(0, 0, 0, 0.15);
}
.mentions__suggestions__item--focused {
background-color: var(--mb-color-background-selected);
color: var(--mb-color-text-selected);
}
.mentions__mention {
position: relative;
z-index: 1;
color: blue;
text-shadow:
1px 1px 1px white,
1px -1px 1px white,
-1px 1px 1px white,
-1px -1px 1px white;
text-decoration: underline;
pointer-events: none;
}

View File

@@ -0,0 +1,78 @@
import { useCallback, useState } from "react";
import { Mention, MentionsInput } from "react-mentions";
import { useListUsersQuery } from "metabase/api";
import Styles from "metabase/css/core/index.css";
import { Flex, type FlexProps, Icon } from "metabase/ui";
import type { User } from "metabase-types/api";
import { UserIcon } from "../comment/Comment";
import CommentInputStyle from "./CommentInput.module.css";
export function CommentInput({
onSubmit,
placeholder,
autoFocus,
user,
...flexProps
}: {
onSubmit: (text: string) => Promise<void>;
user: User;
placeholder: string;
autoFocus: boolean;
} & FlexProps) {
const [text, setText] = useState("");
const handleSubmit = useCallback(
(text: string) => {
onSubmit(text).then(() => setText(""));
},
[onSubmit],
);
const { data: users } = useListUsersQuery({});
return (
<Flex align="center" gap="sm" {...flexProps}>
<UserIcon user={user} />
<MentionsInput
value={text}
onKeyDown={e => {
if (e.key === "Enter") {
handleSubmit(text);
}
}}
onChange={e => setText(e.target.value)}
placeholder={placeholder}
autoFocus
singleLine
className="mentions"
classNames={CommentInputStyle}
>
<Mention
markup="@[__display__](__id__)"
trigger="@"
data={
users?.data.map(user => {
return { id: user.id, display: user.common_name };
}) ?? []
}
/>
</MentionsInput>
{text && (
<Icon
style={{
position: "absolute",
top: 26,
right: 12,
}}
name="enter_or_return"
color="var(--mb-color-brand)"
className={Styles.cursorPointer}
onClick={() => handleSubmit(text)}
/>
)}
</Flex>
);
}

View File

@@ -0,0 +1 @@
export * from "./CommentInput";

View File

@@ -0,0 +1,42 @@
import { getCurrentUser } from "metabase/admin/datamodel/selectors";
import { useSelector } from "metabase/lib/redux";
import { Paper } from "metabase/ui";
import type { Comment as CommentType } from "../../types";
import { CommentInput } from "../CommentInput/CommentInput";
import { Comment } from "../comment/Comment";
export const CommentList = ({
comments,
onResolve,
shadowed = false,
onReply,
}: {
comments: CommentType[];
onResolve?: (comment: CommentType) => void;
shadowed?: boolean;
onReply: (comment: CommentType) => Promise<void>;
}) => {
const currentUser = useSelector(getCurrentUser);
return comments.map(comment => (
<Paper key={comment.id} p="lg" withBorder shadow={shadowed ? "xl" : "none"}>
<Comment
comment={comment}
onResolve={onResolve ? () => onResolve(comment) : undefined}
/>
{comment.replies?.map(c => <Comment key={c.id} comment={c} />)}
<CommentInput
placeholder="Add a comment ..."
user={currentUser}
onSubmit={text =>
onReply({
text,
model: "comment",
model_id: comment.id,
})
}
/>
</Paper>
));
};

View File

@@ -0,0 +1 @@
export * from "./CommentSection";

View File

@@ -0,0 +1,27 @@
.CommentGrid {
display: grid;
grid-template-columns: auto 1fr 1fr 1fr auto;
grid-template-rows: auto 1fr auto;
grid-column-gap: 0;
grid-row-gap: 0;
}
.UserInfo {
grid-area: 1 / 2 / 2 / 5;
}
.ActionIcons {
grid-area: 1 / 6 / 2 / 7;
}
.Avatar {
grid-area: 1 / 1 / 4 / 2;
}
.CommentText {
grid-area: 2 / 2 / 3 / 6;
}
.Reactions {
grid-area: 3 / 2 / 4 / 6;
}

View File

@@ -0,0 +1,105 @@
import { useListUsersQuery, useReactToCommentMutation } from "metabase/api";
import { toMentionDisplay } from "metabase/comments/utils";
import DateTime from "metabase/components/DateTime";
import Markdown from "metabase/core/components/Markdown";
import {
ActionIcon,
Avatar,
Box,
Divider,
Group,
Icon,
Stack,
Text,
} from "metabase/ui";
import type { User, UserId } from "metabase-types/api";
import type { Comment as CommentType } from "../../types";
import { ReactionList } from "../reaction-list";
import CommentS from "./Comment.module.css";
export const Comment = ({
comment,
onResolve,
}: {
comment: CommentType;
onResolve?: () => Promise<void>;
}) => {
const [onReact] = useReactToCommentMutation();
const { data: users } = useListUsersQuery({});
const usersData = users?.data ?? [];
const userMapping = usersData.reduce(
(mapping, user) => {
mapping[user.id] = user;
return mapping;
},
{} as Record<UserId, User>,
);
return (
<Box mb="sm" className={CommentS.CommentGrid}>
<Box ml="sm" className={CommentS.UserInfo}>
<Group spacing="sm">
<Text fw="bold" c="text-dark" size="md">
{comment.author.common_name}
</Text>
<Text span size="md" c="text-medium">
<DateTime value={comment.created_at} />
{/* {comment.created_at ?? "Apr 20 6:90PM"} */}
</Text>
</Group>
</Box>
<Box className={CommentS.ActionIcons}>
<Group spacing="xs" align="end">
{onResolve && (
<ActionIcon onClick={onResolve}>
<Icon name="check" />
</ActionIcon>
)}
<ActionIcon>
<Icon name="ellipsis" />
</ActionIcon>
</Group>
</Box>
<Box className={CommentS.Avatar}>
<Stack h="100%" align="center" spacing="sm">
<UserIcon user={comment.author} />
<Box style={{ flex: 1 }}>
<Divider h="100%" orientation="vertical" />
</Box>
</Stack>
</Box>
<Box ml="0.5rem" className={CommentS.CommentText}>
<Markdown>{toMentionDisplay(comment.text, userMapping)}</Markdown>
</Box>
<Box ml="0.35rem" pt="sm" className={CommentS.Reactions}>
<ReactionList
onAddReaction={(reaction: string) => {
onReact({
id: comment.id,
emoji: reaction,
});
}}
reactions={comment.reactions}
/>
</Box>
</Box>
);
};
export function UserIcon({ user }: { user: User }) {
if (user?.avatar) {
return <Avatar src={user.avatar} radius="xl" size="1.5rem" />;
}
return (
<Avatar radius="xl" c="text-light" size="1.5rem" color="brand">
<Text c="white" size="0.65rem">
{user?.first_name?.at?.(0) ?? "😁"}
{user?.last_name?.at?.(0) ?? ""}
</Text>
</Avatar>
);
}

View File

@@ -0,0 +1 @@
export * from "./Comment";

View File

@@ -0,0 +1,5 @@
export * from "./comment-section/CommentSection";
export * from "./comment/Comment";
export * from "./CommentFeed/CommentFeed";
export * from "./CommentInput/CommentInput";
export * from "./reaction/Reaction";

View File

@@ -0,0 +1,38 @@
import _ from "underscore";
import { getCurrentUser } from "metabase/admin/datamodel/selectors";
import { useSelector } from "metabase/lib/redux";
import { Group } from "metabase/ui";
import type { Reaction as ReactionType } from "../../types";
import { Reaction } from "../reaction";
import { AddReaction } from "../reaction/AddReaction";
export const ReactionList = ({
onAddReaction,
reactions,
}: {
onAddReaction: (reaction: string) => void;
reactions: ReactionType[];
}) => {
const groupedReactions = Object.entries(_.groupBy(reactions, "emoji"));
const user = useSelector(getCurrentUser);
return (
<Group spacing="xs">
{groupedReactions.map(([emoji, userList]) => {
const isSelected = userList.some(u => u.author.email === user.email);
return (
<Reaction
isSelected={isSelected}
key={emoji}
userList={userList.map(u => u.author)}
emoji={emoji}
onAddReaction={isSelected ? () => {} : onAddReaction}
/>
);
})}
<AddReaction onSelect={onAddReaction} />
</Group>
);
};

View File

@@ -0,0 +1 @@
export * from "./ReactionList";

View File

@@ -0,0 +1,10 @@
.EmojiButton {
transition: background ease-in-out 250ms;
background: transparent;
border-radius: 0.25rem;
}
.EmojiButton:hover {
background: var(--mb-color-bg-medium);
transition: all ease-in-out;
}

View File

@@ -0,0 +1,145 @@
import data from "@emoji-mart/data";
import { useDisclosure } from "@mantine/hooks";
import { SearchIndex, init } from "emoji-mart";
import { useState } from "react";
import {
Box,
type ButtonProps,
Center,
Icon,
Input,
Paper,
Popover,
SimpleGrid,
Stack,
Text,
UnstyledButton,
} from "metabase/ui";
import AddReactionS from "./AddReaction.module.css";
import { ReactionBadge } from "./ReactionBadge";
init({ data });
const convertUnicodeToEmoji = unicode => {
// Split the string by "-" and convert each part to a code point
const codePoints = unicode.split("-").map(code => parseInt(code, 16));
// Convert the code points into an emoji using fromCodePoint
return String.fromCodePoint(...codePoints);
};
export const EmojiButton = ({
emoji,
onClick,
}: {
emoji: string;
onClick: ButtonProps["onClick"];
}) => (
<UnstyledButton onClick={onClick} className={AddReactionS.EmojiButton}>
<Center p="xs">
<Text fz="xl">{emoji}</Text>
</Center>
</UnstyledButton>
);
export const ASlightlyCrapEmojiPicker = ({
onSelect,
}: {
onSelect: (value: string) => void;
}) => {
const [searchText, setSearchText] = useState<string>();
const [searchResults, setSearchResults] = useState<any[]>();
const onSearchChange = (t: string) => {
setSearchText(t);
search(t);
};
async function search(value: string) {
const emojis = await SearchIndex.search(value);
const results = emojis.map(emoji => {
return emoji.skins[0].native;
});
setSearchResults(results);
}
return (
<Paper w="20rem" h="30rem" p="md" style={{ overflow: "hidden" }}>
<Stack h="100%" style={{ overflow: "hidden" }}>
<Input
value={searchText}
onChange={e => onSearchChange(e.target.value)}
/>
<Stack style={{ overflowY: "auto", overflowX: "hidden" }}>
{searchText ? (
<SimpleGrid spacing="xs" cols={7}>
{searchResults
? searchResults.map((res: string) => (
<EmojiButton
onClick={() => onSelect(res)}
key={res}
emoji={res}
/>
))
: "No results found"}
</SimpleGrid>
) : (
data.categories.map(({ id, emojis }) => (
<Stack key={id}>
<Text fw="bold" tt="capitalize">
{id}
</Text>
<SimpleGrid spacing="xs" cols={7}>
{emojis.map(e => {
const emojiText = convertUnicodeToEmoji(
data.emojis[e].skins[0].unified,
);
return (
<EmojiButton
onClick={() => onSelect(emojiText)}
emoji={emojiText}
key={e}
/>
);
})}
</SimpleGrid>
</Stack>
))
)}
</Stack>
</Stack>
</Paper>
);
};
export const AddReaction = ({
onSelect,
}: {
onSelect: (value: string) => void;
}) => {
const [opened, { toggle, close }] = useDisclosure(false);
const onSelectReaction = (reaction: string) => {
close();
onSelect(reaction);
};
return (
<Popover opened={opened} onClose={close} closeOnClickOutside>
<Popover.Target>
<Box>
<ReactionBadge
tooltipLabel="Add reaction…"
onClick={toggle}
right={<Icon size="0.8rem" name="add" />}
/>
</Box>
</Popover.Target>
<Popover.Dropdown>
<ASlightlyCrapEmojiPicker onSelect={onSelectReaction} />
</Popover.Dropdown>
</Popover>
);
};

View File

@@ -0,0 +1,84 @@
import { getCurrentUser } from "metabase/admin/datamodel/selectors";
import type { Reaction as ReactionType } from "metabase/comments/types";
import { useSelector } from "metabase/lib/redux";
import { Text } from "metabase/ui";
import { ReactionBadge } from "./ReactionBadge";
function formatList(items: string[]) {
if (items.length === 0) {
return "";
}
if (items.length === 1) {
return items[0];
}
if (items.length === 2) {
return items.join(" and ");
}
// For lists with more than 2 items
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
}
export const Reaction = ({
userList,
emoji,
onAddReaction,
isSelected,
}: {
userList: ReactionType["author"][];
emoji: ReactionType["emoji"];
onAddReaction: (value: string) => void;
isSelected: boolean;
}) => {
const currentUser = useSelector(getCurrentUser);
const getUserListString = () => {
const userStrings = userList
.map(user => {
if (currentUser.email === user.email) {
return null;
}
return `${user.first_name} ${user.last_name}` || user.email;
})
.filter(u => !!u);
if (isSelected) {
return ["You", ...userStrings];
}
return userStrings;
};
return (
<ReactionBadge
isSelected={isSelected}
left={
<Text span lh={1} c="text-dark" fz="md" mr="xs">
{emoji}
</Text>
}
right={
<Text
span
lh={1}
ml="2px"
c="text-dark"
fz="sm"
fw={isSelected ? "bold" : "normal"}
>
{userList.length}
</Text>
}
onClick={() => onAddReaction(emoji)}
tooltipLabel={
<Text fz="sm" span c="text-white">
{formatList(getUserListString())}
<Text fz="sm" span c="text-medium">
{` reacted with ${emoji}`}
</Text>
</Text>
}
/>
);
};

View File

@@ -0,0 +1,33 @@
import type { ReactNode } from "react";
import { Button, type ButtonProps, Group, Tooltip } from "metabase/ui";
export const ReactionBadge = ({
left,
right,
tooltipLabel,
isSelected,
...buttonProps
}: {
left?: ReactNode;
right?: ReactNode;
tooltipLabel?: ReactNode | string;
isSelected: boolean;
} & Omit<ButtonProps, "left" | "right">) => (
<Tooltip disabled={!tooltipLabel} label={tooltipLabel}>
<Button
bg={isSelected ? "focus" : "bg-medium"}
px="12px"
py="6px"
radius="xl"
compact
c={isSelected ? "brand" : undefined}
fw={isSelected ? "bold" : undefined}
{...buttonProps}
>
<Group spacing={0}>
{left} {right}
</Group>
</Button>
</Tooltip>
);

View File

@@ -0,0 +1 @@
export * from "./Reaction";

View File

@@ -0,0 +1,380 @@
import type { Comment } from "../types";
export const TEST_DATA: Comment[] = [
{
model: "dashboard",
model_id: 1,
text: "Q3 Performance Dashboard",
resolved: false,
reactions: [
{
emoji: "👍",
author: {
id: 2,
first_name: "Jane",
last_name: "Doe",
email: "jane.doe@example.com",
},
id: 101,
},
{
emoji: "🎉",
author: {
id: 3,
first_name: "Bob",
last_name: "Smith",
email: "bob.smith@example.com",
},
id: 102,
},
],
author: {
id: 1,
first_name: "John",
last_name: "Doe",
email: "john.doe@example.com",
},
replies: [
{
model: "comment",
model_id: 2,
text: "Great overview of our Q3 performance!",
resolved: true,
reactions: [],
author: {
id: 4,
first_name: "Alice",
last_name: "Johnson",
email: "alice.johnson@example.com",
},
},
{
model: "comment",
model_id: 3,
text: "Can we add a breakdown by region?",
resolved: false,
reactions: [
{
emoji: "🤔",
author: {
id: 5,
first_name: "Charlie",
last_name: "Brown",
email: "charlie.brown@example.com",
},
id: 103,
},
],
author: {
id: 6,
first_name: "Eva",
last_name: "Williams",
email: "eva.williams@example.com",
},
replies: [
{
model: "comment",
model_id: 4,
text: "Good idea, I'll work on that this week.",
resolved: false,
reactions: [],
author: {
id: 1,
first_name: "John",
last_name: "Doe",
email: "john.doe@example.com",
},
},
],
},
],
},
{
model: "card",
model_id: 5,
text: "Revenue by Product Category",
resolved: true,
reactions: [],
author: {
id: 7,
first_name: "David",
last_name: "Miller",
email: "david.miller@example.com",
},
replies: [
{
model: "comment",
model_id: 6,
text: "The numbers for Category A seem off. Can we double-check?",
resolved: true,
reactions: [
{
emoji: "👍",
author: {
id: 8,
first_name: "Fiona",
last_name: "Garcia",
email: "fiona.garcia@example.com",
},
id: 104,
},
],
author: {
id: 9,
first_name: "George",
last_name: "Taylor",
email: "george.taylor@example.com",
},
replies: [
{
model: "comment",
model_id: 7,
text: "You're right, there was an error. I've updated the chart.",
resolved: true,
reactions: [
{
emoji: "🙏",
author: {
id: 9,
first_name: "George",
last_name: "Taylor",
email: "george.taylor@example.com",
},
id: 105,
},
],
author: {
id: 7,
first_name: "David",
last_name: "Miller",
email: "david.miller@example.com",
},
},
],
},
{
model: "comment",
model_id: 8,
text: "Great visualization! Very easy to understand.",
resolved: true,
reactions: [
{
emoji: "❤️",
author: {
id: 10,
first_name: "Hannah",
last_name: "Lee",
email: "hannah.lee@example.com",
},
id: 106,
},
],
author: {
id: 11,
first_name: "Ian",
last_name: "Clark",
email: "ian.clark@example.com",
},
},
],
},
{
model: "data",
model_id: 9,
text: "Customer Satisfaction Survey Results",
resolved: false,
reactions: [
{
emoji: "📊",
author: {
id: 12,
first_name: "Julia",
last_name: "White",
email: "julia.white@example.com",
},
id: 107,
},
],
author: {
id: 13,
first_name: "Kevin",
last_name: "Brown",
email: "kevin.brown@example.com",
},
replies: [
{
model: "comment",
model_id: 10,
text: "Interesting trends in the data. We should focus on improving our customer service based on these results.",
resolved: false,
reactions: [],
author: {
id: 14,
first_name: "Laura",
last_name: "Martinez",
email: "laura.martinez@example.com",
},
},
{
model: "comment",
model_id: 11,
text: "Can we break this down by product line?",
resolved: false,
reactions: [
{
emoji: "👍",
author: {
id: 15,
first_name: "Mike",
last_name: "Johnson",
email: "mike.johnson@example.com",
},
id: 108,
},
],
author: {
id: 16,
first_name: "Nina",
last_name: "Patel",
email: "nina.patel@example.com",
},
},
],
},
{
model: "comment",
model_id: 12,
text: "We should schedule a meeting to discuss the Q4 strategy.",
resolved: false,
reactions: [
{
emoji: "👍",
author: {
id: 17,
first_name: "Oscar",
last_name: "Wilson",
email: "oscar.wilson@example.com",
},
id: 109,
},
{
emoji: "📅",
author: {
id: 18,
first_name: "Patricia",
last_name: "Lopez",
email: "patricia.lopez@example.com",
},
id: 110,
},
],
author: {
id: 19,
first_name: "Quinn",
last_name: "Chen",
email: "quinn.chen@example.com",
},
},
{
model: "comment",
model_id: 13,
text: "Has anyone seen the latest competitor analysis report?",
resolved: true,
reactions: [],
author: {
id: 20,
first_name: "Rachel",
last_name: "Kim",
email: "rachel.kim@example.com",
},
replies: [
{
model: "comment",
model_id: 14,
text: "Yes, I have it. I'll send it to you right away.",
resolved: true,
reactions: [
{
emoji: "🙏",
author: {
id: 20,
first_name: "Rachel",
last_name: "Kim",
email: "rachel.kim@example.com",
},
id: 111,
},
],
author: {
id: 21,
first_name: "Sam",
last_name: "Taylor",
email: "sam.taylor@example.com",
},
},
],
},
{
model: "dashboard",
model_id: 15,
text: "Marketing Campaign Performance",
resolved: false,
reactions: [
{
emoji: "🚀",
author: {
id: 22,
first_name: "Tina",
last_name: "Moore",
email: "tina.moore@example.com",
},
id: 112,
},
],
author: {
id: 23,
first_name: "Ulysses",
last_name: "Grant",
email: "ulysses.grant@example.com",
},
replies: [
{
model: "comment",
model_id: 16,
text: "The social media campaign seems to be outperforming our email marketing. Should we reallocate some budget?",
resolved: false,
reactions: [],
author: {
id: 24,
first_name: "Victoria",
last_name: "Adams",
email: "victoria.adams@example.com",
},
},
{
model: "comment",
model_id: 17,
text: "Great insights! Let's discuss this in our next marketing meeting.",
resolved: true,
reactions: [
{
emoji: "👍",
author: {
id: 25,
first_name: "William",
last_name: "Brown",
email: "william.brown@example.com",
},
id: 113,
},
],
author: {
id: 26,
first_name: "Xena",
last_name: "Wilson",
email: "xena.wilson@example.com",
},
},
],
},
];

View File

@@ -0,0 +1,3 @@
export const useComments = () => {
return []
}

View File

@@ -0,0 +1,7 @@
export * from "./components/comment-section";
export * from "./components/comment";
export * from "./components/CommentFeed";
export * from "./components/CommentInput";
export * from "./components/reaction";
export * from "./data/use-comments";
export * from "./utils";

View File

@@ -0,0 +1,39 @@
import type { CardId, DashboardId, User } from "metabase-types/api";
type ReactionId = number;
type CommentId = number;
type ModelRef =
| {
model: "dashboard";
model_id: DashboardId;
}
| {
model: "card";
model_id: CardId;
}
| {
model: "comment";
model_id: CommentId;
}
| {
model: "data";
model_id: number;
};
export type Comment = ModelRef & {
id: number;
text: string;
created_at: string;
updated_at: string;
resolved: boolean;
reactions: Reaction[];
author: Pick<User, "id" | "first_name" | "last_name" | "email">;
replies?: Comment[];
};
export type Reaction = {
emoji: string;
author: Pick<User, "id" | "first_name" | "last_name" | "email">;
id: ReactionId;
};

View File

@@ -0,0 +1,33 @@
import type { User, UserId } from "metabase-types/api";
interface UserToken {
token: string;
userId: UserId;
}
export function toMentionDisplay(
comment: string,
userMapping: Record<UserId, Pick<User, "common_name">>,
) {
const userTokens = getUserTokens(comment);
return userTokens.reduce((translatedComment, userToken) => {
const user = userMapping[userToken.userId];
if (user) {
return translatedComment.replace(userToken.token, `@${user.common_name}`);
}
return translatedComment;
}, comment);
}
function getUserTokens(comment: string): UserToken[] {
const tokens = comment.match(/@\[.+?\]\(\d+\)/g) ?? [];
return tokens.map(token => {
const [_, userId] = token.match(/\((\d+)\)/) ?? [];
return {
token,
userId: Number(userId),
};
});
}

View File

@@ -0,0 +1,31 @@
import { toMentionDisplay } from "./utils";
describe("comments/mention", () => {
describe("toMentionDisplay", () => {
it("should convert to a display comment with mention", () => {
const comment =
"Hey @[Oisin](123) and @[Ryan](456) can you check this out?";
const expectedComment = "Hey @Oisin and @Ryan can you check this out?";
expect(
toMentionDisplay(comment, {
123: { common_name: "Oisin" },
456: { common_name: "Ryan" },
}),
).toEqual(expectedComment);
});
it("should not convert a user ID not existing in the mapping", () => {
const comment =
"Hey @[Oisin](123) and @[Ryan](456) can you check this out?";
const expectedComment =
"Hey @Oisin and @[Ryan](456) can you check this out?";
expect(
toMentionDisplay(comment, {
123: { common_name: "Oisin" },
}),
).toEqual(expectedComment);
});
});
describe("toMentionData", () => {});
});

View File

@@ -16,10 +16,13 @@ interface User {
last_name: string | null;
common_name: string;
email?: string;
avatar: string | null;
}
interface Group {
first_name: string;
// This doesn't really exist, I just don't want TS to complain
avatar: null;
}
// eslint-disable-next-line import/no-default-export -- deprecated usage
@@ -27,6 +30,17 @@ export default function UserAvatar({
user,
...props
}: UserAvatarProps | GroupProps) {
if (user.avatar) {
return (
<StyledAvatar
{...props}
style={{
backgroundImage: `url(${user.avatar})`,
backgroundSize: "cover",
}}
></StyledAvatar>
);
}
return <StyledAvatar {...props}>{userInitials(user) || "?"}</StyledAvatar>;
}

View File

@@ -15,6 +15,7 @@ import {
AddSectionButton,
CopyAnalyticsDashboardButton,
DashboardActionMenu,
DashboardCommentButton,
DashboardInfoButton,
EditDashboardButton,
FullscreenAnalyticsDashboard,
@@ -46,6 +47,7 @@ export const DASHBOARD_ACTION = {
DASHBOARD_HEADER_ACTION_DIVIDER: "DASHBOARD_HEADER_ACTION_DIVIDER",
DASHBOARD_BOOKMARK: "DASHBOARD_BOOKMARK",
DASHBOARD_INFO: "DASHBOARD_INFO",
DASHBOARD_COMMENT: "DASHBOARD_COMMENT",
DASHBOARD_ACTION_MENU: "DASHBOARD_ACTION_MENU",
FULLSCREEN_ANALYTICS_DASHBOARD: "FULLSCREEN_ANALYTICS_DASHBOARD",
} as const;
@@ -151,6 +153,10 @@ export const dashboardActionButtons: Record<
component: DashboardInfoButton,
enabled: ({ isEditing }) => !isEditing,
},
[DASHBOARD_ACTION.DASHBOARD_COMMENT]: {
component: DashboardCommentButton,
enabled: ({ isEditing }) => !isEditing,
},
[DASHBOARD_ACTION.DASHBOARD_ACTION_MENU]: {
component: withRouter<HeaderButtonProps>(
({

View File

@@ -30,6 +30,7 @@ export const DASHBOARD_VIEW_ACTIONS = [
...DASHBOARD_DISPLAY_ACTIONS,
DASHBOARD_ACTION.DASHBOARD_HEADER_ACTION_DIVIDER,
DASHBOARD_ACTION.DASHBOARD_BOOKMARK,
DASHBOARD_ACTION.DASHBOARD_COMMENT,
DASHBOARD_ACTION.DASHBOARD_INFO,
DASHBOARD_ACTION.DASHBOARD_ACTION_MENU,
DASHBOARD_ACTION.FULLSCREEN_ANALYTICS_DASHBOARD,

View File

@@ -0,0 +1,28 @@
import { t } from "ttag";
import { ToolbarButton } from "metabase/components/ToolbarButton";
import { closeSidebar, setSidebar } from "metabase/dashboard/actions";
import { SIDEBAR_NAME } from "metabase/dashboard/constants";
import { getIsShowDashboardInfoSidebar } from "metabase/dashboard/selectors";
import { useDispatch, useSelector } from "metabase/lib/redux";
export const DashboardCommentButton = () => {
const dispatch = useDispatch();
const isShowingDashboardInfoSidebar = useSelector(
getIsShowDashboardInfoSidebar,
);
return (
<ToolbarButton
aria-label={t`Comments`}
tooltipLabel={t`Comments`}
icon="comment"
isActive={isShowingDashboardInfoSidebar}
onClick={() =>
isShowingDashboardInfoSidebar
? dispatch(closeSidebar())
: dispatch(setSidebar({ name: SIDEBAR_NAME.info }))
}
/>
);
};

View File

@@ -8,6 +8,7 @@ export * from "./CancelEditButton";
export * from "./CopyAnalyticsDashboardButton";
export * from "./DashboardActionMenu";
export * from "./DashboardInfoButton";
export * from "./DashboardCommentButton";
export * from "./EditDashboardButton";
export * from "./FullscreenAnalyticsDashboard";
export * from "./SaveEditButton";

View File

@@ -4,6 +4,7 @@ import { useMount } from "react-use";
import { t } from "ttag";
import ErrorBoundary from "metabase/ErrorBoundary";
import { CommentFeed } from "metabase/comments";
import {
Sidesheet,
SidesheetCard,
@@ -47,6 +48,7 @@ interface DashboardInfoSidebarProps {
enum Tab {
Overview = "overview",
History = "history",
Comments = "comments",
}
export function DashboardInfoSidebar({
@@ -120,11 +122,12 @@ export function DashboardInfoSidebar({
size="md"
>
<Tabs
defaultValue={Tab.Overview}
defaultValue={Tab.Comments}
className={SidesheetS.FlexScrollContainer}
>
<Tabs.List mx="lg">
<Tabs.Tab value={Tab.Overview}>{t`Overview`}</Tabs.Tab>
<Tabs.Tab value={Tab.Comments}>{t`Comments`}</Tabs.Tab>
<Tabs.Tab value={Tab.History}>{t`History`}</Tabs.Tab>
</Tabs.List>
<SidesheetTabPanelContainer>
@@ -140,6 +143,13 @@ export function DashboardInfoSidebar({
showCaching={showCaching}
/>
</Tabs.Panel>
<Tabs.Panel value={Tab.Comments}>
<CommentFeed
model="dashboard"
modelId={dashboard.id}
autoScroll
/>
</Tabs.Panel>
<Tabs.Panel value={Tab.History}>
<HistoryTab
canWrite={canWrite}

View File

@@ -1,25 +1,25 @@
import { useMemo } from "react";
import { useListPopularItemsQuery, useListRecentsQuery } from "metabase/api";
import { useDatabaseListQuery, useSetting } from "metabase/common/hooks";
import { useDatabaseListQuery } from "metabase/common/hooks";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import { useSelector } from "metabase/lib/redux";
import { isSyncCompleted } from "metabase/lib/syncing";
// import { isSyncCompleted } from "metabase/lib/syncing";
import { getUser } from "metabase/selectors/user";
import type Database from "metabase-lib/v1/metadata/Database";
import type { PopularItem, RecentItem, User } from "metabase-types/api";
import { getIsXrayEnabled } from "../../selectors";
// import { getIsXrayEnabled } from "../../selectors";
import { isWithinWeeks } from "../../utils";
import { EmbedHomepage } from "../EmbedHomepage";
import { HomePopularSection } from "../HomePopularSection";
// import { EmbedHomepage } from "../EmbedHomepage";
// import { HomePopularSection } from "../HomePopularSection";
import { HomeRecentSection, recentsFilter } from "../HomeRecentSection";
import { HomeXraySection } from "../HomeXraySection";
// import { HomeXraySection } from "../HomeXraySection";
export const HomeContent = (): JSX.Element | null => {
const user = useSelector(getUser);
const embeddingHomepage = useSetting("embedding-homepage");
const isXrayEnabled = useSelector(getIsXrayEnabled);
// const embeddingHomepage = useSetting("embedding-homepage");
// const isXrayEnabled = useSelector(getIsXrayEnabled);
const { data: databases, error: databasesError } = useDatabaseListQuery();
const { data: recentItemsRaw, error: recentItemsError } = useListRecentsQuery(
undefined,
@@ -42,21 +42,21 @@ export const HomeContent = (): JSX.Element | null => {
return <LoadingAndErrorWrapper loading />;
}
if (embeddingHomepage === "visible" && user.is_superuser) {
return <EmbedHomepage />;
}
// if (embeddingHomepage === "visible" && user.is_superuser) {
// return <EmbedHomepage />;
// }
if (isPopularSection(user, recentItems, popularItems)) {
return <HomePopularSection />;
}
// if (isPopularSection(user, recentItems, popularItems)) {
// return <HomePopularSection />;
// }
if (isRecentSection(user, recentItems)) {
return <HomeRecentSection />;
}
if (isXraySection(databases, isXrayEnabled)) {
return <HomeXraySection />;
}
// if (isXraySection(databases, isXrayEnabled)) {
// return <HomeXraySection />;
// }
return null;
};
@@ -76,18 +76,18 @@ const isLoading = (
}
};
const isPopularSection = (
user: User,
recentItems: RecentItem[] = [],
popularItems: PopularItem[] = [],
): boolean => {
return (
!user.is_installer &&
user.has_question_and_dashboard &&
popularItems.length > 0 &&
(isWithinWeeks(user.first_login, 1) || !recentItems.length)
);
};
// const isPopularSection = (
// user: User,
// recentItems: RecentItem[] = [],
// popularItems: PopularItem[] = [],
// ): boolean => {
// return (
// !user.is_installer &&
// user.has_question_and_dashboard &&
// popularItems.length > 0 &&
// (isWithinWeeks(user.first_login, 1) || !recentItems.length)
// );
// };
const isRecentSection = (
user: User,
@@ -96,9 +96,9 @@ const isRecentSection = (
return user.has_question_and_dashboard && recentItems.length > 0;
};
const isXraySection = (
databases: Database[] = [],
isXrayEnabled: boolean,
): boolean => {
return databases.some(isSyncCompleted) && isXrayEnabled;
};
// const isXraySection = (
// databases: Database[] = [],
// isXrayEnabled: boolean,
// ): boolean => {
// return databases.some(isSyncCompleted) && isXrayEnabled;
// };

View File

@@ -1,12 +1,14 @@
import { t } from "ttag";
import { useListRecentsQuery } from "metabase/api";
import { CommentFeed } from "metabase/comments";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import { getIcon } from "metabase/lib/icon";
import { getName } from "metabase/lib/name";
import { useSelector } from "metabase/lib/redux";
import * as Urls from "metabase/lib/urls";
import { getUser } from "metabase/selectors/user";
import { Flex } from "metabase/ui";
import type { RecentItem } from "metabase-types/api";
import { isWithinWeeks } from "../../utils";
@@ -27,20 +29,28 @@ export const HomeRecentSection = () => {
}
return (
<div>
<HomeCaption>{t`Pick up where you left off`}</HomeCaption>
<SectionBody>
{recentsFilter(recentItems).map((item, index) => (
<HomeModelCard
key={index}
title={getName(item)}
icon={getIcon(item)}
url={Urls.modelToUrl(item) ?? ""}
/>
))}
{hasHelpCard && <HomeHelpCard />}
</SectionBody>
</div>
<Flex gap="lg">
<div style={{ width: "50%" }}>
<HomeCaption>{t`Pick up where you left off`}</HomeCaption>
<SectionBody>
{recentsFilter(recentItems).map((item, index) => (
<HomeModelCard
key={index}
title={getName(item)}
icon={getIcon(item)}
url={Urls.modelToUrl(item) ?? ""}
/>
))}
{hasHelpCard && <HomeHelpCard />}
</SectionBody>
</div>
<div style={{ width: "50%" }}>
<HomeCaption>{t`What people are discussing`}</HomeCaption>
<div style={{ overflow: "auto", maxHeight: "20rem" }}>
<CommentFeed shadowed />
</div>
</div>
</Flex>
);
};

View File

@@ -0,0 +1,12 @@
.AppBarButton {
position: relative;
&:hover {
background-color: var(--mb-color-brand);
color: white;
}
cursor: pointer;
padding: 0.5rem 0.6rem;
border-radius: 0.25rem;
}

View File

@@ -1,5 +1,11 @@
import { useState } from "react";
import { usePrevious } from "react-use";
import { t } from "ttag";
// import { useCurrentUser } from "embedding-sdk";
import { useListCommentQuery } from "metabase/api";
import { CommentFeed } from "metabase/comments";
import { Box, Icon, IconBadgeDotThingy, Popover } from "metabase/ui";
import type { CollectionId } from "metabase-types/api";
import CollectionBreadcrumbs from "../../containers/CollectionBreadcrumbs";
@@ -9,6 +15,7 @@ import { ProfileLink } from "../ProfileLink";
import { SearchBar } from "../search/SearchBar";
import { SearchButton } from "../search/SearchButton";
import Styles from "./AppBar.module.css";
import {
AppBarInfoContainer,
AppBarLeftContainer,
@@ -49,6 +56,22 @@ const AppBarLarge = ({
onLogout,
}: AppBarLargeProps): JSX.Element => {
const isNavBarVisible = isNavBarOpen && isNavBarEnabled;
const [hasNewNotifications, setHasNewNotifications] = useState(false);
const { data: comments } = useListCommentQuery(undefined, {
pollingInterval: 2000,
});
const previousComments = usePrevious(comments);
if (
!hasNewNotifications &&
comments?.length &&
previousComments?.length &&
comments.length > previousComments.length
) {
setHasNewNotifications(true);
}
return (
<AppBarRoot isNavBarOpen={isNavBarVisible}>
@@ -76,6 +99,27 @@ const AppBarLarge = ({
<AppBarRightContainer>
{isSearchVisible && (isEmbedded ? <SearchBar /> : <SearchButton />)}
{isNewButtonVisible && <NewItemButton collectionId={collectionId} />}
<Popover
width="20rem"
position="bottom-end"
onOpen={() => setHasNewNotifications(false)}
>
<Popover.Target>
<div className={Styles.AppBarButton}>
<Icon name="bell" onClick={onToggleNavbar} />
<IconBadgeDotThingy
color="var(--mb-color-danger)"
active={hasNewNotifications}
/>
</div>
</Popover.Target>
<Popover.Dropdown>
<Box w="30rem" p="lg" mah="30rem">
<CommentFeed />
<div style={{ height: "1.5rem" }} />
</Box>
</Popover.Dropdown>
</Popover>
{isProfileLinkVisible && (
<AppBarProfileLinkContainer aria-label={t`Settings menu`}>
<ProfileLink onLogout={onLogout} />

View File

@@ -309,7 +309,7 @@ class View extends Component {
const legacyQuery = question.legacyQuery();
// Normally, when users open native models,
// Normally, when users open native models
// they open an ad-hoc GUI question using the model as a data source
// (using the `/dataset` endpoint instead of the `/card/:id/query`)
// However, users without data permission open a real model as they can't use the `/dataset` endpoint

View File

@@ -275,6 +275,18 @@ export const QuestionActions = ({
/>
</ViewHeaderIconButtonContainer>
)}
<Tooltip tooltip={t`Comments`}>
<ViewHeaderIconButtonContainer>
<Button
onlyIcon
icon="comment"
iconSize={HEADER_ICON_SIZE}
onClick={onInfoClick}
color={infoButtonColor}
data-testid="qb-header-comment-button"
/>
</ViewHeaderIconButtonContainer>
</Tooltip>
<Tooltip tooltip={t`More info`}>
<ViewHeaderIconButtonContainer>
<Button

View File

@@ -2,6 +2,7 @@ import { useState } from "react";
import { useMount } from "react-use";
import { t } from "ttag";
import { CommentFeed } from "metabase/comments";
import {
Sidesheet,
SidesheetCard,
@@ -61,11 +62,12 @@ export const QuestionInfoSidebar = ({
data-testid="question-info-sidebar"
>
<Tabs
defaultValue="overview"
defaultValue="comments"
className={SidesheetStyles.FlexScrollContainer}
>
<Tabs.List mx="lg">
<Tabs.Tab value="overview">{t`Overview`}</Tabs.Tab>
<Tabs.Tab value="comments">{t`Comments`}</Tabs.Tab>
<Tabs.Tab value="history">{t`History`}</Tabs.Tab>
</Tabs.List>
<SidesheetTabPanelContainer>
@@ -101,6 +103,9 @@ export const QuestionInfoSidebar = ({
<EntityIdCard entityId={question._card.entity_id} />
</Stack>
</Tabs.Panel>
<Tabs.Panel value="comments">
<CommentFeed model="card" modelId={question.id()} autoScroll />
</Tabs.Panel>
<Tabs.Panel value="history">
<SidesheetCard>
<QuestionActivityTimeline question={question} />

View File

@@ -1,3 +1,4 @@
export * from "./Accordion";
export * from "./Card";
export * from "./Image";
export { Badge, type BadgeProps } from "@mantine/core";

View File

@@ -4,6 +4,8 @@ import cx from "classnames";
import type { MouseEvent, ReactNode, SVGAttributes } from "react";
import { forwardRef } from "react";
import { Box } from "metabase/ui";
import { Tooltip } from "../../overlays/Tooltip";
import type { IconName } from "./icons";
@@ -47,3 +49,24 @@ export const Icon = forwardRef<SVGSVGElement, IconProps>(function Icon(
export const FixedSizeIcon = styled(Icon)`
flex-shrink: 0;
`;
export const IconBadgeDotThingy = ({
color,
active,
}: {
color: string;
active: boolean;
}) => (
<Box
style={{
position: "absolute",
opacity: active ? 1 : 0,
top: 4,
right: 4,
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: color,
}}
></Box>
);

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 13 14" fill="currentColor" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4812 13.3455L12.4811 13.3458C12.4692 13.3747 12.4514 13.4021 12.4268 13.4267C12.4056 13.4479 12.3823 13.464 12.3581 13.4757C11.9069 13.1767 11.5609 12.8548 11.1891 12.5091C11.0555 12.3848 10.9185 12.2574 10.7721 12.1268L10.7554 12.1119L10.7373 12.0985L9.83585 11.4294C9.95615 11.3212 10.0553 11.1899 10.1264 11.0425L11.202 11.8408L12 12.433V11.4393V5.225C12.0808 5.24139 12.1644 5.25 12.25 5.25C12.3356 5.25 12.4192 5.24139 12.5 5.225V13.25C12.5 13.2849 12.4931 13.3168 12.4812 13.3455ZM9 10.75H8.92039L8.83546 10.687C8.75635 10.5888 8.76238 10.4447 8.85355 10.3536C8.94605 10.2611 9.09301 10.2562 9.19123 10.339C9.2279 10.3825 9.25 10.4387 9.25 10.5C9.25 10.6381 9.13806 10.75 9 10.75ZM7.812 10.25C7.77439 10.4142 7.77028 10.5845 7.79967 10.75H2.25C1.55964 10.75 1 10.1904 1 9.5L1 2.5C1 1.80964 1.55964 1.25 2.25 1.25L11.25 1.25C11.9404 1.25 12.5 1.80964 12.5 2.5V2.77501C12.4192 2.75861 12.3356 2.75 12.25 2.75C12.1644 2.75 12.0808 2.75861 12 2.77501V2.5C12 2.08579 11.6642 1.75 11.25 1.75L2.25 1.75C1.83572 1.75 1.5 2.08583 1.5 2.5L1.5 9.5C1.5 9.91424 1.83576 10.25 2.25 10.25H7.812ZM12.5 4C12.5 4.13807 12.3881 4.25 12.25 4.25C12.1119 4.25 12 4.13807 12 4C12 3.86193 12.1119 3.75 12.25 3.75C12.3881 3.75 12.5 3.86193 12.5 4Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -91,6 +91,8 @@ import cloud_filled_component from "./cloud_filled.svg?component";
import cloud_filled_source from "./cloud_filled.svg?source";
import combine_component from "./combine.svg?component";
import combine_source from "./combine.svg?source";
import comment_component from "./comment.svg?component";
import comment_source from "./comment.svg?source";
import compare_component from "./compare.svg?component";
import compare_source from "./compare.svg?source";
import connections_component from "./connections.svg?component";
@@ -577,6 +579,10 @@ export const Icons = {
component: combine_component,
source: combine_source,
},
comment: {
component: comment_component,
source: comment_source,
},
connections: {
component: connections_component,
source: connections_source,

View File

@@ -9,3 +9,4 @@ export * from "./overlays";
export * from "./theme";
export * from "./typography";
export * from "./utils";
export {Avatar} from "@mantine/core"

View File

@@ -13,6 +13,7 @@
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^8.0.0",
"@emoji-mart/data": "^1.2.1",
"@emotion/cache": "^11.11.0",
"@emotion/is-prop-valid": "^1.1.1",
"@emotion/react": "^11.11.0",
@@ -50,6 +51,7 @@
"detect-package-manager": "^3.0.2",
"diff": "^3.2.0",
"echarts": "^5.5.1-rc.1",
"emoji-mart": "^5.6.0",
"fast-text-encoding": "^1.0.6",
"formik": "^2.4.5",
"hast-util-from-html": "^2.0.1",
@@ -94,6 +96,7 @@
"react-innertext": "^1.1.5",
"react-is": "^18.2.0",
"react-markdown": "^8.0.4",
"react-mentions": "^4.4.10",
"react-redux": "^9.1.2",
"react-resizable": "^3.0.5",
"react-router": "3",
@@ -204,6 +207,7 @@
"@types/react-dom": "^18.2.17",
"@types/react-grid-layout": "^1.1.3",
"@types/react-is": "^18.2.4",
"@types/react-mentions": "^4.4.0",
"@types/react-resizable": "^3.0.4",
"@types/react-router-redux": "^4.0.53",
"@types/react-textarea-autosize": "^4.3.6",

View File

@@ -9288,6 +9288,179 @@ databaseChangeLog:
name: type
indexName: idx_collection_type
- changeSet:
id: v51.2024-09-18T09:29:12
author: tsmacdonald
comment: Add the metabase_comment table
preConditions:
- onFail: MARK_RAN
- not:
- tableExists:
tableName: metabase_comment
changes:
- createTable:
tableName: metabase_comment
remarks: 'Comments. For Metabase!'
columns:
- column:
name: id
remarks: PK
type: int
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: text
type: ${text.type}
remarks: Comment body
- column:
name: active
type: ${boolean.type}
defaultValueBoolean: false
remarks: Is the comment resolved
constraints:
nullable: false
- column:
name: model_id
type: int
remarks: 'The card/dashboard/etc the comment is attached to'
constraints:
nullable: false
- column:
name: model
type: varchar(16)
remarks: 'The type of model that the comment is attached to'
constraints:
nullable: false
- column:
name: author_id
type: int
remarks: 'The author ID'
constraints:
nullable: false
referencedTableName: core_user
referencedColumnNames: id
foreignKeyName: fk_metabase_comment_user_id
- column:
remarks: The timestamp of when the comment was created
name: created_at
type: ${timestamp_type}
defaultValueComputed: current_timestamp
constraints:
nullable: false
- column:
remarks: The timestamp of when the comment was updated
name: updated_at
type: ${timestamp_type}
defaultValueComputed: current_timestamp
constraints:
nullable: false
- changeSet:
id: v51.2024-09-18T11:29:12
author: tsmacdonald
comment: Rename metabase_comment.active to .resolved because I'm tonto
changes:
- renameColumn:
tableName: metabase_comment
columnDataType: ${boolean.type}
oldColumnName: active
newColumnName: resolved
- changeSet:
id: v51.2024-09-18T12:01:12
author: tsmacdonald
comment: Add user avatars!
preConditions:
- not:
- columnExists:
tableName: core_user
columnName: avatar
changes:
- addColumn:
tableName: core_user
columns:
- column:
name: avatar
type: ${text.type}
remarks: Base64-encoded avatar image
- changeSet:
id: v51.2024-09-18T12:15:12
author: tsmacdonald
comment: Add the metabase_comment_reaction table
preConditions:
- onFail: MARK_RAN
- not:
- tableExists:
tableName: metabase_comment_reaction
changes:
- createTable:
tableName: metabase_comment_reaction
remarks: 'Emoji. For Metabase!'
columns:
- column:
name: id
remarks: PK
type: int
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: emoji
type: varchar(4)
remarks: The actual emoji
- column:
name: author_id
type: int
remarks: 'The reactor. Not nuclear.'
constraints:
nullable: false
referencedTableName: core_user
referencedColumnNames: id
foreignKeyName: fk_metabase_comment_reaction_user_id
- column:
name: comment_id
type: int
remarks: 'What do you think?'
constraints:
nullable: false
referencedTableName: metabase_comment
referencedColumnNames: id
foreignKeyName: fk_metabase_comment_reaction_comment_id
- column:
remarks: The timestamp of when the reaction was reacted
name: created_at
type: ${timestamp_type}
defaultValueComputed: current_timestamp
constraints:
nullable: false
- changeSet:
id: v51.2024-09-18T15:56:12
author: tsmacdonald
comment: Add metabase_comment.resolved_by_id
preConditions:
- not:
- columnExists:
tableName: metabase_comment
columnName: resolved_by_id
changes:
- addColumn:
tableName: metabase_comment
columns:
- column:
name: resolved_by_id
type: int
remarks: 'As it sounds'
constraints:
nullable: true
referencedTableName: core_user
referencedColumnNames: id
foreignKeyName: fk_metabase_comment_resolved_by_id
# >>>>>>>>>> DO NOT ADD NEW MIGRATIONS BELOW THIS LINE! ADD THEM ABOVE <<<<<<<<<<
########################################################################################################################

View File

@@ -0,0 +1,69 @@
(ns metabase.api.comment
"/api/comment endpoints!"
(:require
[compojure.core :refer [GET POST PUT]]
[metabase.api.common :as api]
[metabase.models.comment :as comment]
[metabase.models.reaction :as reaction]
[metabase.util :as u]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(set! *warn-on-reflection* true)
(def ^:private commentable-model-schema "As it says"
(into [:enum] comment/commentable-models))
(api/defendpoint GET "/"
"Get juicy things"
[model model_id user_id]
{model [:maybe commentable-model-schema]
model_id [:maybe ms/PositiveInt]
user_id [:maybe ms/PositiveInt]}
(cond
model (do
(api/checkp (integer? model_id) "model_id" "model_id is required when specifying a model")
(api/checkp (nil? user_id) "user_id" "When specifying a model, do not provide user_id")
(comment/for-model model model_id))
user_id (comment/user-notifications api/*current-user-id*)
:else (comment/all)))
(api/defendpoint POST "/"
"Create a comment"
[:as {{:keys [model model_id text]} :body}]
{model commentable-model-schema
model_id ms/PositiveInt
text ms/NonBlankString}
(comment/create! {:model model
:model_id model_id
:text text
:author_id api/*current-user-id*}))
(api/defendpoint PUT "/:id"
"Update a comment (resolve; edit)"
[id :as {{:keys [resolved text]} :body}]
{id ms/PositiveInt
resolved [:maybe :boolean]
text [:maybe ms/NonBlankString]}
(let [comment-updates {:resolved resolved
:text text
:resolved_by_id (when resolved api/*current-user-id*)}]
(comment/update! id (u/select-non-nil-keys comment-updates [:resolved :text :resolved_by_id]))))
(api/defendpoint POST "/:comment-id/react"
"Create a comment"
[comment-id :as {{:keys [emoji]} :body}]
{comment-id ms/PositiveInt
emoji ms/NonBlankString}
(reaction/create! {:comment_id comment-id
:emoji emoji
:author_id api/*current-user-id*}))
(api/defendpoint GET "/my-name-is-bruno-and-i-am-drunk-with-power"
"Deletes all comments"
[]
{}
(t2/delete! :model/Comment)
{:status "I am become death, destroyer of worlds"})
(api/define-routes)

View File

@@ -13,6 +13,7 @@
[metabase.api.channel :as api.channel]
[metabase.api.cloud-migration :as api.cloud-migration]
[metabase.api.collection :as api.collection]
[metabase.api.comment :as api.comment]
[metabase.api.common :as api :refer [defroutes context]]
[metabase.api.dashboard :as api.dashboard]
[metabase.api.database :as api.database]
@@ -110,6 +111,7 @@
(context "/card" [] (+auth api.card/routes))
(context "/cloud-migration" [] (+auth api.cloud-migration/routes))
(context "/collection" [] (+auth api.collection/routes))
(context "/comment" [] (+auth api.comment/routes))
(context "/channel" [] (+auth api.channel/routes))
(context "/dashboard" [] (+auth api.dashboard/routes))
(context "/database" [] (+auth api.database/routes))

View File

@@ -411,9 +411,10 @@
"Update an existing, active `User`.
Self or superusers can update user info and groups.
Group Managers can only add/remove users from groups they are manager of."
[id :as {{:keys [email first_name last_name user_group_memberships
[id :as {{:keys [avatar email first_name last_name user_group_memberships
is_superuser is_group_manager login_attributes locale] :as body} :body}]
{id ms/PositiveInt
avatar [:maybe ms/NonBlankString]
email [:maybe ms/Email]
first_name [:maybe ms/NonBlankString]
last_name [:maybe ms/NonBlankString]
@@ -450,7 +451,7 @@
(u/select-keys-when body
:present (cond-> #{:first_name :last_name :locale}
api/*is-superuser?* (conj :login_attributes))
:non-nil (cond-> #{:email}
:non-nil (cond-> #{:email :avatar}
api/*is-superuser?* (conj :is_superuser))))]
(t2/update! :model/User id changes)
(events/publish-event! :event/user-update {:object (t2/select-one :model/User :id id)

View File

@@ -0,0 +1,25 @@
{{> metabase/email/_header }}
<div class="card-container" style="color: {{colorTextMedium}};">
<p style="font-weight: 500; line-height: 1.375em; color: {{colorTextMedium}};">
Hi {{recipientFirstName}},
</p>
<p style="font-weight: 500; line-height: 1.375em; color: {{colorTextMedium}};">
{{commentAuthorName}} ({{commentAuthorEmail}}) commented on
<a href="{{modelURL}}" style="color:{{applicationColor}}">{{modelTitle}}:</a>
</p>
<p style="font-weight:600;line-height:1.375em;color:{{colorTextMedium}};border-left: 2px solid {{colorTextMedium}};padding: 0.5em 0 0.5em 1em;">
{{commentBody}}
</p>
<div>
<a
href="{{replyLink}}"
style="display: inline-block; padding: 0.75rem 1.125em; background-color: {{applicationColor}}; color: #FFF; border-radius: 6px; font-size: 0.875em; font-weight: 700; text-decoration: none;"
>
Reply
</a>
</div>
</div>
{{> metabase/email/_footer }}

View File

@@ -0,0 +1,25 @@
{{> metabase/email/_header }}
<div class="card-container" style="color: {{colorTextMedium}};">
<p style="font-weight: 500; line-height: 1.375em; color: {{colorTextMedium}};">
Hi {{recipientFirstName}},
</p>
<p style="font-weight: 500; line-height: 1.375em; color: {{colorTextMedium}};">
{{commentAuthorName}} ({{commentAuthorEmail}}) replied to your comment on
<a href="{{modelURL}}" style="color:{{applicationColor}}">{{modelTitle}}:</a>
</p>
<p style="font-weight:600;line-height:1.375em;color:{{colorTextMedium}};border-left: 2px solid {{colorTextMedium}};padding: 0.5em 0 0.5em 1em;">
{{commentBody}}
</p>
<div>
<a
href="{{replyLink}}"
style="display: inline-block; padding: 0.75rem 1.125em; background-color: {{applicationColor}}; color: #FFF; border-radius: 6px; font-size: 0.875em; font-weight: 700; text-decoration: none;"
>
Reply
</a>
</div>
</div>
{{> metabase/email/_footer }}

View File

@@ -127,10 +127,51 @@
:sentFromSetup sent-from-setup?}))]
(email/send-message!
{:subject (str (trs "You''re invited to join {0}''s {1}" company (app-name-trs)))
:recipients [(:email invited)]
:recipients [(:email invited)];; (t2/select-fn-vec :email ... user-ids
:message-type :html
:message message-body})))
(defn- comment-url-bit
[comment]
(condp = (:model comment)
"card" (format "card/%d" (:model_id comment))
"dashboard" (format "dashboard/%d" (:model_id comment))
"comment" (recur (t2/select-one :model/Comment :id (:model_id comment)))))
(defn- model-title
[comment]
(condp = (:model comment)
"card" (t2/select-one-fn :name :model/Card :id (:model_id comment))
"dashboard" (t2/select-one-fn :name :model/Dashboard :id (:model_id comment))
"comment" (recur (t2/select-one :model/Comment :id (:model_id comment)))))
(defn send-comment-notification!
"Send an email to `users` letting them know that there's a new comment"
[users comment]
(let [full-comment (t2/hydrate comment :author)
reply? (= "comment" (:model comment))
author (:author full-comment)
model-url (str (public-settings/site-url) "/" (comment-url-bit comment))
the-model-title (model-title comment)
subject (format "New %s on %s" (if reply? "reply" "comment") the-model-title)]
(doseq [user users]
(let [message-body (stencil/render-file (str "metabase/email/" (if reply?
"comment_reply"
"comment_notification"))
(merge (common-context)
{:recipientFirstName (:first_name user)
:commentAuthorName (:common_name author)
:commentAuthorEmail (:email author)
:modelURL model-url
:modelTitle the-model-title
:replyLink model-url
:commentBody (:text comment)}))]
(email/send-message!
{:subject subject
:recipients [(:email user)]
:message-type :html
:message message-body})))))
(defn- all-admin-recipients
"Return a sequence of email addresses for all Admin users.

View File

@@ -0,0 +1,98 @@
(ns metabase.models.comment
(:require
[metabase.email.messages :as messages]
[metabase.models.interface :as mi]
[metabase.util :as u]
[methodical.core :as methodical]
[toucan2.core :as t2]))
(methodical/defmethod t2/table-name :model/Comment [_model] :metabase_comment)
(doto :model/Comment
(derive :metabase/model))
(declare send-notifications!)
(t2/define-after-insert :model/Comment
[comment]
(u/prog1 comment
(send-notifications! comment)))
(methodical/defmethod t2/batched-hydrate [:default :reactions]
[_model k comments]
(mi/instances-with-hydrated-data
comments k
#(group-by :comment_id
(t2/select :model/Reaction :comment_id [:in (map :id comments)]))
:id
{:default []}))
(methodical/defmethod t2/batched-hydrate [:default :replies]
[_model k comments]
(mi/instances-with-hydrated-data
comments k
#(group-by :model_id
(t2/select :model/Comment :model "comment" :model_id [:in (map :id comments)]))
:id
{:default []}))
(def commentable-models "Set of models that allow comments"
#{"card"
"comment"
"dashboard"})
(defn- hydrate-comment
[comment-or-comments]
(t2/hydrate comment-or-comments :author [:reactions :author] [:replies :author] [:replies :reactions] [:replies [:reactions :author]] :resolved_by))
(defn for-model
"All the comments for the given model"
[model-type model-id]
(hydrate-comment
(t2/select :model/Comment :model model-type :model_id model-id {:order-by [[:created_at :asc]]})))
(defn user-notifications
"All the comments that a given user should care about"
[user-or-id]
(let [user-id (u/the-id user-or-id)
model-pairs (distinct (t2/select-fn-vec (juxt :model_id :model) :model/Comment :author_id user-id))
model-clause (into [:or] (map (fn [[m-id m]] [:and
[:= :model_id m-id]
[:= :model m]]) model-pairs))
where-clause [:and
[:not= :author_id user-id]
model-clause]]
(hydrate-comment
(t2/select :model/Comment {:where where-clause
:order-by [[:created_at :desc]]}))))
(defn- send-notifications!
[comment]
(let [user-ids (t2/select-fn-set :author_id :model/Comment {:where [:or
[:and
[:= :model_id (:model_id comment)]
[:= :model (:model comment)]]
[:and
[:= :id (:model_id comment)]
[:= :model "comment"]]]})
users (t2/select :model/User :id [:in user-ids])]
(messages/send-comment-notification! users comment)))
(defn all
"All top-level comments"
[]
(hydrate-comment
(t2/select :model/Comment :model [:not= "comment"] {:order-by [[:created_at :desc]]})))
(defn create!
"make the thing"
[params-map]
(hydrate-comment
(t2/insert-returning-instance! :model/Comment params-map)))
(defn update!
"edit the thing"
[comment-id comment-updates]
(hydrate-comment
(t2/select-one :model/Comment
:id (first (t2/update-returning-pks! :model/Comment comment-id comment-updates)))))

View File

@@ -0,0 +1,22 @@
(ns metabase.models.reaction
(:require
[metabase.util :as u]
[methodical.core :as methodical]
[toucan2.core :as t2]))
(methodical/defmethod t2/table-name :model/Reaction [_model] :metabase_comment_reaction)
(doto :model/Reaction
(derive :metabase/model))
(defn for-comment
"All the reactions for a given comment"
[comment-or-id]
(t2/hydrate
(t2/select :model/Reaction :comment_id (u/the-id comment-or-id) {:order-by [[:created_at :asc]]})
:author))
(defn create!
"make the thing"
[params-map]
(t2/insert-returning-instance! :model/Reaction params-map))

View File

@@ -43,10 +43,11 @@
:model/User)
(methodical/defmethod t2/table-name :model/User [_model] :core_user)
(methodical/defmethod t2/model-for-automagic-hydration [:default :author] [_original-model _k] :model/User)
(methodical/defmethod t2/model-for-automagic-hydration [:default :creator] [_original-model _k] :model/User)
(methodical/defmethod t2/model-for-automagic-hydration [:default :updated_by] [_original-model _k] :model/User)
(methodical/defmethod t2/model-for-automagic-hydration [:default :user] [_original-model _k] :model/User)
(methodical/defmethod t2/model-for-automagic-hydration [:default :author] [_original-model _k] :model/User)
(methodical/defmethod t2/model-for-automagic-hydration [:default :creator] [_original-model _k] :model/User)
(methodical/defmethod t2/model-for-automagic-hydration [:default :updated_by] [_original-model _k] :model/User)
(methodical/defmethod t2/model-for-automagic-hydration [:default :resolved_by] [_original-model _k] :model/User)
(methodical/defmethod t2/model-for-automagic-hydration [:default :user] [_original-model _k] :model/User)
(doto :model/User
(derive :metabase/model)
@@ -204,7 +205,7 @@
(def ^:private default-user-columns
"Sequence of columns that are normally returned when fetching a User from the DB."
[:id :email :date_joined :first_name :last_name :last_login :is_superuser :is_qbnewb])
[:id :avatar :email :date_joined :first_name :last_name :last_login :is_superuser :is_qbnewb])
(def admin-or-self-visible-columns
"Sequence of columns that we can/should return for admins fetching a list of all Users, or for the current user

View File

@@ -0,0 +1,96 @@
(ns metabase.api.comment-test
(:require
[clojure.test :refer :all]
[metabase.api.comment :as api.comment]
[metabase.test :as mt]
[toucan2.core :as t2]))
(set! *warn-on-reflection* true)
(comment api.comment/keep-me)
(deftest get-all-test
(testing "GET /api/comment"
(mt/with-temp
[:model/Card {card-id :id} {}
:model/Comment {c1-id :id} {:model "card" :model_id card-id :text "first!"}
:model/Comment {c2-id :id} {:model "card" :model_id card-id :text "first!! Edit: dangit"}
:model/Comment {c3-id :id} {:model "card" :model_id card-id :text "Here is some actual insight..."}]
(let [result (mt/user-http-request :rasta :get 200 "comment")]
(is (= [c3-id c2-id c1-id] (filter #{c3-id c2-id c1-id} (map :id result))))
(is (=? {:model "card" :text "Here is some actual insight..." :model_id card-id :author {:first_name "Rasta"}}
(first result)))))))
(deftest get-for-model-test
(testing "GET /api/comment/model=x&model_id=42"
(mt/with-temp
[:model/Card {card-id :id} {}
:model/Card {card2-id :id} {}
:model/Comment {c1-id :id} {:model "card" :model_id card-id :text "first!"}
:model/Comment {c2-id :id} {:model "card" :model_id card-id :text "first!! Edit: dangit"}
:model/Comment {c3-id :id} {:model "card" :model_id card-id :text "Here is some actual insight..."}
:model/Comment {c4-id :id} {:model "card" :model_id card2-id :text "Something else"}]
(let [result (mt/user-http-request :rasta :get 200 (format "comment?model=card&model_id=%d" card-id))]
(is (= [c1-id c2-id c3-id] (filter #{c4-id c3-id c2-id c1-id} (map :id result))))
(is (=? {:model "card" :text "first!" :model_id card-id :author {:first_name "Rasta"}}
(first result))))))
(testing "Comments with replies"
(mt/with-temp
[:model/Card {card-id :id} {}
:model/Comment {c1-id :id} {:model "card" :model_id card-id :text "first!"}
:model/Comment {c2-id :id} {:model "comment" :model_id c1-id :text "A reply"}
:model/Comment {c3-id :id} {:model "card" :model_id card-id :text "Here is some actual insight..."}
:model/Comment {c4-id :id} {:model "comment" :model_id c1-id :text "Another reply"}]
(let [result (mt/user-http-request :rasta :get 200 (format "comment?model=card&model_id=%d" card-id))]
(is (=?
[{:id c1-id
:replies [{:id c2-id}
{:id c4-id}]}
{:id c3-id
:replies []}]
result))))))
(deftest comment-creation-test
(testing "POST /api/comment"
(mt/with-temp [:model/Card {card-id :id} {}]
(let [new-comment {:model "card" :model_id card-id :text "Oh-SHEEN"}
result (mt/user-http-request :rasta :post 200 "comment" new-comment)]
(t2/delete! :model/Comment :id (:id result))
(is (=? (assoc new-comment :author {:first_name "Rasta"})
result))))))
(deftest comment-resolution-test
(testing "PUT /api/comment resolved"
(mt/with-temp
[:model/Card {card-id :id} {}
:model/Comment {comment-id :id} {:model "card" :model_id card-id :text "first!"}]
(let [result (mt/user-http-request :rasta :put 200 (format "comment/%d" comment-id) {:resolved true})]
(is (=? {:resolved true :text "first!" :author {:first_name "Rasta"} :resolved_by {:first_name "Rasta"}}
result))
(is (true? (t2/select-one-fn :resolved :model/Comment :id comment-id))))))
(testing "PUT /api/comment text"
(mt/with-temp
[:model/Card {card-id :id} {}
:model/Comment {comment-id :id} {:model "card" :model_id card-id :text "first!"}]
(let [result (mt/user-http-request :rasta :put 200 (format "comment/%d" comment-id) {:text "oops"})]
(is (=? {:resolved false :text "oops" :author {:first_name "Rasta"}}
result))
(is (= "oops" (t2/select-one-fn :text :model/Comment :id comment-id)))))))
(deftest comment-reaction-test
(testing "POST /api/comment/:id/react"
(mt/with-temp
[:model/Card {card-id :id} {}
:model/Comment {comment-id :id} {:model "card" :model_id card-id :text "first!"}]
(try
(mt/user-http-request :rasta :post 200
(format "comment/%d/react" comment-id)
{:emoji "🐄"})
(let [comment (first (mt/user-http-request :rasta :get 200 (format "comment?model=card&model_id=%d" card-id)))]
(is (=? {:text "first!"
:author {:first_name "Rasta"}
:reactions [{:emoji "🐄" :author {:first_name "Rasta"}}]}
comment)))
(finally
(t2/delete! :model/Reaction :comment_id comment-id))))))

View File

@@ -120,6 +120,11 @@
:model/Collection
(fn [_] (default-created-at-timestamped {:name (u.random/random-name)}))
:model/Comment
(fn [_] (default-timestamped
{:author_id (rasta-id)
:text (u.random/random-name)}))
:model/Action
(fn [_] {:creator_id (rasta-id)})

View File

@@ -1229,6 +1229,13 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@7.4.5":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@7.7.2":
version "7.7.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a"
@@ -1236,7 +1243,7 @@
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.25.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2"
integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==
@@ -1490,6 +1497,11 @@
dependencies:
tslib "^2.0.0"
"@emoji-mart/data@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.2.1.tgz#0ad70c662e3bc603e23e7d98413bd1e64c4fcb6c"
integrity sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==
"@emotion/babel-plugin@^11.11.0":
version "11.11.0"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c"
@@ -5711,6 +5723,13 @@
dependencies:
"@types/react" "*"
"@types/react-mentions@^4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@types/react-mentions/-/react-mentions-4.4.0.tgz#d5396140390a10ae696222a8a2a4b51cf58fca01"
integrity sha512-dKnY1h42GPUO/QAyei6HxEsFUbEcqK/t1k60ZbLJstB9RAs8OCT69mj9AnUbeNdbzYVISE88OC2IYkkthAAn2g==
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.20":
version "7.1.24"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0"
@@ -10770,6 +10789,11 @@ emittery@^0.13.1:
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
emoji-mart@^5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023"
integrity sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==
emoji-regex@^10.3.0:
version "10.3.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23"
@@ -19975,6 +19999,16 @@ react-markdown@^8.0.4:
unist-util-visit "^4.0.0"
vfile "^5.0.0"
react-mentions@^4.4.10:
version "4.4.10"
resolved "https://registry.yarnpkg.com/react-mentions/-/react-mentions-4.4.10.tgz#ae6c1e310a405597e83ce786f12c5bfb93b097ce"
integrity sha512-JHiQlgF1oSZR7VYPjq32wy97z1w1oE4x10EuhKjPr4WUKhVzG1uFQhQjKqjQkbVqJrmahf+ldgBTv36NrkpKpA==
dependencies:
"@babel/runtime" "7.4.5"
invariant "^2.2.4"
prop-types "^15.5.8"
substyle "^9.1.0"
react-merge-refs@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06"
@@ -22415,6 +22449,14 @@ stylis@^4.3.0:
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444"
integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==
substyle@^9.1.0:
version "9.4.1"
resolved "https://registry.yarnpkg.com/substyle/-/substyle-9.4.1.tgz#6a4647f363bc14fecc51aac371d4dbeda082aa50"
integrity sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA==
dependencies:
"@babel/runtime" "^7.3.4"
invariant "^2.2.4"
supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.1:
version "8.1.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"