feat(comments): various (back + front)

updates `updatedAt` of parent comment on reply; shuffles events; finalises various interactions
This commit is contained in:
Dimitrie Stefanescu
2022-03-12 18:27:25 +00:00
parent 5be650bf6d
commit 9e2cfa2c39
9 changed files with 171 additions and 88 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'
Vue.use(PerfectScrollbar)
import VTooltip from 'v-tooltip'
Vue.use(VTooltip, { defaultDelay: 300 })
Vue.use(VTooltip, { defaultDelay: 300, defaultBoundariesElement: document.body })
import VueMatomo from 'vue-matomo'
@@ -12,8 +12,13 @@
>
<v-icon>mdi-minus</v-icon>
</v-btn> -->
<v-btn icon class="primary dark ml-2 elevation-10" @click="$emit('close', comment)">
<v-icon>mdi-close</v-icon>
<v-btn
icon
small
class="primary dark white--text ml-2 elevation-10"
@click="$emit('close', comment)"
>
<v-icon small>mdi-close</v-icon>
</v-btn>
</div>
<div v-show="!minimise" style="width: 100%">
@@ -51,17 +56,19 @@
@click:append="addReply"
@keydown.enter.shift.exact.prevent="addReply()"
></v-textarea>
<v-btn
v-tooltip="'Marks this thread as archived.'"
class="float-right"
x-small
rounded
depressed
color="error"
@click="showArchiveDialog = true"
>
Archive
</v-btn>
<div class="text-right">
<v-btn
v-tooltip="'Marks this thread as archived.'"
class="white--text mt-2 mr-2"
small
icon
depressed
color="error"
@click="showArchiveDialog = true"
>
<v-icon small>mdi-delete-outline</v-icon>
</v-btn>
</div>
<v-dialog v-model="showArchiveDialog" max-width="500">
<v-card>
<v-toolbar color="error" dark flat>
@@ -229,6 +236,11 @@ export default {
}
setTimeout(() => {
// Shhh.
// eslint-disable-next-line vue/no-mutating-props
this.comment.replies.totalCount++
// eslint-disable-next-line vue/no-mutating-props
this.comment.updatedAt = Date.now()
this.$emit('refresh-layout') // needed for layout reshuffle in parent
}, 100)
},
@@ -5,50 +5,71 @@
$store.state.selectedComment || $store.state.addingComment ? '0.2' : '1'
};`"
>
<div
v-for="user in users"
:ref="`user-target-${user.uuid}`"
:key="user.uuid + 'target'"
:class="`absolute-pos rounded-pill primary`"
:style="` opacity: ${
user.hidden ? '0.2' : 1
}; transform-origin:center; width: 10px; height:10px; pointer-events:none`"
></div>
<div
v-for="user in users"
:ref="`user-arrow-${user.uuid}`"
:key="user.uuid + 'arrow'"
:class="`absolute-pos d-flex align-center justify-center`"
:style="`opacity: ${
user.hidden ? '0.2' : 1
}; pointer-events:none; transform-origin:center; width: 32px; height:32px; transform: rotateY(0) rotate(90deg)`"
>
<!-- <v-icon class="primary--text" style="position: relative; right: -90%">mdi-arrow-right</v-icon> -->
<!-- <v-icon class="primary--text" style="position: relative; right: -90%">mdi-pan-right</v-icon> -->
<v-icon class="primary--text" large style="position: relative; right: -60%; font-size: 4.2em">
mdi-menu-right
</v-icon>
</div>
<div
v-for="sessionUser in users"
:ref="`user-bubble-${sessionUser.uuid}`"
:key="sessionUser.uuid"
class="absolute-pos rounded-pill user-bubble elevation-5"
:style="`opacity: ${sessionUser.hidden ? '0.2' : 1}; border: 4px solid ${
$vuetify.theme.dark ? '#047EFB' : '#047EFB'
}`"
>
<div @click="setUserPow(sessionUser)">
<user-avatar
:id="sessionUser.id"
v-tooltip="sessionUser.name"
:show-hover="false"
:size="30"
:margin="false"
></user-avatar>
<text-dots-typing v-if="sessionUser.status === 'writing'" />
<div v-show="showBubbles">
<div
v-for="user in users"
:ref="`user-target-${user.uuid}`"
:key="user.uuid + 'target'"
:class="`absolute-pos rounded-pill primary`"
:style="` opacity: ${
user.hidden ? '0.2' : 1
}; transform-origin:center; width: 10px; height:10px; pointer-events:none`"
></div>
<div
v-for="user in users"
:ref="`user-arrow-${user.uuid}`"
:key="user.uuid + 'arrow'"
:class="`absolute-pos d-flex align-center justify-center`"
:style="`opacity: ${
user.hidden ? '0.2' : 1
}; pointer-events:none; transform-origin:center; width: 32px; height:32px; transform: rotateY(0) rotate(90deg)`"
>
<!-- <v-icon class="primary--text" style="position: relative; right: -90%">mdi-arrow-right</v-icon> -->
<!-- <v-icon class="primary--text" style="position: relative; right: -90%">mdi-pan-right</v-icon> -->
<v-icon
class="primary--text"
large
style="position: relative; right: -60%; font-size: 4.2em"
>
mdi-menu-right
</v-icon>
</div>
<div
v-for="sessionUser in users"
:ref="`user-bubble-${sessionUser.uuid}`"
:key="sessionUser.uuid"
class="absolute-pos rounded-pill user-bubble elevation-5"
:style="`opacity: ${sessionUser.hidden ? '0.2' : 1}; border: 4px solid ${
$vuetify.theme.dark ? '#047EFB' : '#047EFB'
}`"
>
<div @click="setUserPow(sessionUser)">
<user-avatar
:id="sessionUser.id"
v-tooltip="sessionUser.name"
:show-hover="false"
:size="30"
:margin="false"
></user-avatar>
<text-dots-typing v-if="sessionUser.status === 'writing'" />
</div>
</div>
</div>
<portal to="viewercontrols" :order="4">
<v-btn
key="bubbles-toggle-button"
v-show="users.length !== 0"
v-tooltip="`Toggle real time user bubbles`"
small
rounded
icon
class="mr-2"
@click="showBubbles = !showBubbles"
>
<v-icon v-if="showBubbles" small>mdi-account</v-icon>
<v-icon v-else small>mdi-account-off</v-icon>
</v-btn>
</portal>
</div>
</template>
<script>
@@ -137,7 +158,8 @@ export default {
selectedIds: [],
selectionLocation: null,
selectionCenter: null,
users: []
users: [],
showBubbles: true
}
},
mounted() {
@@ -23,7 +23,7 @@
>
<div
class="d-flex align-center"
:style="`height: 48px; width: ${$vuetify.breakpoint.xs ? '100vw' : '320px'}`"
:style="`height: 48px; width: ${$vuetify.breakpoint.xs ? '90vw' : '320px'}`"
>
<v-btn
v-tooltip="!expand ? 'Add a comment (ctrl + shift + c)' : 'Cancel'"
@@ -37,7 +37,7 @@
<v-icon v-else dark x-small>mdi-close</v-icon>
</v-btn>
<v-slide-x-transition>
<div v-if="expand" style="width: 100%" class="d-flex">
<div v-if="expand && !$vuetify.breakpoint.xs" style="width: 100%" class="d-flex">
<v-textarea
v-model="commentText"
solo
@@ -64,6 +64,38 @@
</div>
</v-slide-x-transition>
</div>
<v-dialog
v-if="$vuetify.breakpoint.xs"
v-model="expand"
class="elevation-0 flat"
@input="toggleExpand()"
>
<div class="d-flex justify-center" style="position: relative; left: 24px">
<v-textarea
v-model="commentText"
solo
hide-details
autofocus
auto-grow
rows="1"
placeholder="Your comment..."
class="mouse rounded-xl caption elevation-15"
append-icon="mdi-send"
@keydown.enter.shift.exact.prevent="addComment()"
></v-textarea>
<v-btn
v-tooltip="'Send comment (shift + enter)'"
icon
dark
large
class="mouse elevation-0 primary pa-0 ma-o"
style="left: -47px; top: 1px; height: 48px; width: 48px"
@click="addComment()"
>
<v-icon dark small>mdi-send</v-icon>
</v-btn>
</div>
</v-dialog>
</div>
</v-slide-x-transition>
<portal to="viewercontrols" :order="100">
@@ -238,6 +270,11 @@ export default {
}
</script>
<style scoped>
::v-deep .v-dialog {
box-shadow: none;
overflow-y: hidden;
overflow-x: hidden;
}
.no-mouse {
pointer-events: none;
}
@@ -19,7 +19,7 @@
: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
hasExpandedComment && !comment.expanded && !comment.hovered && !comment.bouncing
? 'opacity: 0.1;'
: 'opacity: 1;'
}`"
@@ -29,8 +29,8 @@
<div class="" style="pointer-events: none">
<div class="d-flex align-center" style="pointer-events: none">
<v-btn
v-show="!($vuetify.breakpoint.xs && comment.expanded)"
:ref="`comment-button-${comment.id}`"
v-tooltip="comment.expanded ? 'Close comment thread' : 'Open comment thread'"
small
icon
:class="`elevation-5 pa-0 ma-0 mouse ${
@@ -38,14 +38,26 @@
}`"
@click="comment.expanded ? collapseComment(comment) : expandComment(comment)"
>
<!-- @click="comment.expanded ? (comment.expanded = false) : expandComment(comment)" -->
<!-- <span v-if="!comment.expanded" class="primary--text">
<v-icon small>mdi-comment</v-icon>
1
</span> -->
<v-icon v-if="!comment.expanded" x-small class="">mdi-comment</v-icon>
<v-icon v-if="comment.expanded" x-small class="">mdi-close</v-icon>
</v-btn>
<v-slide-x-transition>
<div
v-if="comment.hovered && !comment.expanded"
style="position: absolute; left: 30px; width: max-content"
class="rounded-xl primary white--text px-2 ml-1 caption"
>
<timeago :datetime="comment.updatedAt" class="font-italic mr-2"></timeago>
<v-icon x-small class="white--text">mdi-comment-outline</v-icon>
{{ comment.replies.totalCount + 1 }}
<v-icon v-if="comment.data.filters" x-small class="white--text">
mdi-filter-variant
</v-icon>
<v-icon v-if="comment.data.sectionBox" x-small class="white--text">
mdi-scissors-cutting
</v-icon>
</div>
</v-slide-x-transition>
</div>
</div>
</div>
@@ -78,6 +90,7 @@
</div>
<portal to="viewercontrols" :order="5">
<v-btn
key="comment-toggle-button"
v-tooltip="`Toggle comments (${localComments.length})`"
rounded
icon
@@ -110,7 +123,11 @@ export default {
authorId
text
createdAt
updatedAt
data
replies {
totalCount
}
}
}
}
@@ -165,7 +182,7 @@ export default {
result({ data }) {
if (!data.commentActivity) return
// Creation
if (data.commentActivity.action === 'created') {
if (data.commentActivity.eventType === 'comment-added') {
data.commentActivity.expanded = false
data.commentActivity.hovered = false
data.commentActivity.bouncing = false
@@ -175,10 +192,6 @@ export default {
this.bounceComment(data.commentActivity.id)
}, 10)
}
// Deletion
if (data.commentActivity.action === 'deleted') {
// TODO
}
}
}
}
@@ -3,7 +3,7 @@
<v-card
class="elevation-5 rounded-xl pl-3 py-0 d-flex align-center"
height="44"
style="max-width: 100vw; overflow-x: scroll"
style="max-width: 90vw; overflow-x: scroll;"
>
<v-btn
v-show="showVisReset"
@@ -16,7 +16,8 @@
<v-icon small class="mr-2">mdi-eye</v-icon>
Reset Filters
</v-btn>
<v-btn
<!-- disabling ortho mode because comment intersection are f*ed. -->
<!-- <v-btn
v-tooltip="`Toggle between perspective or ortho camera.`"
:small="small"
rounded
@@ -25,7 +26,7 @@
@click="toggleCamera()"
>
<v-icon small>mdi-perspective-less</v-icon>
</v-btn>
</v-btn> -->
<canonical-views :small="small" />
<v-btn v-tooltip="'Zoom extents'" :small="small" rounded icon class="mr-2" @click="zoomEx()">
<v-icon small>mdi-arrow-expand</v-icon>
@@ -40,18 +41,8 @@
>
<v-icon small>mdi-scissors-cutting</v-icon>
</v-btn>
<v-btn
v-tooltip="`Toggle real time user bubbles`"
:small="small"
rounded
icon
class="mr-2"
@click="sectionToggle()"
>
<v-icon small>mdi-account</v-icon>
</v-btn>
<!-- Other components teleport extra controls in here -->
<portal-target name="viewercontrols" multiple></portal-target>
<portal-target name="viewercontrols" class="d-flex align-center" multiple></portal-target>
</v-card>
</div>
</template>
@@ -100,6 +100,7 @@ $primary-gradient: linear-gradient(0deg, $primary-darken 0%, $primary-base 40%);
z-index: 10000;
font-family: 'Roboto', sans-serif !important;
font-size: 0.75rem !important;
pointer-events: none;
}
.tooltip .tooltip-inner {
@@ -56,7 +56,7 @@ module.exports = {
let id = await createComment( { userId: context.userId, input: args.input } )
// console.log( args.input )
await pubsub.publish( 'COMMENT_ACTIVITY', {
commentActivity: { ...args.input, authorId: context.userId, id, createdAt: Date.now(), action: 'created' },
commentActivity: { ...args.input, authorId: context.userId, id, replies: { totalCount: 0 }, updatedAt: Date.now(), createdAt: Date.now(), eventType: 'comment-added' },
streamId: args.input.streamId,
resourceId: args.input.resources[1].resourceId // TODO: hack for now
} )
@@ -66,6 +66,7 @@ module.exports = {
// TODO
},
async commentArchive( parent, args, context, info ) {
await authorizeStreamAccess( { streamId: args.streamId, userId: context.userId, auth: context.auth } )
await archiveComment( { ...args } )
await pubsub.publish( 'COMMENT_THREAD_ACTIVITY', {
commentThreadActivity: { eventType: 'comment-archived' },
@@ -85,7 +86,7 @@ module.exports = {
// console.log(input.resources)
let id = await createComment( { userId: context.userId, input } )
await pubsub.publish( 'COMMENT_THREAD_ACTIVITY', {
commentThreadActivity: { eventType: 'reply-added', ...args.input, id, authorId: context.userId, createdAt: Date.now() },
commentThreadActivity: { eventType: 'reply-added', ...args.input, id, authorId: context.userId, updatedAt: Date.now(), createdAt: Date.now() },
streamId: args.input.streamId,
commentId: args.input.parentComment
} )
@@ -73,10 +73,12 @@ module.exports = {
const streamResources = input.resources.filter( r => r.resourceType === 'stream' )
if ( streamResources.length > 1 ) throw Error( 'Commenting on multiple streams is not supported' )
const [ stream ] = streamResources
if ( stream.resourceId !== input.streamId ) throw Error( 'Input streamId doesn\'t match the stream resource.resourceId' )
const commentResource = input.resources.find( r => r.resourceType === 'comment' )
let comment = { ...input }
delete comment.resources
@@ -88,6 +90,10 @@ module.exports = {
await Comments().insert( comment )
await persistResourceLinks( comment.id, input.resources )
if ( commentResource ) {
await Comments().where( { id: commentResource.resourceId } ).update( { updatedAt: knex.fn.now( ) } )
}
return comment.id
},