Skip to content
This repository was archived by the owner on Nov 5, 2023. It is now read-only.

Commit b6728eb

Browse files
committed
Add ability to have vanity links
1 parent 5effeb4 commit b6728eb

File tree

14 files changed

+1258
-251
lines changed

14 files changed

+1258
-251
lines changed

app/.env.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ FIREBASE_DATABASE_URL=
2424
FIREBASE_PROJECT_ID=
2525
FIREBASE_STORAGE_BUCKET=
2626
FIREBASE_MESSAGING_SENDER_ID=
27+
FIREBASE_NODE_SERVICE_ACCOUNT_KEY=
2728

2829
ADMIN_TOKEN=

app/api/routes/rooms/index.js

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ const openGraphScraper = require('open-graph-scraper');
77
const vibrant = require('node-vibrant')
88
const url = require('url');
99
const twitch = require('./twitch');
10+
const admin = require('firebase-admin');
11+
12+
admin.initializeApp({
13+
credential: admin.credential.cert(JSON.parse(process.env.FIREBASE_NODE_SERVICE_ACCOUNT_KEY))
14+
});
1015

1116
const expireHours = 48;
1217

@@ -71,13 +76,43 @@ rooms.post('/', async (req, res, next) => {
7176
}
7277

7378
let roomKey, roomId, roomKeyAlreadyExists;
74-
do {
75-
roomId = nanoidGenerate('23456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghjkmnpqrstuvwxyz-', 8);
76-
roomKey = 'rooms:' + roomId;
77-
roomKeyAlreadyExists = await redisClient.existsAsync(roomKey) === 1;
78-
}
79-
while (roomKeyAlreadyExists); // repeat roomkey generation on collision
79+
if (req.body.urlType === 'vanity') {
80+
if (!req.body.uid) {
81+
// Need a user ID to continue
82+
res.sendStatus(403);
83+
return;
84+
}
85+
86+
// Check that they're allowed to request this vanity URL
87+
let db = admin.firestore();
88+
let vanity = await db.collection('users')
89+
.doc(req.body.uid)
90+
.collection('privileges')
91+
.doc('share')
92+
.get()
93+
.then((document) => {
94+
if (document.exists) {
95+
return document.data().vanity;
96+
}
97+
});
8098

99+
if (vanity) {
100+
// They have a vanity URL
101+
roomId = vanity;
102+
roomKey = 'rooms:' + roomId;
103+
} else {
104+
// They don't have a vanity URL
105+
res.sendStatus(403);
106+
return;
107+
}
108+
} else {
109+
do {
110+
roomId = nanoidGenerate('23456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghjkmnpqrstuvwxyz-', 8);
111+
roomKey = 'rooms:' + roomId;
112+
roomKeyAlreadyExists = await redisClient.existsAsync(roomKey) === 1;
113+
}
114+
while (roomKeyAlreadyExists); // repeat roomkey generation on collision
115+
}
81116
const ownerKey = nanoid(50);
82117

83118
const backlink = req.body.backlink ? ['backlink', req.body.backlink] : [];
@@ -86,13 +121,22 @@ rooms.post('/', async (req, res, next) => {
86121
const appearance = req.body.appearance && req.body.appearance.length <= 1000 ? ['appearance', req.body.appearance] : [];
87122

88123
redisClient.hmset(roomKey, 'ownerKey', ownerKey, ...backlink, ...appearance);
89-
redisClient.expire(roomKey, 60 * 60 * expireHours);
124+
125+
if (req.body.urlType === 'random') {
126+
// Random URLs expire
127+
redisClient.expire(roomKey, 60 * 60 * expireHours);
128+
}
90129

91130
res.send(JSON.stringify({
92131
roomId,
93132
ownerKey,
94133
url: process.env.HOSTNAME + '/s/' + roomId,
95-
expireDate: new Date((new Date()).getTime() + (1000 * 60 * 60 * expireHours)),
134+
135+
// Vanity URLs never expire
136+
expires: req.body.urlType === 'random',
137+
expireDate: req.body.urlType === 'random' ?
138+
(new Date((new Date()).getTime() + (1000 * 60 * 60 * expireHours))) :
139+
null,
96140
}));
97141
return;
98142
});

app/components/ShareButton.vue

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
<template>
2-
<div>
2+
<b-btn-group class="mr-2">
33
<b-btn
44
v-b-tooltip.hover
55
:title="tooltip"
6-
:variant="hasValidShareLink ? 'secondary' : 'info'"
7-
class="mr-2"
6+
:variant="on ? 'secondary' : 'outline-light'"
87
@click="showShareSettings()"
98
>
109
<fa icon="broadcast-tower"/>
@@ -16,18 +15,43 @@
1615
<fa icon="exclamation-triangle"></fa>
1716
</b-badge>
1817
</b-btn>
19-
</div>
18+
<b-btn
19+
v-b-tooltip.hover
20+
:title="on ? 'Turn off sharing' : 'Turn on sharing'"
21+
:variant="on ? 'light' : 'light'"
22+
id="share-on-off-toggle"
23+
v-if="hasValidShareLink && !expired"
24+
class="px-3"
25+
@click="on = !on; $event.target.blur(); $event.target.parentElement.blur()"
26+
>
27+
<fa :icon="on ? 'toggle-on' : 'toggle-off'" style="font-size:1.3rem;margin-top:0.2rem"/>
28+
</b-btn>
29+
</b-btn-group>
2030
</template>
2131

32+
33+
<style>
34+
.pointerSwitch,
35+
.pointerSwitch:hover,
36+
.pointerSwitch.custom-switch .custom-control-label::before,
37+
.pointerSwitch.custom-switch .custom-control-label::after {
38+
cursor: pointer;
39+
}
40+
</style>
41+
2242
<script>
2343
import bBtn from 'bootstrap-vue/es/components/button/button';
44+
import bBtnGroup from 'bootstrap-vue/es/components/button-group/button-group';
2445
import bBadge from 'bootstrap-vue/es/components/badge/badge';
2546
import bTooltip from 'bootstrap-vue/es/directives/tooltip/tooltip';
47+
import bFormCheckbox from 'bootstrap-vue/es/components/form-checkbox/form-checkbox';
2648
2749
export default {
2850
components: {
2951
bBtn,
52+
bBtnGroup,
3053
bBadge,
54+
bFormCheckbox,
3155
},
3256
directives: {
3357
bTooltip,
@@ -43,14 +67,16 @@ export default {
4367
this.$store.commit('share/SET_SHOW_SETTINGS', { on: true });
4468
}
4569
},
70+
hideAllTooltips: function() {
71+
this.$root.$emit('bv::hide::tooltip');
72+
},
4673
},
4774
computed: {
4875
tooltip: function() {
4976
if (this.expired) {
5077
return 'Share Captions (Link Expired)';
5178
} else if (this.hasValidShareLink && this.subscriberCount > 0) {
5279
return (
53-
'Sharing captions with ' +
5480
this.subscriberCount +
5581
' viewer' +
5682
(this.subscriberCount != 1 ? 's' : '')
@@ -59,6 +85,18 @@ export default {
5985
return 'Share Captions';
6086
}
6187
},
88+
on: {
89+
get() {
90+
return this.$store.state.settings.share.on;
91+
},
92+
set(on) {
93+
this.$store.commit('SET_SHARE_ON', { on });
94+
this.hideAllTooltips();
95+
this.$nextTick(function() {
96+
this.$root.$emit('bv::show::tooltip', 'share-on-off-toggle');
97+
});
98+
},
99+
},
62100
shareLink: function() {
63101
return this.$store.state.settings.share.url;
64102
},
@@ -75,7 +113,9 @@ export default {
75113
return this.$store.state.share.expired;
76114
},
77115
hasValidShareLink() {
78-
return this.shareLink && this.roomId && this.expireDate;
116+
return (
117+
this.shareLink && this.roomId && (this.expireDate || !this.expires)
118+
);
79119
},
80120
},
81121
};

app/components/toasts/Share.vue

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,30 @@
2525
Random link
2626
<span class="small text-muted">(Expires in 48 hours)</span>
2727
</b-form-radio>
28-
<b-form-radio value="vanity" v-if="false">
29-
My custom vanity link
28+
<b-form-radio value="vanity">
29+
Custom vanity link
3030
<span class="small text-muted">(Never expires)</span>
3131
</b-form-radio>
3232
</b-form-radio-group>
3333
</b-form-group>
34-
<p
35-
v-if="urlType === 'vanity'"
36-
class="small text-danger mb-0 mt-2"
37-
>Sorry, vanity links aren't available to everyone yet.</p>
34+
<transition name="fade-in">
35+
<div v-if="urlType === 'vanity'">
36+
<b-badge
37+
v-if="vanity"
38+
variant="success"
39+
class="ml-4 mt-1 px-2 py-1"
40+
style="font-size:.9rem"
41+
>
42+
<fa icon="star"/>
43+
{{vanity}}
44+
</b-badge>
45+
<p
46+
v-else-if="vanity === false"
47+
class="small text-danger mb-0 mt-2"
48+
>Hang tight! Vanity links aren't available to everyone yet.</p>
49+
<b-spinner v-else small class="mt-2 ml-4" variant="muted"></b-spinner>
50+
</div>
51+
</transition>
3852
</form>
3953
</div>
4054
<div v-else style="width:500px; min-width:200px; max-width:100%">
@@ -108,15 +122,13 @@
108122
</div>
109123
</b-collapse>
110124
<hr class="my-3">
111-
<p class="small text-muted mb-2">
112-
Link expires
113-
<timeago :datetime="expireDate"></timeago>
114-
</p>
115-
<!-- <div class="card p-2 bg-primary text-info mb-3">
116-
<p class="text-monospace text-uppercase font-weight-bold mb-1"><fa icon="info-circle"/> Enjoy this Preview!</p>
117-
<span class="small">I hope you enjoy the preview of this new feature! Please send me feedback on <a href="https://facebook.com/webcaptioner" target="_blank">Facebook</a> or <a href="https://twitter.com/webcaptioner" target="_blank">Twitter</a> about how well it works for you and your viewers.</span>
118-
</div>-->
119-
<hr class="my-3">
125+
<div v-if="expireDate !== null">
126+
<p class="small text-muted mb-2">
127+
Link expires
128+
<timeago :datetime="expireDate"></timeago>
129+
</p>
130+
<hr class="my-3">
131+
</div>
120132
<b-dropdown
121133
text="Options"
122134
variant="outline-secondary"
@@ -155,8 +167,8 @@
155167
<template v-if="!hasValidShareLink" slot="footer">
156168
<b-btn
157169
@click="getLink()"
158-
:disabled="gettingLink || urlType === 'vanity'"
159-
:variant="urlType === 'vanity' ? 'light' : 'secondary'"
170+
:disabled="gettingLink || (urlType === 'vanity' && !vanity)"
171+
:variant="(urlType === 'vanity' && !vanity) ? 'light' : 'secondary'"
160172
>
161173
Get Link
162174
<fa v-if="gettingLink" icon="spinner" spin/>
@@ -176,6 +188,8 @@ import bFormCheckbox from 'bootstrap-vue/es/components/form-checkbox/form-checkb
176188
import bFormGroup from 'bootstrap-vue/es/components/form-group/form-group';
177189
import bFormRadio from 'bootstrap-vue/es/components/form-radio/form-radio';
178190
import bFormRadioGroup from 'bootstrap-vue/es/components/form-radio/form-radio-group';
191+
import bSpinner from 'bootstrap-vue/es/components/spinner/spinner';
192+
import bBadge from 'bootstrap-vue/es/components/badge/badge';
179193
180194
export default {
181195
components: {
@@ -188,6 +202,8 @@ export default {
188202
bFormGroup,
189203
bFormRadio,
190204
bFormRadioGroup,
205+
bSpinner,
206+
bBadge,
191207
},
192208
directives: {
193209
bTooltip,
@@ -236,17 +252,24 @@ export default {
236252
}
237253
238254
try {
239-
const { roomId, ownerKey, url, expireDate } = await this.$axios.$post(
240-
'/api/rooms',
241-
{
242-
backlink: this.backlink,
243-
appearance: JSON.stringify(this.$store.state.settings.appearance),
244-
}
245-
);
255+
const {
256+
roomId,
257+
ownerKey,
258+
url,
259+
expires,
260+
expireDate,
261+
} = await this.$axios.$post('/api/rooms', {
262+
backlink: this.backlink,
263+
appearance: JSON.stringify(this.$store.state.settings.appearance),
264+
urlType: this.urlType,
265+
uid: this.$store.state.user.uid,
266+
});
246267
268+
this.$store.commit('SET_SHARE_ON', { on: true });
247269
this.$store.commit('SET_SHARE_ROOM_ID', { roomId });
248270
this.$store.commit('SET_SHARE_OWNER_KEY', { ownerKey });
249271
this.$store.commit('SET_SHARE_URL', { url });
272+
this.$store.commit('SET_SHARE_EXPIRES', { expires });
250273
this.$store.commit('SET_SHARE_EXPIRE_DATE', { expireDate });
251274
this.$store.commit('SET_SHARE_SUBSCRIBER_COUNT', {
252275
subscriberCount: 0,
@@ -284,10 +307,12 @@ export default {
284307
}
285308
},
286309
nullLinkProperties() {
310+
this.$store.commit('SET_SHARE_ON', { on: false });
287311
this.$store.commit('SET_SHARE_ROOM_ID', { roomId: null });
288312
this.$store.commit('SET_SHARE_OWNER_KEY', { ownerKey: null });
289313
this.$store.commit('SET_SHARE_URL', { url: null });
290314
this.$store.commit('SET_SHARE_EXPIRE_DATE', { expireDate: null });
315+
this.$store.commit('SET_SHARE_URL_TYPE', { urlType: 'random' });
291316
},
292317
shareLinkSelect() {
293318
this.$nextTick(function() {
@@ -332,6 +357,24 @@ export default {
332357
}
333358
});
334359
},
360+
urlType: function(urlType) {
361+
if (urlType === 'vanity') {
362+
let db = this.$firebase.firestore();
363+
db.collection('users')
364+
.doc(this.$store.state.user.uid)
365+
.collection('privileges')
366+
.doc('share')
367+
.get()
368+
.then((document) => {
369+
if (document.exists) {
370+
const { vanity } = document.data();
371+
this.$store.commit('SET_SHARE_VANITY', {
372+
vanity: vanity || false,
373+
});
374+
}
375+
});
376+
}
377+
},
335378
},
336379
computed: {
337380
show: function() {
@@ -360,8 +403,13 @@ export default {
360403
this.$store.commit('SET_SHARE_URL_TYPE', { urlType });
361404
},
362405
},
406+
vanity: function() {
407+
return this.$store.state.settings.share.vanity;
408+
},
363409
hasValidShareLink() {
364-
return this.shareLink && this.roomId && this.expireDate;
410+
return (
411+
this.shareLink && this.roomId && (this.expireDate || !this.expires)
412+
);
365413
},
366414
// facebookShareLink() {
367415
// return 'https://www.facebook.com/dialog/share?app_id=1339681726086659&amp;display=popup&amp;href=' + encodeURIComponent(this.shareLink);

app/nuxt.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ module.exports = {
8080
'FIREBASE_STORAGE_BUCKET',
8181
'FIREBASE_MESSAGING_SENDER_ID',
8282
'GOOGLE_CAST_APP_ID',
83+
'HOSTNAME',
8384
'STRIPE_API_KEY_PUBLIC',
8485
]
8586
}],
@@ -164,6 +165,9 @@ module.exports = {
164165
'faWindowRestore',
165166
'faBars',
166167
'faUserCircle',
168+
'faStar',
169+
'faToggleOn',
170+
'faToggleOff',
167171
],
168172
},
169173
{

0 commit comments

Comments
 (0)