Skip to content

Commit 3520931

Browse files
author
Guillaume Chau
committed
test: wip demo chat
1 parent 5e50581 commit 3520931

File tree

12 files changed

+533
-222
lines changed

12 files changed

+533
-222
lines changed

tests/demo/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
},
1313
"dependencies": {
1414
"graphql-type-json": "^0.2.1",
15+
"marked": "^0.4.0",
1516
"shortid": "^2.2.8",
1617
"vue": "^2.5.16",
17-
"vue-apollo": "^3.0.0-beta.10",
18+
"vue-apollo": "^3.0.0-beta.16",
1819
"vue-router": "^3.0.1",
1920
"vuex": "^3.0.1"
2021
},
@@ -28,7 +29,7 @@
2829
"graphql-tag": "^2.5.0",
2930
"stylus": "^0.54.5",
3031
"stylus-loader": "^3.0.2",
31-
"vue-cli-plugin-apollo": "file:../../../vue-cli-plugin-apollo",
32+
"vue-cli-plugin-apollo": "^0.13.4",
3233
"vue-template-compiler": "^2.5.16"
3334
},
3435
"browserslist": [

tests/demo/src/App.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ body
3737
grid-template-columns 300px 1fr
3838
grid-template-rows 1fr
3939
40+
:focus
41+
outline none
42+
4043
a
4144
text-decoration none
4245
color $color

tests/demo/src/components/ChannelList.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ export default {
3737
@import '~@/style/imports'
3838
3939
.channel-list
40-
background desaturate(darken($color, 40%), 95%)
40+
background desaturate(darken($color, 60%), 95%)
4141
color white
42-
padding 8px
42+
padding 12px
4343
4444
.channel
4545
display block
46-
padding 8px
46+
padding 12px
4747
border-radius 4px
4848
&:hover
4949
background rgba($color, .3)

tests/demo/src/components/ChannelView.vue

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,35 @@
55
:variables="{
66
id
77
}"
8+
@result="onResult"
89
>
910
<template slot-scope="{ result: { data, loading } }">
10-
<div v-if="loading" class="loading">Loading...</div>
11+
<div v-if="!data && loading" class="loading">Loading...</div>
1112

1213
<div v-else-if="data">
14+
<!-- Websockets -->
15+
<ApolloSubscribeToMore
16+
:document="require('../graphql/messageChanged.gql')"
17+
:variables="{
18+
channelId: id,
19+
}"
20+
:updateQuery="onMessageChanged"
21+
/>
22+
1323
<div class="wrapper">
1424
<div class="header">
1525
<div class="id">#{{ data.channel.id }}</div>
1626
<div class="name">{{ data.channel.name }}</div>
1727
</div>
18-
<div class="body">
1928

29+
<div ref="body" class="body">
30+
<MessageItem
31+
v-for="message in data.channel.messages"
32+
:key="message.id"
33+
:message="message"
34+
/>
2035
</div>
36+
2137
<div class="footer">
2238
<MessageForm :channel-id="id" />
2339
</div>
@@ -29,12 +45,14 @@
2945
</template>
3046

3147
<script>
48+
import MessageItem from './MessageItem.vue'
3249
import MessageForm from './MessageForm.vue'
3350
3451
export default {
3552
name: 'ChannelView',
3653
3754
components: {
55+
MessageItem,
3856
MessageForm,
3957
},
4058
@@ -44,6 +62,63 @@ export default {
4462
required: true,
4563
},
4664
},
65+
66+
watch: {
67+
id: {
68+
handler () {
69+
this.$_init = false
70+
},
71+
immediate: true,
72+
},
73+
},
74+
75+
methods: {
76+
onMessageChanged (previousResult, { subscriptionData }) {
77+
const { type, message } = subscriptionData.data.messageChanged
78+
79+
// No list change
80+
if (type === 'updated') return previousResult
81+
82+
const messages = previousResult.channel.messages.slice()
83+
// Add or remove item
84+
if (type === 'added') {
85+
messages.push(message)
86+
} else if (type === 'removed') {
87+
const index = messages.findIndex(m => m.id === message.id)
88+
if (index !== -1) messages.splice(index, 1)
89+
}
90+
91+
// New query result
92+
return {
93+
channel: {
94+
...previousResult.channel,
95+
messages,
96+
},
97+
}
98+
},
99+
100+
async scrollToBottom (force = false) {
101+
let el = this.$refs.body
102+
103+
// No body element yet
104+
if (!el) {
105+
setTimeout(() => this.scrollToBottom(force), 100)
106+
return
107+
}
108+
// User is scrolling up => no auto scroll
109+
if (!force && el.scrollTop + el.clientHeight < el.scrollHeight - 100) return
110+
111+
// Scroll to bottom
112+
await this.$nextTick()
113+
el.scrollTop = el.scrollHeight
114+
},
115+
116+
onResult () {
117+
// The first time we load a channel, we force scroll to bottom
118+
this.scrollToBottom(!this.$_init)
119+
this.$_init = true
120+
},
121+
},
47122
}
48123
</script>
49124

