Skip to content

Commit 963f10d

Browse files
authored
feat: add session reactions (#3799)
- closes #1168
1 parent c34778a commit 963f10d

55 files changed

Lines changed: 3538 additions & 36 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Our goal is to allow event organizers to set up a professional conference websit
2424
| **SEO optimized** | index all content and get to the top in search results |
2525
| **Speakers and schedule management** | keep and update all information in the Firebase |
2626
| **My schedule** | let attendees save sessions they want to visit |
27+
| **Reactions** | let attendees react to sessions with applause, love and more |
2728
| **Customizable theme** | light and dark themes, with your own colors and fonts |
2829
| **Blog** | post announcements, updates and useful information |
2930

‎docs/tutorials/01-configure-app.md‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ Upstream defaults for the other settings in `site.json`, such as `navigation` an
3333

3434
`theme` picks the look: the `festival`, `spotlight`, `paper` or `glass` theme, the color scheme, colors, fonts, spacing and decorations. See [Styling][style app].
3535

36-
`auth.providers` lists the ways visitors sign in, in the order the sign-in dialog shows them. Visitors sign in to save sessions to My Schedule and to rate sessions.
36+
`auth.providers` lists the ways visitors sign in, in the order the sign-in dialog shows them. Visitors sign in to save sessions to My Schedule, to rate sessions and to react to them.
3737

3838
- `emailLink` (the default): visitors enter their email address and get a link that signs them in, with no password. It is the only one on by default.
3939
- `google`, `facebook` and `twitter`: sign in with that account in a popup.
4040

41-
For example, `["emailLink", "google"]` adds Google, and `["google"]` turns email links off. Turn on each method you list in the Firebase console too. See [Sign-in](02-firebase.md#sign-in). When `mySchedule` or `feedback` is on, the list can't be empty.
41+
For example, `["emailLink", "google"]` adds Google, and `["google"]` turns email links off. Turn on each method you list in the Firebase console too. See [Sign-in](02-firebase.md#sign-in). When `mySchedule` or `feedback` is on, the list can't be empty. With an empty list, turn `reactions` off too, since nobody could react.
4242

4343
## Validation
4444

@@ -122,7 +122,7 @@ Turn parts of the site off in `features` in `packages/config/site.json`. Every f
122122
}
123123
```
124124

125-
A feature that is off has no pages, navigation entry or home page block, and its code is left out of the build. The features are `blog`, `codeOfConduct`, `demo`, `faq`, `feedback`, `forkMe`, `functions`, `gallery`, `map`, `mySchedule`, `notifications`, `partners`, `previousSpeakers`, `schedule`, `socialImages`, `speakers`, `subscribe`, `team`, `tickets` and `videos`.
125+
A feature that is off has no pages, navigation entry or home page block, and its code is left out of the build. The features are `blog`, `codeOfConduct`, `demo`, `faq`, `feedback`, `forkMe`, `functions`, `gallery`, `map`, `mySchedule`, `notifications`, `partners`, `previousSpeakers`, `reactions`, `schedule`, `socialImages`, `speakers`, `subscribe`, `team`, `tickets` and `videos`.
126126

127127
The build fails when:
128128

@@ -137,11 +137,17 @@ With `functions` on, every Cloud Function always deploys. When its feature is of
137137

138138
When `feedback` is off, the feedback dialog is still in the build, but nothing opens it. Save buttons show only when `mySchedule` is on.
139139

140+
`reactions` adds a row of reactions to each session page: Applause, Love, Insightful, Mind blown and Funny. Signed-in visitors add any of them, each once. Everyone sees the counts, and each count's label names the latest people who reacted. Visitors can react before a session and until a week after it ends, in `event.timezone`. After that, they can only take their reactions away. Reactions show only on session pages, so they need `schedule` to show anywhere. They don't need `functions`.
141+
142+
The first time visitors react, they pick the name to show, and the photo from their sign-in account if it has one. That is their public profile, in the `profiles` collection, and anyone can read it. Visitors change it or delete it under **Public profile** in the account menu. Deleting it deletes their reactions too. Reactions are in `sessions/{sessionId}/reactions/{userId}`. To remove an abusive name or reaction, delete the document in the Firebase console.
143+
144+
A session page reads every reaction to its session, and the profiles of up to 10 people it names, each time it opens. A session with 200 reactions costs about 210 Firestore reads per view (verify).
145+
140146
`socialImages` builds a share image for each session and speaker page, which social networks and chat apps show with a link to the page. A session's image has its title, speakers, day, time and track. A speaker's image has their photo, name, company and the event's dates. Both have the logo from `packages/web/public/images/logo.svg`, the venue and the site's address, in the theme's colors and fonts. The build downloads speaker photos for them, and a photo that fails to download shows the speaker's initials, with a warning in the build output. With `socialImages` off, those pages share the first speaker's photo or `image` from `site.json`.
141147

142148
The images come from the content at build time, like the rest of the page. After changing sessions or speakers, deploy again to update them. Each image's file name changes with what it shows, so social networks fetch the new one. To check how a page looks when shared, paste its URL into the [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/) or the [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/).
143149

144-
The header follows the features too. Its button links to tickets until the event is over, or to the schedule otherwise. The account button shows when `mySchedule` or `feedback` is on, and the notifications bell when `notifications` is on. The footer links to the home page's subscribe band when `subscribe` is on, and shows a "Fork me on GitHub" sticker when `forkMe` is on.
150+
The header follows the features too. Its button links to tickets until the event is over, or to the schedule otherwise. The account button shows when `mySchedule`, `feedback` or `reactions` is on, and the notifications bell when `notifications` is on. The footer links to the home page's subscribe band when `subscribe` is on, and shows a "Fork me on GitHub" sticker when `forkMe` is on.
145151

146152
## Content Security Policy
147153

‎docs/tutorials/06-security.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ What Hoverboard protects for you, and what you set in your own Firebase project
44

55
## What Hoverboard does
66

7-
- **Firestore rules.** Visitors can only read your content. You change it in the Firebase console or with `./hb firestore-*`. Signed-in visitors can write only their own saved sessions, notification settings and feedback. The subscribe and partner forms can only add documents, with checked fields and sizes, and nobody can read them from the site. Export them with [`./hb firestore-csv`](01-configure-app.md#subscribers-and-partner-leads).
7+
- **Firestore rules.** Visitors can only read your content. You change it in the Firebase console or with `./hb firestore-*`. Signed-in visitors can write only their own saved sessions, notification settings, feedback, reactions and public profile. A reaction needs its author's profile and a session that exists, and the rules check every field. The subscribe and partner forms can only add documents, with checked fields and sizes, and nobody can read them from the site. Export them with [`./hb firestore-csv`](01-configure-app.md#subscribers-and-partner-leads).
88
- **Storage rules.** The site can't read or write your Storage bucket.
99
- **Content.** Links in your config and content can only be `https:`, `http:`, `mailto:` or a path on your site. The site drops other links, such as `javascript:` ones, even when they come straight from the Firebase console. Markdown is sanitized before it is shown, and the build sanitizes the hero illustration.
1010
- **Headers.** `firebase.json` sends `Strict-Transport-Security`, `Referrer-Policy`, `Permissions-Policy` and other headers on every page, and every page has a [Content Security Policy](01-configure-app.md#content-security-policy).
1111
- **Sign-out.** Signing out deletes the copy of the visitor's data that the site keeps in the browser for offline use.
12+
- **Public profiles.** With `reactions` on, the name and photo a visitor picks show to anyone, with their reactions. Nobody has a profile until they react and confirm it. The photo can only be the one from their sign-in account, so a profile can't point other visitors' browsers at any other address. Visitors delete their profile and reactions themselves. To delete someone's data on request, delete their `profiles/{userId}` document and their documents in `sessions/*/reactions` in the Firebase console.
1213
- **Logs.** The functions don't log emails, push tokens or user IDs.
1314
- **Deploys.** GitHub Actions deploy without a service account key, with only the roles a deploy needs. See [Deploying to Firebase with Github Actions](04-deploy.md#deploying-to-firebase-with-github-actions).
1415

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import {
2+
deleteDoc,
3+
doc,
4+
getDoc,
5+
serverTimestamp,
6+
setDoc,
7+
Timestamp,
8+
updateDoc,
9+
} from 'firebase/firestore';
10+
import { beforeEach, describe, it } from 'vitest';
11+
import { expect } from '../helpers';
12+
import { anonContext, authedContext, seed } from './setup';
13+
14+
const ownerUid = 'owner-uid';
15+
const otherUid = 'other-uid';
16+
const picture = 'https://photos.test/ada.jpg';
17+
const profilePath = `profiles/${ownerUid}`;
18+
const profile = () => ({ name: 'Ada Lovelace', photoUrl: picture, updatedAt: serverTimestamp() });
19+
20+
const asOwner = (token: Record<string, string> = { picture }) =>
21+
authedContext(ownerUid, token).firestore();
22+
23+
describe('profiles rules', () => {
24+
beforeEach(() =>
25+
seed({
26+
[`profiles/${otherUid}`]: { name: 'Grace Hopper', photoUrl: '', updatedAt: new Date() },
27+
}),
28+
);
29+
30+
it('lets anyone read a profile', async () => {
31+
await expect(getDoc(doc(anonContext().firestore(), `profiles/${otherUid}`))).toAllow();
32+
await expect(getDoc(doc(asOwner(), `profiles/${otherUid}`))).toAllow();
33+
});
34+
35+
it('lets the owner create, update and delete their profile', async () => {
36+
await expect(setDoc(doc(asOwner(), profilePath), profile())).toAllow();
37+
await expect(
38+
updateDoc(doc(asOwner(), profilePath), { name: 'Ada King', updatedAt: serverTimestamp() }),
39+
).toAllow();
40+
await expect(deleteDoc(doc(asOwner(), profilePath))).toAllow();
41+
});
42+
43+
it('allows no photo, with or without a photo from the sign-in provider', async () => {
44+
await expect(setDoc(doc(asOwner(), profilePath), { ...profile(), photoUrl: '' })).toAllow();
45+
await expect(setDoc(doc(asOwner({}), profilePath), { ...profile(), photoUrl: '' })).toAllow();
46+
});
47+
48+
it("denies a photo that isn't the sign-in provider's", async () => {
49+
await expect(
50+
setDoc(doc(asOwner(), profilePath), { ...profile(), photoUrl: 'https://tracker.test/1.gif' }),
51+
).toDeny();
52+
await expect(setDoc(doc(asOwner({}), profilePath), profile())).toDeny();
53+
});
54+
55+
it.each([
56+
['an empty name', { name: '' }],
57+
['a blank name', { name: ' ' }],
58+
['a name over 100 characters', { name: 'x'.repeat(101) }],
59+
['a name that is not text', { name: 42 }],
60+
['an extra field', { bio: 'Hi' }],
61+
['a time from the client', { updatedAt: Timestamp.fromDate(new Date('2026-01-01')) }],
62+
])('denies %s', async (_description, overrides) => {
63+
await expect(setDoc(doc(asOwner(), profilePath), { ...profile(), ...overrides })).toDeny();
64+
});
65+
66+
it('denies a profile without a name', async () => {
67+
const { name: _name, ...withoutName } = profile();
68+
await expect(setDoc(doc(asOwner(), profilePath), withoutName)).toDeny();
69+
});
70+
71+
it("denies writing or deleting someone else's profile", async () => {
72+
const path = `profiles/${otherUid}`;
73+
await expect(setDoc(doc(asOwner(), path), profile())).toDeny();
74+
await expect(deleteDoc(doc(asOwner(), path))).toDeny();
75+
});
76+
77+
it('denies signed-out visitors any write', async () => {
78+
const firestore = anonContext().firestore();
79+
await expect(setDoc(doc(firestore, profilePath), profile())).toDeny();
80+
await expect(deleteDoc(doc(firestore, `profiles/${otherUid}`))).toDeny();
81+
});
82+
});

0 commit comments

Comments
 (0)