2017-07-21 17:00:36 +00:00
< template lang = "pug" >
2017-08-14 21:44:09 +00:00
. container
2017-07-21 17:00:36 +00:00
. row
2017-08-14 21:44:09 +00:00
. col - 12
copy - as - todo - modal ( : copying - message = 'copyingMessage' , : group - name = 'groupName' , : group - id = 'groupId' )
report - flag - modal
2017-07-29 22:08:36 +00:00
2017-09-29 21:03:43 +00:00
div ( v - for = "(msg, index) in messages" , v - if = 'chat && canViewFlag(msg)' )
2017-08-14 21:44:09 +00:00
// @TODO: is there a different way to do these conditionals? This creates an infinite loop
//.hr(v-if='displayDivider(msg)')
. hr - middle ( v - once ) { { msg . timestamp } }
2017-08-18 18:30:39 +00:00
. row ( v - if = 'user._id !== msg.uuid' )
2017-09-25 18:02:12 +00:00
div ( : class = 'inbox ? "col-4" : "col-2"' )
2017-09-21 17:49:27 +00:00
avatar (
2017-09-29 21:03:43 +00:00
v - if = 'cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected' ,
2017-09-23 00:30:37 +00:00
: member = "cachedProfileData[msg.uuid]" ,
2017-09-21 17:49:27 +00:00
: avatarOnly = "true" ,
: hideClassBadge = 'true' ,
@ click . native = "showMemberModal(msg.uuid)" ,
)
2017-09-25 18:02:12 +00:00
. card ( : class = 'inbox ? "col-8" : "col-10"' )
. mentioned - icon ( v - if = 'isUserMentioned(msg)' )
2017-09-25 23:33:46 +00:00
. message - hidden ( v - if = 'msg.flagCount === 1 && user.contributor.admin' ) Message flagged once , not hidden
. message - hidden ( v - if = 'msg.flagCount > 1 && user.contributor.admin' ) Message hidden
2017-11-09 18:37:47 +00:00
. card - body
2017-09-21 17:49:27 +00:00
h3 . leader (
: class = 'userLevelStyle(cachedProfileData[msg.uuid])'
@ click = "showMemberModal(msg.uuid)" ,
)
2017-08-29 20:23:09 +00:00
| { { msg . user } }
2017-08-30 21:19:17 +00:00
. svg - icon ( v - html = "icons[`tier${cachedProfileData[msg.uuid].contributor.level}`]" , v - if = 'cachedProfileData[msg.uuid] && cachedProfileData[msg.uuid].contributor && cachedProfileData[msg.uuid].contributor.level' )
2017-09-25 18:02:12 +00:00
p . time { { msg . timestamp | timeAgo } }
2017-08-18 18:30:39 +00:00
. text ( v - markdown = 'msg.text' )
hr
. action ( @ click = 'like(msg, index)' , v - if = 'msg.likes' , : class = '{active: msg.likes[user._id]}' )
. svg - icon ( v - html = "icons.like" )
span ( v - if = '!msg.likes[user._id]' ) { { $t ( 'like' ) } }
span ( v - if = 'msg.likes[user._id]' ) { { $t ( 'liked' ) } }
2017-10-01 12:15:44 +00:00
// @TODO make copyAsTodo work in Tavern, guilds, party (inbox can be done later)
span . action ( v - if = '!inbox' , @ click = 'copyAsTodo(msg)' )
. svg - icon ( v - html = "icons.copy" )
| { { $t ( 'copyAsTodo' ) } }
// @TODO make copyAsTodo work in the inbox
2017-09-29 20:56:44 +00:00
span . action ( v - if = '!inbox && user.flags.communityGuidelinesAccepted' , @ click = 'report(msg)' )
2017-08-18 18:30:39 +00:00
. svg - icon ( v - html = "icons.report" )
| { { $t ( 'report' ) } }
2017-09-29 20:56:44 +00:00
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
span . action ( v - if = 'msg.uuid === user._id || inbox || user.contributor.admin' , @ click = 'remove(msg, index)' )
2017-08-18 18:30:39 +00:00
. svg - icon ( v - html = "icons.delete" )
| { { $t ( 'delete' ) } }
2017-09-25 18:02:12 +00:00
span . action . float - right . liked ( v - if = 'likeCount(msg) > 0' )
2017-08-18 18:30:39 +00:00
. svg - icon ( v - html = "icons.liked" )
| + { { likeCount ( msg ) } }
2017-09-21 17:49:27 +00:00
// @TODO can we avoid duplicating all this code? Cannot we just push everything
// to the right if the user is the author?
2017-09-25 18:02:12 +00:00
// Maybe we just create two sub components instead
2017-08-18 18:30:39 +00:00
. row ( v - if = 'user._id === msg.uuid' )
2017-09-25 18:02:12 +00:00
. card ( : class = 'inbox ? "col-8" : "col-10"' )
. mentioned - icon ( v - if = 'isUserMentioned(msg)' )
2017-09-25 23:33:46 +00:00
. message - hidden ( v - if = 'msg.flagCount === 1 && user.contributor.admin' ) Message flagged once , not hidden
. message - hidden ( v - if = 'msg.flagCount > 1 && user.contributor.admin' ) Message hidden
2017-11-09 18:37:47 +00:00
. card - body
2017-09-21 17:49:27 +00:00
h3 . leader (
: class = 'userLevelStyle(cachedProfileData[msg.uuid])' ,
@ click = "showMemberModal(msg.uuid)" ,
)
2017-08-29 20:23:09 +00:00
| { { msg . user } }
. svg - icon ( v - html = "icons[`tier${cachedProfileData[msg.uuid].contributor.level}`]" , v - if = 'cachedProfileData[msg.uuid] && cachedProfileData[msg.uuid].contributor && cachedProfileData[msg.uuid].contributor.level' )
2017-09-25 18:02:12 +00:00
p . time { { msg . timestamp | timeAgo } }
2017-08-18 18:30:39 +00:00
. text ( v - markdown = 'msg.text' )
hr
. action ( @ click = 'like(msg, index)' , v - if = 'msg.likes' , : class = '{active: msg.likes[user._id]}' )
. svg - icon ( v - html = "icons.like" )
span ( v - if = '!msg.likes[user._id]' ) { { $t ( 'like' ) } }
span ( v - if = 'msg.likes[user._id]' ) { { $t ( 'liked' ) } }
2017-10-01 12:15:44 +00:00
// @TODO make copyAsTodo work in Tavern, guilds, party (inbox can be done later)
span . action ( v - if = '!inbox' , @ click = 'copyAsTodo(msg)' )
. svg - icon ( v - html = "icons.copy" )
| { { $t ( 'copyAsTodo' ) } }
// @TODO make copyAsTodo work in the inbox
2017-09-25 23:33:46 +00:00
span . action ( v - if = 'user.flags.communityGuidelinesAccepted' , @ click = 'report(msg)' )
2017-09-29 20:56:44 +00:00
span . action ( v - if = '!inbox && user.flags.communityGuidelinesAccepted' , @ click = 'report(msg)' )
2017-08-18 18:30:39 +00:00
. svg - icon ( v - html = "icons.report" )
| { { $t ( 'report' ) } }
2017-09-29 20:56:44 +00:00
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
2017-08-18 18:30:39 +00:00
span . action ( v - if = 'msg.uuid === user._id' , @ click = 'remove(msg, index)' )
. svg - icon ( v - html = "icons.delete" )
| { { $t ( 'delete' ) } }
2017-09-25 18:02:12 +00:00
span . action . float - right . liked ( v - if = 'likeCount(msg) > 0' )
2017-08-18 18:30:39 +00:00
. svg - icon ( v - html = "icons.liked" )
| + { { likeCount ( msg ) } }
2017-09-25 18:02:12 +00:00
div ( : class = 'inbox ? "col-4" : "col-2"' )
2017-09-21 17:49:27 +00:00
avatar (
2017-09-29 21:03:43 +00:00
v - if = 'cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected' ,
2017-09-23 00:30:37 +00:00
: member = "cachedProfileData[msg.uuid]" ,
2017-09-21 17:49:27 +00:00
: avatarOnly = "true" ,
: hideClassBadge = 'true' ,
@ click . native = "showMemberModal(msg.uuid)" ,
)
2017-07-21 17:00:36 +00:00
< / template >
< style lang = "scss" scoped >
@ import '~client/assets/scss/colors.scss' ;
2017-09-21 17:49:27 +00:00
// @TODO: Move this to an scss
2017-08-29 20:23:09 +00:00
. tier1 {
color : # c42870 ;
}
. tier2 {
color : # b01515 ;
}
. tier3 {
color : # d70e14 ;
}
. tier4 {
color : # c24d00 ;
}
. tier5 {
color : # 9 e650f ;
}
. tier6 {
color : # 2 b8363 ;
}
. tier7 {
color : # 167 e87 ;
}
2017-08-30 21:19:17 +00:00
. tier8 {
2017-08-29 20:23:09 +00:00
color : # 277 eab ;
}
2017-08-31 22:14:09 +00:00
. tier9 {
2017-08-29 20:23:09 +00:00
color : # 6133 b4 ;
}
2017-08-31 22:14:09 +00:00
. tier10 {
2017-08-30 21:19:17 +00:00
color : # 77 f4c7 ;
fill : # 77 f4c7 ;
stroke : # 005737 ;
2017-08-29 20:23:09 +00:00
}
// End of tier colors
2017-09-25 18:02:12 +00:00
. leader {
margin - bottom : 0 ;
}
. time {
font - size : 12 px ;
color : # 878190 ;
}
. mentioned - icon {
width : 16 px ;
height : 16 px ;
border - radius : 50 % ;
background - color : # bda8ff ;
box - shadow : 0 1 px 1 px 0 rgba ( 26 , 24 , 29 , 0.12 ) ;
position : absolute ;
right : - .5 em ;
top : - .5 em ;
}
2017-09-21 17:49:27 +00:00
h3 { // this is the user name
cursor : pointer ;
2017-08-29 20:23:09 +00:00
. svg - icon {
width : 10 px ;
display : inline - block ;
margin - left : .5 em ;
}
}
2017-07-29 22:08:36 +00:00
. hr {
width : 100 % ;
height : 20 px ;
border - bottom : 1 px solid $gray - 500 ;
text - align : center ;
margin : 2 em 0 ;
}
. hr - middle {
font - size : 16 px ;
font - weight : bold ;
font - family : 'Roboto Condensed' ;
line - height : 1.5 ;
text - align : center ;
color : $gray - 200 ;
background - color : $gray - 700 ;
padding : .2 em ;
margin - top : .2 em ;
display : inline - block ;
width : 100 px ;
}
2017-07-21 17:00:36 +00:00
. card {
2017-08-18 18:30:39 +00:00
margin - bottom : .5 em ;
2017-07-21 17:00:36 +00:00
}
. text {
font - size : 14 px ;
color : # 4 e4a57 ;
2017-09-25 18:02:12 +00:00
text - align : left ! important ;
2017-07-21 17:00:36 +00:00
}
. action {
display : inline - block ;
color : # 878190 ;
margin - right : 1 em ;
}
. action : hover {
cursor : pointer ;
}
2017-09-25 18:02:12 +00:00
. liked : hover {
cursor : default ;
}
2017-07-21 17:00:36 +00:00
. action . svg - icon {
margin - right : .2 em ;
width : 16 px ;
display : inline - block ;
color : # A5A1AC ;
}
. action . active , . active . svg - icon {
color : # 46 a7d9
}
2017-09-06 23:28:52 +00:00
. message - hidden {
margin - left : 1.5 em ;
margin - top : 1 em ;
color : red ;
}
2017-07-21 17:00:36 +00:00
< / style >
< script >
2017-08-14 21:44:09 +00:00
import axios from 'axios' ;
2017-07-21 17:00:36 +00:00
import moment from 'moment' ;
2017-08-03 20:04:03 +00:00
import cloneDeep from 'lodash/cloneDeep' ;
2017-07-21 17:00:36 +00:00
import { mapState } from 'client/libs/store' ;
2017-09-29 21:03:43 +00:00
import debounce from 'lodash/debounce' ;
2017-11-15 16:39:29 +00:00
import escapeRegExp from 'lodash/escapeRegExp' ;
2017-08-07 20:26:17 +00:00
import markdownDirective from 'client/directives/markdown' ;
2017-08-14 21:44:09 +00:00
import Avatar from '../avatar' ;
2017-08-18 18:30:39 +00:00
import styleHelper from 'client/mixins/styleHelper' ;
2017-07-21 17:00:36 +00:00
import copyAsTodoModal from './copyAsTodoModal' ;
2017-08-03 20:04:03 +00:00
import reportFlagModal from './reportFlagModal' ;
2017-07-21 17:00:36 +00:00
import deleteIcon from 'assets/svg/delete.svg' ;
import copyIcon from 'assets/svg/copy.svg' ;
import likeIcon from 'assets/svg/like.svg' ;
import likedIcon from 'assets/svg/liked.svg' ;
import reportIcon from 'assets/svg/report.svg' ;
2017-08-29 20:23:09 +00:00
import tier1 from 'assets/svg/tier-1.svg' ;
import tier2 from 'assets/svg/tier-2.svg' ;
import tier3 from 'assets/svg/tier-3.svg' ;
import tier4 from 'assets/svg/tier-4.svg' ;
import tier5 from 'assets/svg/tier-5.svg' ;
import tier6 from 'assets/svg/tier-6.svg' ;
import tier7 from 'assets/svg/tier-7.svg' ;
2017-08-30 21:19:17 +00:00
import tier8 from 'assets/svg/tier-mod.svg' ;
2017-09-05 18:34:00 +00:00
import tier9 from 'assets/svg/tier-staff.svg' ;
import tier10 from 'assets/svg/tier-npc.svg' ;
2017-07-21 17:00:36 +00:00
export default {
2017-08-26 02:56:21 +00:00
props : [ 'chat' , 'groupId' , 'groupName' , 'inbox' ] ,
2017-08-18 18:30:39 +00:00
mixins : [ styleHelper ] ,
2017-07-21 17:00:36 +00:00
components : {
copyAsTodoModal ,
2017-08-03 20:04:03 +00:00
reportFlagModal ,
2017-08-14 21:44:09 +00:00
Avatar ,
2017-07-21 17:00:36 +00:00
} ,
2017-08-07 20:26:17 +00:00
directives : {
markdown : markdownDirective ,
} ,
2017-08-14 21:44:09 +00:00
mounted ( ) {
this . loadProfileCache ( ) ;
} ,
2017-08-23 03:58:13 +00:00
created ( ) {
2017-09-29 21:03:43 +00:00
window . addEventListener ( 'scroll' , this . handleScroll ) ;
2017-08-23 03:58:13 +00:00
} ,
destroyed ( ) {
2017-09-29 21:03:43 +00:00
window . removeEventListener ( 'scroll' , this . handleScroll ) ;
2017-08-23 03:58:13 +00:00
} ,
2017-07-21 17:00:36 +00:00
data ( ) {
return {
icons : Object . freeze ( {
like : likeIcon ,
copy : copyIcon ,
report : reportIcon ,
delete : deleteIcon ,
liked : likedIcon ,
2017-08-29 20:23:09 +00:00
tier1 ,
tier2 ,
tier3 ,
tier4 ,
tier5 ,
tier6 ,
tier7 ,
2017-08-30 21:19:17 +00:00
tier8 ,
tier9 ,
tier10 ,
2017-07-21 17:00:36 +00:00
} ) ,
copyingMessage : { } ,
2017-07-29 22:08:36 +00:00
currentDayDividerDisplay : moment ( ) . day ( ) ,
2017-08-14 21:44:09 +00:00
cachedProfileData : { } ,
2017-08-23 03:58:13 +00:00
currentProfileLoadedCount : 0 ,
currentProfileLoadedEnd : 10 ,
2017-09-29 21:03:43 +00:00
loading : false ,
2017-07-21 17:00:36 +00:00
} ;
} ,
filters : {
timeAgo ( value ) {
return moment ( value ) . fromNow ( ) ;
} ,
2017-07-29 22:08:36 +00:00
date ( value ) {
// @TODO: Add user preference
return moment ( value ) . toDate ( ) ;
} ,
2017-07-21 17:00:36 +00:00
} ,
computed : {
... mapState ( { user : 'user.data' } ) ,
2017-09-29 21:03:43 +00:00
// @TODO: We need a different lazy load mechnism.
// But honestly, adding a paging route to chat would solve this
2017-08-03 20:04:03 +00:00
messages ( ) {
return this . chat ;
} ,
2017-07-21 17:00:36 +00:00
} ,
2017-08-14 21:44:09 +00:00
watch : {
2017-09-29 21:03:43 +00:00
messages ( oldValue , newValue ) {
if ( newValue . length === oldValue . length ) return ;
2017-08-14 21:44:09 +00:00
this . loadProfileCache ( ) ;
} ,
} ,
2017-07-21 17:00:36 +00:00
methods : {
2017-09-29 21:03:43 +00:00
handleScroll ( ) {
this . loadProfileCache ( window . scrollY / 1000 ) ;
} ,
2017-09-25 18:02:12 +00:00
isUserMentioned ( message ) {
let user = this . user ;
if ( message . hasOwnProperty ( 'highlight' ) ) return message . highlight ;
message . highlight = false ;
let messagetext = message . text . toLowerCase ( ) ;
let username = user . profile . name ;
let mentioned = messagetext . indexOf ( username . toLowerCase ( ) ) ;
2017-11-15 16:39:29 +00:00
let escapedUsername = escapeRegExp ( username ) ;
2017-11-16 20:12:26 +00:00
let pattern = ` @ ${ escapedUsername } ([^ \ w]| $ ){1} ` ;
2017-09-25 18:02:12 +00:00
if ( mentioned === - 1 ) return message . highlight ;
let preceedingchar = messagetext . substring ( mentioned - 1 , mentioned ) ;
if ( mentioned === 0 || preceedingchar . trim ( ) === '' || preceedingchar === '@' ) {
let regex = new RegExp ( pattern , 'i' ) ;
message . highlight = regex . test ( messagetext ) ;
}
return message . highlight ;
} ,
2017-09-06 23:28:52 +00:00
canViewFlag ( message ) {
if ( message . uuid === this . user . _id ) return true ;
2017-09-25 23:33:46 +00:00
if ( ! message . flagCount || message . flagCount < 2 ) return true ;
2017-09-06 23:28:52 +00:00
return this . user . contributor . admin ;
} ,
2017-09-29 21:03:43 +00:00
loadProfileCache : debounce ( function loadProfileCache ( screenPosition ) {
this . _loadProfileCache ( screenPosition ) ;
} , 1000 ) ,
async _loadProfileCache ( screenPosition ) {
2017-08-14 21:44:09 +00:00
let promises = [ ] ;
2017-08-23 03:58:13 +00:00
// @TODO: write an explination
if ( screenPosition && Math . floor ( screenPosition ) + 1 > this . currentProfileLoadedEnd / 10 ) {
this . currentProfileLoadedEnd = 10 * ( Math . floor ( screenPosition ) + 1 ) ;
2017-08-26 02:56:21 +00:00
} else if ( screenPosition ) {
2017-08-23 03:58:13 +00:00
return ;
}
2017-09-01 21:26:10 +00:00
let aboutToCache = { } ;
2017-08-14 21:44:09 +00:00
this . messages . forEach ( message => {
let uuid = message . uuid ;
2017-09-29 21:03:43 +00:00
if ( Boolean ( uuid ) && ! this . cachedProfileData [ uuid ] && ! aboutToCache [ uuid ] ) {
2017-08-23 03:58:13 +00:00
if ( uuid === 'system' || this . currentProfileLoadedCount === this . currentProfileLoadedEnd ) return ;
2017-09-01 21:26:10 +00:00
aboutToCache [ uuid ] = { } ;
2017-08-14 21:44:09 +00:00
promises . push ( axios . get ( ` /api/v3/members/ ${ uuid } ` ) ) ;
2017-08-23 03:58:13 +00:00
this . currentProfileLoadedCount += 1 ;
2017-08-14 21:44:09 +00:00
}
} ) ;
2017-08-22 16:26:53 +00:00
let results = await Promise . all ( promises ) ;
2017-08-14 21:44:09 +00:00
results . forEach ( result => {
2017-09-29 21:03:43 +00:00
// We could not load the user. Maybe they were deleted. So, let's cache empty so we don't try again
if ( ! result || ! result . data || result . status >= 400 ) {
return ;
}
2017-08-14 21:44:09 +00:00
let userData = result . data . data ;
this . $set ( this . cachedProfileData , userData . _id , userData ) ;
} ) ;
2017-09-29 21:03:43 +00:00
// Merge in any attempts that were rejected so we don't attempt again
for ( let uuid in aboutToCache ) {
if ( ! this . cachedProfileData [ uuid ] ) {
this . $set ( this . cachedProfileData , uuid , { rejected : true } ) ;
}
}
2017-08-14 21:44:09 +00:00
} ,
2017-07-29 22:08:36 +00:00
displayDivider ( message ) {
if ( this . currentDayDividerDisplay !== moment ( message . timestamp ) . day ( ) ) {
this . currentDayDividerDisplay = moment ( message . timestamp ) . day ( ) ;
return true ;
}
return false ;
} ,
2017-07-21 17:00:36 +00:00
likeCount ( message ) {
2017-07-29 22:08:36 +00:00
if ( ! message . likes ) return 0 ;
2017-09-25 18:02:12 +00:00
let likeCount = 0 ;
for ( let key in message . likes ) {
let like = message . likes [ key ] ;
if ( like ) likeCount += 1 ;
}
return likeCount ;
2017-07-21 17:00:36 +00:00
} ,
2017-08-03 20:04:03 +00:00
async like ( messageToLike , index ) {
let message = cloneDeep ( messageToLike ) ;
await this . $store . dispatch ( 'chat:like' , {
groupId : this . groupId ,
chatId : message . id ,
} ) ;
2017-07-21 17:00:36 +00:00
if ( ! message . likes [ this . user . _id ] ) {
message . likes [ this . user . _id ] = true ;
} else {
message . likes [ this . user . _id ] = ! message . likes [ this . user . _id ] ;
}
2017-08-03 20:04:03 +00:00
this . chat . splice ( index , 1 , message ) ;
2017-07-21 17:00:36 +00:00
} ,
copyAsTodo ( message ) {
this . copyingMessage = message ;
2017-11-08 17:40:37 +00:00
this . $root . $emit ( 'bv::show::modal' , 'copyAsTodo' ) ;
2017-07-21 17:00:36 +00:00
} ,
async report ( message ) {
2017-08-03 20:04:03 +00:00
this . $store . state . flagChatOptions . message = message ;
this . $store . state . flagChatOptions . groupId = this . groupId ;
2017-11-08 17:40:37 +00:00
this . $root . $emit ( 'bv::show::modal' , 'report-flag' ) ;
2017-07-21 17:00:36 +00:00
} ,
async remove ( message , index ) {
2017-09-25 18:02:12 +00:00
if ( ! confirm ( this . $t ( 'areYouSureDeleteMessage' ) ) ) return ;
2017-07-21 17:00:36 +00:00
this . chat . splice ( index , 1 ) ;
2017-09-21 23:53:45 +00:00
if ( this . inbox ) {
axios . delete ( ` /api/v3/user/messages/ ${ message . id } ` ) ;
2017-09-25 18:02:12 +00:00
this . $delete ( this . user . inbox . messages , message . id ) ;
2017-09-21 23:53:45 +00:00
return ;
}
2017-07-21 17:00:36 +00:00
await this . $store . dispatch ( 'chat:deleteChat' , {
groupId : this . groupId ,
chatId : message . id ,
} ) ;
} ,
2017-09-21 17:49:27 +00:00
showMemberModal ( memberId ) {
2017-10-02 21:01:27 +00:00
const profile = this . cachedProfileData [ memberId ] ;
// Open the modal only if the data is available
if ( profile && ! profile . rejected ) {
// @TODO move to action or anyway move from here because it's super duplicate
this . $store . state . profileUser = profile ;
this . $store . state . profileOptions . startingPage = 'profile' ;
2017-11-08 17:40:37 +00:00
this . $root . $emit ( 'bv::show::modal' , 'profile' ) ;
2017-10-02 21:01:27 +00:00
}
2017-09-21 17:49:27 +00:00
} ,
2017-07-21 17:00:36 +00:00
} ,
} ;
< / script >