@@ -57,7 +132,7 @@ export default {
57132
grid-template-rows auto 1fr auto
58133
59134
.header
60-
padding 8px
135+
padding 12px
61136
border-bottom $border
62137
63138
.id
@@ -67,6 +142,10 @@ export default {
67142
.name
68143
color #555
69144
145+
.body
146+
overflow-x hidden
147+
overflow-y auto
148+
70149
.footer
71150
border-top $border
72151
</style>

tests/demo/src/components/MessageForm.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
>
1313
<input
1414
slot-scope="{ mutate, loading, error }"
15+
ref="input"
1516
v-model="newMessage"
1617
:disabled="loading"
1718
class="form-input"
@@ -39,6 +40,7 @@ export default {
3940
methods: {
4041
onDone () {
4142
this.newMessage = ''
43+
this.$refs.input.focus()
4244
},
4345
},
4446
}
@@ -48,7 +50,7 @@ export default {
4850
@import '~@/style/imports'
4951
5052
.message-form
51-
padding 8px
53+
padding 12px
5254
width 100%
5355
box-sizing border-box
5456
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<template>
2+
<div class="message-item">
3+
<div class="user">{{ message.user.nickname }}</div>
4+
<div class="content" v-html="html"/>
5+
</div>
6+
</template>
7+
8+
<script>
9+
import marked from 'marked'
10+
11+
// Open links in new tab
12+
const renderer = new marked.Renderer()
13+
const linkRenderer = renderer.link
14+
renderer.link = (href, title, text) => {
15+
const html = linkRenderer.call(renderer, href, title, text)
16+
return html.replace(/^<a /, '<a target="_blank" rel="nofollow" ')
17+
}
18+
19+
export default {
20+
props: {
21+
message: {
22+
type: Object,
23+
required: true,
24+
},
25+
},
26+
27+
computed: {
28+
html () {
29+
return marked(this.message.content, { renderer })
30+
},
31+
},
32+
}
33+
</script>
34+
35+
<style lang="stylus" scoped>
36+
@import '~@/style/imports'
37+
38+
.message-item
39+
padding 12px 12px
40+
&:hover
41+
background #f8f8f8
42+
43+
.user
44+
color #777
45+
font-size 13px
46+
margin-bottom 2px
47+
48+
.content
49+
word-wrap break-word
50+
51+
>>>
52+
p
53+
margin 0
54+
55+
img
56+
max-width 500px
57+
max-height 500px
58+
</style>

tests/demo/src/components/UserCurrent.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ export default {
4848
display grid
4949
grid-template-columns auto 1fr auto
5050
grid-template-rows auto
51-
grid-gap 8px
51+
grid-gap 12px
5252
align-items center
5353
margin-bottom 20px
54-
padding 8px 0 8px 8px
54+
padding 12px 0 12px 12px
5555
5656
.email
5757
font-size 12px

tests/demo/src/components/UserLogin.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ export default {
119119
if (!result.data.userLogin) return
120120
const apolloClient = this.$apollo.provider.defaultClient
121121
// Update token and reset cache
122-
await onLogin(apolloClient, result.data.userLogin.token)
122+
const { id, userId, expiration } = result.data.userLogin.token
123+
await onLogin(apolloClient, { id, userId, expiration })
123124
// Update cache
124125
apolloClient.writeQuery({
125126
query: USER_CURRENT,
@@ -171,10 +172,10 @@ export default {
171172
box-sizing border-box
172173
173174
.form-input
174-
margin-bottom 8px
175+
margin-bottom 12px
175176
176177
.actions
177-
margin-top 8px
178+
margin-top 12px
178179
text-align center
179180
font-size 12px
180181
</style>

tests/demo/src/graphql-api/context.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ const users = require('./connectors/users')
44
// req => Query
55
// connection => Subscription
66
// eslint-disable-next-line no-unused-vars
7-
module.exports = (req, connection) => {
8-
const bearer = req ? req.get('Authorization') : null
9-
const token = bearer ? JSON.parse(bearer) : null
7+
module.exports = (req, wsConnection) => {
8+
let rawToken
9+
if (req) rawToken = req.get('Authorization')
10+
if (wsConnection) rawToken = wsConnection.authorization
11+
const token = rawToken ? JSON.parse(rawToken) : null
1012
let userId
1113

1214
if (token && users.validateToken(token)) {

tests/demo/src/graphql-api/directives/private.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ const { defaultFieldResolver } = require('graphql')
44
module.exports = class PrivateDirective extends SchemaDirectiveVisitor {
55
visitFieldDefinition (field) {
66
const { resolve = defaultFieldResolver } = field
7-
field.resolve = (root, args, context) => {
7+
field.resolve = (root, args, context, info) => {
88
if (!context.userId) throw new Error('Unauthorized')
9-
return resolve(root, args, context)
9+
return resolve(root, args, context, info)
1010
}
1111
}
1212
}

tests/demo/src/graphql-api/resolvers.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ module.exports = {
3434
},
3535

3636
Subscription: {
37-
messageChanged: withFilter(
38-
(parent, args, { pubsub }) => pubsub.asyncIterator(triggers.MESSAGE_CHANGED),
39-
(payload, vars) => payload.messageChanged.channelId === vars.channelId
40-
),
37+
messageChanged: {
38+
subscribe: withFilter(
39+
(parent, args, { pubsub }) => pubsub.asyncIterator(triggers.MESSAGE_CHANGED),
40+
(payload, vars) => payload.messageChanged.message.channelId === vars.channelId
41+
),
42+
},
4143
},
4244
}

0 commit comments

Comments
 (0)