feat(comments): selection, replies, alignments and other interactions

This commit is contained in:
Dimitrie Stefanescu
2022-03-10 21:29:44 +00:00
parent db6a1c7b56
commit ed40ff571c
12 changed files with 267 additions and 74 deletions
@@ -1,10 +1,11 @@
<template>
<div class="mt-2 pa-1 d-flex align-center" style="width: 300px">
<div class="">
<div class="" style="width: 100%">
<template v-for="(reply, index) in thread">
<div v-if="index % 3 === 0" :key="index + 'date'" class="d-flex justify-center mouse">
<div v-if="showTime(index)" :key="index + 'date'" class="d-flex justify-center mouse">
<div class="d-inline px-2 py-0 caption text-center mb-2 rounded-lg background grey--text">
{{ new Date(Date.now()).toLocaleString() }}
{{ new Date(reply.createdAt).toLocaleString() }}
<timeago :datetime="reply.createdAt" class="font-italic ma-1"></timeago>
</div>
</div>
<div
@@ -23,15 +24,27 @@
</template>
<div class="px-0 mb-4">
<v-textarea
v-model="replyText"
solo
hide-details
auto-grow
rows="1"
placeholder="Reply"
placeholder="Reply (shift + enter to send)"
class="rounded-xl mb-2 caption"
append-icon="mdi-send"
@click:append="addReply"
@keydown.enter.shift.exact.prevent="addReply()"
></v-textarea>
<v-btn
v-tooltip="'Marks this thread as resolved.'"
class="float-right"
x-small
rounded
depressed
color="error"
>
Archive
</v-btn>
</div>
</div>
</div>
@@ -46,7 +59,7 @@ export default {
comment: { type: Object, default: () => null }
},
apollo: {
barf: {
replyQuery: {
query: gql`
query($streamId: String!, $id: String!) {
comment(streamId: $streamId, id: $id) {
@@ -70,54 +83,96 @@ export default {
id: this.comment.id
}
},
skip() {
return !this.comment.expanded
},
// result({ data }) {
// console.log('data')
// console.log(data)
// skip() {
// return !this.comment.expanded
// },
update: (data) => {
console.log(data)
return data.comment
result({ data }) {
data.comment.replies.items.forEach((item) => {
if (this.localReplies.findIndex((c) => c.id === item.id) === -1)
this.localReplies.push(item)
})
// this.localReplies.push(...data.comment.replies.items)
},
update: (data) => data.comment
},
$subscribe: {
commentReplyCreated: {
query: gql`
subscription($streamId: String!, $commentId: String!) {
commentReplyCreated(streamId: $streamId, commentId: $commentId)
}
`,
variables() {
return {
streamId: this.$route.params.streamId,
commentId: this.comment.id
}
},
// skip() {
// return !this.comment.expanded
// },
result({ data }) {
if (!this.comment.expanded) return this.$emit('bounce', this.comment.id)
this.localReplies.push({ ...data.commentReplyCreated })
}
}
}
},
data: function () {
return {
replyText: null
replyText: null,
localReplies: []
}
},
computed: {
thread() {
// TODO: add the replies in here too
return [this.comment]
return [this.comment, ...this.localReplies]
}
},
watch: {
'comment.expanded': {
deep: true,
handler(newVal, oldVal) {
if (!newVal) return
this.localReplies = []
this.$apollo.queries.replyQuery.refetch()
}
}
},
methods: {
showTime(index) {
if (index === 0) return true
let curr = new Date(this.thread[index].createdAt)
let prev = new Date(this.thread[index - 1].createdAt)
let delta = Math.abs(prev - curr)
return delta > 450000
},
async addReply() {
if (!this.commentText || this.commentText.length < 5) {
if (!this.replyText || this.replyText.length < 3) {
this.$eventHub.$emit('notification', {
text: `Reply must be at least 5 characters.`
text: `Reply must be at least 3 characters.`
})
return
}
let commentInput = {
let replyInput = {
streamId: this.$route.params.streamId,
resources: [{ resourceId: this.comment.id, resourceType: 'comment' }],
parentComment: this.comment.id,
// resources: [{ resourceId: this.$route.params.streamId, resourceType: 'stream' }],
text: this.replyText
}
try {
await this.$apollo.mutate({
mutation: gql`
mutation commentCreate($input: CommentCreateInput!) {
commentCreate(input: $input)
mutation commentReply($input: ReplyCreateInput!) {
commentReply(input: $input)
}
`,
variables: { input: commentInput }
variables: { input: replyInput }
})
this.replyText = null
} catch (e) {
this.$eventHub.$emit('notification', {
text: e.message
@@ -125,7 +180,7 @@ export default {
}
setTimeout(() => {
this.$emit('reply-added') // needed for layout reshuffle in parent
this.$emit('refresh-layout') // needed for layout reshuffle in parent
}, 100)
}
}
@@ -91,7 +91,7 @@ export default {
result({ data }) {
// Note: swap user id checks for .userId (vs. uuid) if wanting to not allow same user two diff browsers
// it's easier to test like this though :)
if (data.userCommentActivity.status === 'disconnect') {
if (data.userCommentActivity.status && data.userCommentActivity.status === 'disconnect') {
this.users = this.users.filter((u) => u.uuid !== data.userCommentActivity.uuid)
this.updateBubbles(true)
return
@@ -225,10 +225,15 @@ export default {
controls._zoom
]
let selectionLocation = this.selectionLocation
if (this.$store.state.selectedComment) {
selectionLocation = this.$store.state.selectedComment.data.location
}
let data = {
filter: this.$store.state.appliedFilter,
selection: this.selectedIds,
selectionLocation: this.selectionLocation,
selectionLocation,
sectionBox: window.__viewer.sectionBox.getCurrentBox(),
selectionCenter: this.selectionCenter,
camera: c,
@@ -16,7 +16,11 @@
class="no-mouse"
>
<v-slide-x-transition>
<div v-show="visible" ref="commentButton" class="absolute-pos">
<div
v-show="visible && !$store.state.selectedComment"
ref="commentButton"
class="absolute-pos"
>
<div class="d-flex align-center" style="height: 48px; width: 320px">
<v-btn
v-tooltip="!expand ? 'Add a comment (ctrl + shift + c)' : 'Cancel'"
@@ -62,7 +66,7 @@
<portal to="viewercontrols" :order="100">
<v-slide-x-transition>
<v-btn
v-show="!location"
v-show="!location && !$store.state.selectedComment"
v-tooltip="'Add a comment (ctrl + shift + c)'"
icon
dark
@@ -115,6 +119,7 @@ export default {
let commentInput = {
streamId: this.$route.params.streamId,
resources: [
{ resourceType: 'stream', resourceId: this.$route.params.streamId },
{
resourceType: this.$route.path.includes('object') ? 'object' : 'commit',
resourceId: this.$route.params.resourceId
@@ -126,11 +131,11 @@ export default {
? this.location
: new THREE.Vector3(camTarget.x, camTarget.y, camTarget.z),
camPos: getCamArray(),
filters: null, // TODO
sectionBox: null, // TODO
selection: null, // TODO
screenshot: null // TODO
}
filters: this.$store.state.appliedFilter,
sectionBox: window.__viewer.sectionBox.getCurrentBox(),
selection: null // TODO for later, lazy now
},
screenshot: window.__viewer.interactions.screenshot()
}
if (this.$route.query.overlay) {
commentInput.resources.push(
@@ -5,18 +5,20 @@
<div
ref="parent"
style="width: 100%; height: 100vh; position: absolute; pointer-events: none; overflow: hidden"
class="d-flex align-center justify-center no-mouse-parent"
class="d-flex align-center justify-center no-mouse"
>
<div v-show="showComments">
<div
v-show="showComments"
style="width: 100%; height: 100vh; position: absolute; pointer-events: none; overflow: hidden"
class="no-mouse"
>
<!-- Comment bubbles -->
<div
v-for="(comment, index) in localComments"
:key="index"
:ref="`comment-${index}`"
:class="`absolute-pos rounded-xl`"
:style="`pointer-events: none; transition: opacity 0.2s ease; z-index:${
comment.expanded ? '20' : '10'
}; ${
v-for="comment in localComments"
:key="comment.id"
:ref="`comment-${comment.id}`"
:class="`absolute-pos rounded-xl no-mouse`"
:style="`transition: opacity 0.2s ease; z-index:${comment.expanded ? '20' : '10'}; ${
hasExpandedComment && !comment.expanded && !comment.hovered
? 'opacity: 0.1;'
: 'opacity: 1;'
@@ -27,10 +29,12 @@
<div class="" style="pointer-events: none">
<div class="d-flex align-center" style="pointer-events: none">
<v-btn
:ref="`comment-button-${comment.id}`"
v-tooltip="comment.expanded ? 'Close comment thread' : 'Open comment thread'"
small
icon
:class="`elevation-5 pa-0 ma-0 ${
comment.expanded ? 'dark white--text primary' : 'background'
:class="`elevation-5 pa-0 ma-0 mouse ${
comment.expanded || comment.bouncing ? 'dark white--text primary' : 'background'
}`"
@click="toggleComment(comment)"
>
@@ -47,9 +51,9 @@
</div>
<!-- Comment Threads -->
<div
v-for="(comment, index) in localComments"
:key="index + 'card'"
:ref="`commentcard-${index}`"
v-for="comment in localComments"
:key="comment.id + '-card'"
:ref="`commentcard-${comment.id}`"
:class="`hover-bg absolute-pos rounded-xl overflow-y-auto ${
comment.hovered && false ? 'background elevation-5' : ''
}`"
@@ -60,7 +64,11 @@
<!-- <v-card class="elevation-0 ma-0 transparent" style="height: 100%"> -->
<v-fade-transition>
<div v-show="comment.expanded">
<comment-thread-viewer :comment="comment" @reply-added="replyAdded" />
<comment-thread-viewer
:comment="comment"
@bounce="bounceComment"
@refresh-layout="updateCommentBubbles()"
/>
</div>
</v-fade-transition>
<!-- </v-card> -->
@@ -132,6 +140,7 @@ export default {
for (let c of data.comments.items) {
c.expanded = false
c.hovered = false
c.bouncing = false
if (this.localComments.findIndex((lc) => c.id === lc.id) === -1)
this.localComments.push({ ...c })
}
@@ -155,10 +164,15 @@ export default {
},
result({ data }) {
// console.log(data.commentCreated)
if (!data.commentCreated) return
data.commentCreated.expanded = false
data.commentCreated.hovered = false
data.commentCreated.bouncing = false
this.localComments.push(data.commentCreated)
setTimeout(this.updateCommentBubbles, 0)
setTimeout(() => {
this.updateCommentBubbles()
this.bounceComment(data.commentCreated.id)
}, 10)
}
}
}
@@ -185,6 +199,7 @@ export default {
for (let c of this.localComments) {
c.expanded = false
}
this.$store.commit('setCommentSelection', { comment: null })
}.bind(this),
10
)
@@ -201,6 +216,7 @@ export default {
for (let c of this.localComments) {
if (c.id === comment.id && comment.expanded === false) {
c.preventAutoClose = true
this.$store.commit('setCommentSelection', { comment: c })
this.setCommentPow(c)
setTimeout(() => {
c.expanded = true
@@ -213,6 +229,7 @@ export default {
// this.updateCommentBubbles()
}, 1000)
} else {
if (c.expanded) this.$store.commit('setCommentSelection', { comment: null })
c.expanded = false
}
}
@@ -252,33 +269,48 @@ export default {
if (camToSet[6] === 1) {
window.__viewer.cameraHandler.activeCam.controls.zoom(camToSet[7], true)
}
if (comment.data.filters) {
this.$store.commit('setFilterDirect', { filter: comment.data.filters })
} else {
this.$store.commit('resetFilters')
}
if (comment.data.sectionBox) {
window.__viewer.sectionBox.setBox(comment.data.sectionBox, 0)
} else {
// TODO: Toggle section box off
// window.__viewer.sectionBox
}
// TODO: apply filters, section box, etc.
},
replyAdded() {
this.updateCommentBubbles()
},
updateCommentBubbles() {
if (!this.comments) return
let index = -1
let cam = window.__viewer.cameraHandler.camera
cam.updateProjectionMatrix()
for (let comment of this.localComments) {
index++
let commentEl = this.$refs[`comment-${index}`][0]
// get html elements
let commentEl = this.$refs[`comment-${comment.id}`][0]
let card = this.$refs[`commentcard-${comment.id}`][0]
if (!commentEl) continue
let location = new THREE.Vector3(
comment.data.location.x,
comment.data.location.y,
comment.data.location.z
)
location.project(cam)
let commentLocation = new THREE.Vector3(
(location.x * 0.5 + 0.5) * this.$refs.parent.clientWidth,
(location.y * -0.5 + 0.5) * this.$refs.parent.clientHeight,
0
)
let tX = commentLocation.x - 20
let tY = commentLocation.y - 20
const paddingX = 10
const paddingYTop = 70
const paddingYBottom = 90
@@ -304,13 +336,18 @@ export default {
tY = this.$refs.parent.clientHeight - paddingYBottom
}
commentEl.style.transform = `translate(${tX}px,${tY}px)`
commentEl.style.top = `${tY}px`
commentEl.style.left = `${tX}px`
let card = this.$refs[`commentcard-${index}`][0]
let maxHeight = this.$refs.parent.clientHeight - paddingYTop - paddingYBottom
card.style.maxHeight = `${maxHeight}px`
if (tX > this.$refs.parent.clientWidth - (paddingX + 50 + card.scrollWidth)) {
tX = this.$refs.parent.clientWidth - (paddingX + 50 + card.scrollWidth)
}
card.style.left = `${tX + 40}px`
// card.style.right = '0px'
let cardTop = paddingYTop
@@ -334,6 +371,16 @@ export default {
card.style.top = `${cardTop}px`
}
}
},
bounceComment(id) {
let commentEl = this.$refs[`comment-${id}`][0]
commentEl.classList.add('tada-once')
let comment = this.localComments.find((c) => c.id === id)
comment.bouncing = true
setTimeout(() => {
commentEl.classList.remove('tada-once')
comment.bouncing = false
}, 2000)
}
}
}
@@ -364,4 +411,10 @@ export default {
.hover-bg {
transition: background 0.3s ease;
}
.no-mouse {
pointer-events: none;
}
.mouse {
pointer-events: auto;
}
</style>
@@ -136,7 +136,7 @@ export default {
this.parseAndSetFilters()
}
},
'$store.state.appliedFilter'(val) {
'$store.state.appliedFilter'() {
if (this.trySetPresetFilter) return
if (this.$store.state.appliedFilter && this.$store.state.appliedFilter.filterBy) {
let key = Object.keys(this.$store.state.appliedFilter.filterBy)[0]
@@ -4,10 +4,8 @@
<v-btn
v-show="showVisReset"
v-tooltip="`Resets all applied filters`"
:zzzdisabled="!showVisReset"
:small="small"
small
rounded
icon
class="mr-2"
@click="resetVisibility()"
>
+6 -1
View File
@@ -17,9 +17,14 @@ const store = new Vuex.Store({
isolateCategoryKey: null,
isolateCategoryValues: [],
hideCategoryKey: null,
hideCategoryValues: []
hideCategoryValues: [],
selectedComment: null
},
mutations: {
setCommentSelection(state, { comment }) {
if (comment) window.__viewer.interactions.deselectObjects()
state.selectedComment = comment
},
isolateObjects(state, { filterKey, filterValues }) {
state.hideKey = null
state.hideValues = []
+67
View File
@@ -26,6 +26,73 @@ $primary-gradient: linear-gradient(0deg, $primary-darken 0%, $primary-base 40%);
background: linear-gradient(to top left, #243b55, #141e30) !important;
}
// TADAs
.tada-infinte {
-webkit-animation-name: tada;
animation-name: tada;
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
animation-iteration-count: infinite;
}
.tada-once {
-webkit-animation-name: tada;
animation-name: tada;
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
animation-iteration-count: 2;
}
@-webkit-keyframes tada {
0% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%, 20% {
-webkit-transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg);
transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg);
}
30%, 50%, 70%, 90% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
}
40%, 60%, 80% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
}
100% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
@keyframes tada {
0% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%, 20% {
-webkit-transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg);
transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg);
}
30%, 50%, 70%, 90% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
}
40%, 60%, 80% {
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
}
100% {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
/* TOOLTIPs */
.tooltip {
@@ -54,11 +54,11 @@ module.exports = {
// TODO: check perms, persist comment
await authorizeResolver( context.userId, args.input.streamId, 'stream:reviewer' )
let id = await createComment( { userId: context.userId, input: args.input } )
console.log( args.input )
// console.log( args.input )
await pubsub.publish( 'COMMENT_CREATED', {
commentCreated: { ...args.input, authorId: context.userId, createdAt: Date.now() },
commentCreated: { ...args.input, authorId: context.userId, id, createdAt: Date.now() },
streamId: args.input.streamId,
resourceId: args.input.resources[0].resourceId // TODO: hack for now
resourceId: args.input.resources[1].resourceId // TODO: hack for now
} )
return id
},
@@ -70,14 +70,15 @@ module.exports = {
await authorizeResolver( context.userId, args.input.streamId, 'stream:reviewer' )
// the reply also has to be linked to the stream, for the recursive reply lookup to work
let input = { ...args.input, resources: [
{ id: args.input.parentComment, type: 'comment' },
{ id: args.input.streamId, type: 'stream' }
{ resourceId: args.input.parentComment, resourceType: 'comment' },
{ resourceId: args.input.streamId, resourceType: 'stream' }
] }
// console.log(input.resources)
let id = await createComment( { userId: context.userId, input } )
await pubsub.publish( 'COMMENT_REPLY_CREATED', {
commentCreated: args.input,
streamId: args.streamId,
resourceId: args.resourceId
commentReplyCreated: { ...args.input, id, authorId: context.userId, createdAt: Date.now() },
streamId: args.input.streamId,
commentId: args.input.parentComment
} )
return id
},
@@ -101,7 +102,10 @@ module.exports = {
commentReplyCreated: {
subscribe: withFilter( () => pubsub.asyncIterator( [ 'COMMENT_REPLY_CREATED' ] ), async( payload, variables, context ) => {
await authorizeResolver( context.userId, payload.streamId, 'stream:reviewer' )
return payload.streamId === variables.streamId && payload.resourceId === variables.resourceId
console.log( 'sub' )
console.log( payload )
console.log( variables )
return payload.streamId === variables.streamId && payload.commentId === variables.commentId
} )
}
}
@@ -62,7 +62,7 @@ input ReplyCreateInput {
streamId: String!
parentComment: String!
text: String!
data: JSONObject!
data: JSONObject
}
input CommentEditInput {
@@ -5,7 +5,7 @@ exports.up = async ( knex ) => {
table.string( 'authorId', 10 ).references( 'id' ).inTable( 'users' ).notNullable().index( )
table.timestamp( 'createdAt' ).defaultTo( knex.fn.now( ) )
table.timestamp( 'updatedAt' ).defaultTo( knex.fn.now( ) )
table.string( 'text' )
table.text( 'text' )
table.text( 'screenshot' )
table.jsonb( 'data' )
table.boolean( 'archived' ).defaultTo( false ).notNullable()
@@ -53,8 +53,9 @@ const getCommentLinksForResources = async ( streamId, resources ) => {
module.exports = {
async createComment( { userId, input } ) {
console.log(input)
const streamResources = input.resources.filter( r => r.resourceType === 'stream' )
if ( streamResources.length < 1 ) throw Error( 'Must specify atleast a stream as the comment target' )
if ( streamResources.length < 1 ) throw Error( 'Must specify at least a stream as the comment target' )
if ( streamResources.length > 1 ) throw Error( 'Commenting on multiple streams is not supported' )
const [ stream ] = streamResources