say hello to reactions 🎉

and bug fixes...
This commit is contained in:
Aziz Hasanain 2021-02-19 16:07:17 +03:00
parent 3e11254592
commit 2779e8053a
7 changed files with 495 additions and 39 deletions

View File

@ -18,6 +18,9 @@
},
"main": "background.js",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.34",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/vue-fontawesome": "^2.0.2",
"@kevinfaguiar/vue-twemoji-picker": "^5.7.4",
"@trevoreyre/autocomplete-vue": "^2.2.0",
"auto-launch": "^5.0.5",

View File

@ -304,13 +304,12 @@ export default {
this.status = 2
},
fetchMessages (data) {
if (data && data[0] && !this.$store.state.messagesCache[data[0].chatId]) {
this.$store.commit('addMessages', { id: data[0].chatId, data: data })
if (data && data[0] && !this.$store.state.messagesCache[data[0].personId]) {
this.$store.commit('addMessages', { id: data[0].personId, data: data })
}
},
newMessage (data) {
var chatData = data.chat[0]
console.log(data)
if (chatData && chatData.personId) {
var chatIndex = this.chats.findIndex(obj => obj.personId == chatData.personId)
@ -325,9 +324,13 @@ export default {
var messageData = data.message[0]
if (messageData) {
if (this.$store.state.messagesCache[messageData.chatId]) {
if (this.$store.state.messagesCache[messageData.chatId].findIndex(obj => obj.id == messageData.id) != -1) return
this.$store.state.messagesCache[messageData.chatId].unshift(messageData)
if (this.$store.state.messagesCache[messageData.personId]) {
let oldMsgIndex = this.$store.state.messagesCache[messageData.personId].findIndex(obj => obj.guid == messageData.guid)
if (oldMsgIndex != -1) {
this.$store.state.messagesCache[messageData.personId][oldMsgIndex] = messageData
return
}
this.$store.state.messagesCache[messageData.personId].unshift(messageData)
}
if (messageData.sender != 1 && remote.Notification.isSupported()) {
@ -346,7 +349,7 @@ export default {
notif.on('click', (event, arg) => {
if (chatData && chatData.id) {
ipcRenderer.send('show_win')
this.$router.push('/message/'+messageData.chatId)
this.$router.push('/message/'+messageData.personId)
}
})
notif.show()
@ -355,6 +358,26 @@ export default {
}
}
},
newReaction (data) {
let reactions = data.reactions
if (reactions && reactions.length > 0 && this.$store.state.messagesCache[reactions[0].personId]) {
let msgIndex = this.$store.state.messagesCache[reactions[0].personId].findIndex(obj => obj.guid == reactions[0].forGUID)
if (msgIndex > -1) {
this.$store.state.messagesCache[reactions[0].personId][msgIndex].reactions = reactions
}
}
let chatData = data.chat[0]
if (chatData && chatData.personId) {
let chatIndex = this.chats.findIndex(obj => obj.personId == chatData.personId)
if (chatIndex > -1) {
this.chats.splice(chatIndex, 1)
}
this.chats.unshift(chatData)
}
},
setAsRead (data) {
if (this.$store.state.messagesCache[data.chatId]) {
let messageIndex = this.$store.state.messagesCache[data.chatId].findIndex(obj => obj.guid == data.guid)
@ -364,6 +387,21 @@ export default {
}
}
},
removeChat (data) {
if (data.chatId) {
let chatId = data.chatId
var chatIndex = this.chats.findIndex(obj => obj.address == chatId)
if (chatIndex > -1) {
let chat = this.chats[chatIndex]
if (this.$route.path == '/message/'+chat.personId) {
this.$router.push('/')
}
this.$store.state.messagesCache[chat.personId] = null
this.chats.splice(chatIndex, 1)
}
}
},
onopen () {
this.offset = 0
this.requestChats()
@ -378,7 +416,6 @@ export default {
this.chats = []
},
onclose (e) {
console.log(e)
this.loading = false
if (this.$socket && this.$socket.readyState == 1) return
this.status = 0

View File

@ -18,16 +18,18 @@
<img src="@/assets/loading.webp" style="height:18px;" />
</div>
<template v-else-if="$route.params.id != 'new' || this.receiver != ''">
<simplebar class="messages" ref="messages" data-simplebar-auto-hide="false">
<div v-for="(msg, i) in sortedMessages" :key="msg.id">
<reactionMenu :target="reactingMessage" :reactions="reactingMessageReactions" :guid="reactingMessageGUID" @close="closeReactionMenu" @sendReaction="sendReaction"></reactionMenu>
<simplebar class="messages" ref="messages">
<div v-for="(msg, i) in sortedMessages" :key="'msg'+msg.id">
<div class="timegroup" v-html="dateGroup(i-1, i)" v-if="dateGroup(i-1, i) != ''"></div>
<div :ref="'msg'+msg.id" :class="(msg.sender == 1 ? 'send ' : 'receive ') + msg.type" class="messageGroup">
<div :ref="'msg'+msg.id" :class="(msg.sender == 1 ? 'send ' : 'receive ') + msg.type + (sortedMessages.length-1 == i ? ' last' : '')" class="messageGroup">
<div v-if="msg.group && msg.sender != 1" class="senderName" v-html="$options.filters.twemoji(msg.author)"></div>
<template v-for="(text, i) in msg.texts">
<div :key="'wrapper'+i" v-longclick="openReactionMenu" style="display: contents;">
<div v-for="(attachment, index) in text.attachments" :key="`${i}-${index}`" class="attachment">
<template v-for="(text, ii) in msg.texts">
<div :key="'wrapper'+ii" :ref="'msg'+msg.id+'-text'+ii" :id="'msg'+msg.id+'-text'+ii" @mousedown.left="startInterval(msg.id, ii, text.guid, text.reactions)" @mouseup.left="stopInterval" @mouseleave="stopInterval" class="textWrapper">
<reactions :click="() => openReactionMenu(msg.id, ii, text.guid, text.reactions)" :target="'#msg'+msg.id+'-text'+ii" :reactions="text.reactions" v-if="text.attachments && text.attachments.length > 0 && $options.filters.twemoji(text.text) == ''" :targetFromMe="msg.sender == 1"></reactions>
<div v-for="(attachment, index) in text.attachments" :key="`${ii}-${index}`" class="attachment">
<template v-if="attachment[0] != '' && !attachment[0].includes('.pluginPayloadAttachment')">
<expandable-image v-if="isImage(attachment[1])" :loadedData="scrollToBottom" :path="attachment[0]" :type="attachment[1]" />
<video-player v-else-if="isVideo(attachment[1])" :loadedData="scrollToBottom" :path="attachment[0]" :type="attachment[1]" />
@ -35,16 +37,17 @@
</template>
</div>
<reactions :click="() => openReactionMenu(msg.id, ii, text.guid, text.reactions)" :target="'#msg'+msg.id+'-text'+ii" :reactions="text.reactions" v-if="$options.filters.twemoji(text.text) != ''" :targetFromMe="msg.sender == 1"></reactions>
<div
class="message"
:key="i"
:class="(msg.texts.length-1 == i ? 'last ' : '') + (isEmojis(text.text) ? 'jumbo' : '')"
:key="'msg'+msg.id+'-text'+ii"
:class="(msg.texts.length-1 == ii ? 'last ' : '') + (isEmojis(text.text) ? 'jumbo' : '')"
:style="msg.sender == 1 && text.showStamp && (text.read > 0 || text.delivered > 0) ? 'margin-bottom: 0px;' : ''"
v-if="$options.filters.twemoji(text.text) != ''">
<span style="white-space: pre-wrap;" v-html="$options.filters.twemoji(text.text)" v-linkified></span>
</div>
</div>
<div class="receipt" :key="i+'receipt'" v-if="msg.sender == 1 && text.showStamp && (text.read > 0 || text.delivered > 0)">
<div class="receipt" :key="'msg'+msg.id+'-text'+ii+'-receipt'" v-if="msg.sender == 1 && text.showStamp && (text.read > 0 || text.delivered > 0)">
<span class="type">{{ text.read > 0 ? "Read" : "Delivered" }}</span> {{ humanReadableTimestamp(text.read > 0 ? text.read : text.delivered) }}
</div>
</template>
@ -93,6 +96,8 @@ import VideoPlayer from './VideoPlayer'
import ExpandableImage from './ExpandableImage'
import DownloadAttachment from './DownloadAttachment'
import UploadButton from './UploadButton'
import ReactionMenu from './ReactionMenu'
import Reactions from './Reactions'
import axios from 'axios'
export default {
@ -104,7 +109,9 @@ export default {
VideoPlayer,
ExpandableImage,
DownloadAttachment,
UploadButton
UploadButton,
ReactionMenu,
Reactions
},
data: function () {
return {
@ -117,7 +124,12 @@ export default {
ignoreNextScroll: false,
loading: false,
canSend: true,
hasAttachments: false
hasAttachments: false,
interval: null,
timeHolding: 0,
reactingMessage: null,
reactingMessageGUID: null,
reactingMessageReactions: null
}
},
computed: {
@ -140,13 +152,13 @@ export default {
const groupDates = (date1, date2) => (date2 - date1 < 6000)
const groupAuthor = (author1, author2) => (author1 == author2)
const groupedMessages = messages.reduce((r, { text, dateRead, dateDelivered, guid, ...rest }, i, arr) => {
const groupedMessages = messages.reduce((r, { text, dateRead, dateDelivered, guid, reactions, ...rest }, i, arr) => {
const prev = arr[i +-1]
if (prev && groupAuthor(rest.author, prev.author) && groupAuthor(rest.sender, prev.sender) && groupDates(rest.date, prev.date))
r[r.length - 1].texts.unshift({ text: text.trim(), date: rest.date, attachments: rest.attachments, read: dateRead, delivered: dateDelivered, guid: guid, showStamp: rest.sender == 1 && !lastSentMessageFound })
r[r.length - 1].texts.unshift({ text: text, date: rest.date, attachments: rest.attachments, read: dateRead, delivered: dateDelivered, guid: guid, reactions: reactions, showStamp: rest.sender == 1 && !lastSentMessageFound })
else
r.push({ ...rest, texts: [{ text: text.trim(), date: rest.date, attachments: rest.attachments, read: dateRead, delivered: dateDelivered, guid: guid, showStamp: rest.sender == 1 && !lastSentMessageFound }] })
r.push({ ...rest, texts: [{ text: text, date: rest.date, attachments: rest.attachments, read: dateRead, delivered: dateDelivered, guid: guid, reactions: reactions, showStamp: rest.sender == 1 && !lastSentMessageFound }] })
if (rest.sender == 1 && !lastSentMessageFound) lastSentMessageFound = true
@ -165,6 +177,9 @@ export default {
this.ignoreNextScroll = false
this.canSend = true
this.hasAttachments = false
this.reactingMessage = null
this.reactingMessageGUID = null
this.reactingMessageReactions = null
if (this.$refs.uploadButton) {
this.$refs.uploadButton.clear()
}
@ -183,6 +198,28 @@ export default {
isVideo(type) {
return type.includes('video/')
},
startInterval (msgId, textId, guid, reactions) {
if (!this.interval) {
this.interval = setInterval(() => {
this.timeHolding++
if (this.timeHolding > 7) { //> 0.7 seconds, will trigger at 0.8 seconds
this.openReactionMenu(msgId, textId, guid, reactions)
clearInterval(this.interval)
this.interval = false
this.timeHolding = 0
}
}, 100)
}
},
stopInterval () {
clearInterval(this.interval)
this.interval = false
this.timeHolding = 0
},
closeReactionMenu () {
this.reactingMessage = null
this.reactingMessageGUID = null
},
dateGroup(prev, current) {
let prevstamp = this.sortedMessages[prev] ? this.sortedMessages[prev].date : 0
let currstamp = this.sortedMessages[current].date
@ -316,20 +353,23 @@ export default {
}
})
},
openReactionMenu (e) {
console.log(e)
openReactionMenu (msgId, textId, guid, reactions) {
let el = $(this.$refs['msg'+msgId+'-text'+textId])
this.reactingMessageReactions = reactions
this.reactingMessageGUID = guid
this.reactingMessage = el
},
sendReaction (text) {
sendReaction (reactionId, guid) {
if (!this.messages[0]) return
this.sendSocket({ action: 'sendReaction', data: {
chatId: this.messages[0].chatId,
guid: text.guid
guid: guid,
reactionId: reactionId
}})
},
sendText () {
let messageText = this.messageText[this.$route.params.id]
if (!messageText) messageText = ''
messageText = messageText.trim()
if (messageText == '' && (!this.$refs.uploadButton.attachments || this.$refs.uploadButton.attachments.length == 0)) return
if (!this.canSend) return
this.canSend = false
@ -389,8 +429,10 @@ export default {
autoCompleteInput (input) {
if (input && input.name) {
this.receiver = input.phone
this.hookPasteAndDrop()
} else if (/^\+\d{11,16}/gi.test(input)) {
this.receiver = input
this.hookPasteAndDrop()
}
},
previewFiles () {
@ -434,9 +476,13 @@ export default {
this.offset += this.limit
this.loading = false
this.hookPasteAndDrop()
},
hookPasteAndDrop () {
this.$nextTick(() => {
var el = document.getElementById('twemoji-textarea')
if (el) {
setTimeout(() => { el.focus() }, 10)
el.addEventListener('paste', e => {
if (e.clipboardData.files && e.clipboardData.files.length > 0) {
let text = e.clipboardData.getData('Text')
@ -481,7 +527,6 @@ export default {
if (this.offset == 0 && !this.$store.state.messagesCache[this.$route.params.id]) {
this.messages = data
this.$store.commit('addMessages', { id: this.$route.params.id, data: data })
}
else this.messages.push(...data)
@ -497,6 +542,7 @@ export default {
if (this.lastHeight == null) {
this.$nextTick(() => { this.scrollToBottom() })
}
this.messages.__ob__.dep.notify()
}
}
},
@ -516,7 +562,11 @@ export default {
console.log("Received a message, but content was empty.")
} else {
if (this.messages && this.messages.length > 0 && message[0]['personId'] == this.$route.params.id) {
if (this.messages.findIndex(obj => obj.id == message[0].id) != -1) return
let oldMsgIndex = this.messages.findIndex(obj => obj.id == message[0].id)
if (oldMsgIndex != -1) {
this.messages[oldMsgIndex] = message[0]
return
}
this.messages.unshift(message[0])
@ -531,6 +581,22 @@ export default {
}
}
},
newReaction (data) {
let reactions = data.reactions
if (reactions && reactions.length > 0) {
let msgIndex = this.messages.findIndex(obj => obj.guid == reactions[0].forGUID)
if (msgIndex > -1) {
this.messages[msgIndex].reactions = reactions
this.messages.__ob__.dep.notify()
let intervalTime = 0
let interval = setInterval(() => {
if (this.lastHeight == null) this.scrollToBottom()
if (intervalTime >= 300) clearInterval(interval)
intervalTime += 1
}, 1)
}
}
},
onerror () {
this.loading = false
this.messages = []
@ -900,27 +966,32 @@ export default {
}
.messageGroup {
// margin-top: 30px;
margin-bottom: 10px;
padding-left: 20px;
padding-right: 20px;
display: flex;
flex-direction: column;
&.last {
margin-bottom: 0px;
}
.attachment {
max-width: 75%;
max-width: 60%;
max-height: 60%;
border-radius: 18px;
height: fit-content;
margin-bottom: -2px;
img {
max-width: 280px;
max-height: 700px;
// max-width: 280px;
// max-height: 700px;
border-radius: 12px;
}
video {
max-width: 420px;
max-height: 700px;
// max-width: 420px;
// max-height: 700px;
border-radius: 12px;
}
}
@ -948,6 +1019,13 @@ export default {
.receive {
align-items: flex-start;
.textWrapper {
display: flex;
align-items: flex-start;
flex-direction: column;
width: 100%;
}
.message {
color: white;
margin-right: 25%;
@ -961,8 +1039,6 @@ export default {
}
&.last {
margin-bottom: 10px;
&:before {
content: "";
position: absolute;
@ -1005,6 +1081,13 @@ export default {
.send {
align-items: flex-end;
.textWrapper {
display: flex;
align-items: flex-end;
flex-direction: column;
width: 100%;
}
.message {
color: white;
margin-left: 25%;
@ -1018,8 +1101,6 @@ export default {
}
&.last {
margin-bottom: 10px;
&:before {
content: "";
position: absolute;

View File

@ -0,0 +1,195 @@
<template>
<transition name="fade" mode="out-in">
<div id="reactionMenu" v-if="target">
<div class="backdrop" @click="closeMenu">
</div>
<div class="menu" :style="{ top: position.top, left: position.left, right: position.right, bottom: position.bottom }" :class=" direction ">
<font-awesome-icon @click="sendReaction(icon)" v-for="icon in icons" :key="icon.name" :icon="icon.name" :class="{ active: activeReactions.includes(icon.id) }" size="3x" />
</div>
</div>
</transition>
</template>
<script>
export default {
name: "ReactionMenu",
data() {
return {
icons: [
{ name: 'heart', id: 2000 },
{ name: 'thumbs-up', id: 2001 },
{ name: 'thumbs-down', id: 2002 },
{ name: 'laugh-squint', id: 2003 },
{ name: 'exclamation', id: 2004 },
{ name: 'question', id: 2005 },
],
position: { top: '50px', left: '20px' },
direction: 'right',
clone: null,
activeReactions: []
}
},
props: {
target: { type: Object },
guid: { type: String },
reactions: { type: Array }
},
watch: {
target(newTarget) {
if (newTarget != null) {
let target = newTarget.children().last()
let parent = newTarget.parent()
let p = target.offset()
let w = target.width()
let h = target.height()
let clone = target.clone()
clone.css({
position: 'fixed',
top: p.top+'px',
left: p.left+'px',
width: w+'px',
height: h+'px',
margin: '0'
})
let msgWrapper = $('<div class="messages" style="display:contents;z-index:-1;"></div>')
$('<div></div>').attr('class', parent.attr('class')).appendTo(msgWrapper)
clone.appendTo(msgWrapper.children().first())
this.$nextTick(() => {
msgWrapper.appendTo('#reactionMenu')
})
this.clone = msgWrapper
this.direction = parent.hasClass('send') ? 'right' : 'left'
this.position = { }
this.position.top = (p.top - 45) + 'px'
if (this.direction == 'right') {
this.position.right = 6 + 'px'
} else {
this.position.left = (p.left - 15) + 'px'
}
let activeReactionsList = this.reactions.filter(reaction => reaction.reactionType >= 2000 && reaction.reactionType < 3000 && reaction.sender == 1)
this.activeReactions = []
activeReactionsList.forEach((reaction) => {
this.activeReactions.push(reaction.reactionType)
})
} else {
this.activeReactions = []
}
}
},
mounted() {
},
methods: {
sendReaction (icon) {
let id = icon.id
if (this.activeReactions.includes(id)) id += 1000 //Remove reaction
this.$emit('sendReaction', id, this.guid)
this.closeMenu()
},
closeMenu () {
setTimeout(() => {
if (this.clone) {
this.clone.remove()
this.clone = null
this.activeReactions = []
}
}, 500)
this.$emit('close')
}
},
}
</script>
<style lang="scss">
#reactionMenu {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
.message.last:after {
background: #090909;
}
.backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
z-index: -1;
}
.menu {
position: fixed;
background-color: rgb(70,70,70);
padding: 5px 10px;
border-radius: 20px;
color: rgb(140,140,140);
z-index: 1;
svg {
padding: 8px;
border-radius: 50%;
width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
vertical-align: middle;
cursor: pointer;
&:hover {
background-color: rgba(0,0,0,0.2);
}
&.active {
background-color: #2484FF;
color: white;
}
}
&:after {
content: "";
position: absolute;
bottom: -5px;
right: 20px;
width: 10px;
height: 10px;
border-radius: 50%;
background: rgb(70,70,70);
}
&:before {
content: "";
position: absolute;
bottom: -12px;
right: 19px;
width: 6px;
height: 6px;
border-radius: 50%;
background: rgb(70,70,70);
}
&.left {
&:after {
left: 20px;
right: unset;
}
&:before {
left: 19px;
right: unset;
}
}
}
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<transition name="slide-fade" mode="out-in">
<div class="reactions" v-if="reactionList.length > 0" :class="{ left: !targetFromMe }">
<div v-for="(reaction, i) in reactionList" @click="click" :key="reaction.guid" class="bubble" :class="{ isMe: reaction.sender == 1 }" :style="{ zIndex: 4-i, top: position.top+'px', left: (position.left+(targetFromMe ? (-i*3) : (i*3)))+'px' }">
<font-awesome-icon v-if="reaction.icon" :icon="reaction.icon.name" size="lg" :style="reaction.icon.style" />
</div>
</div>
</transition>
</template>
<script>
export default {
name: "Reactions",
props: {
reactions: { type: Array },
targetFromMe: { type: Boolean },
target: { type: String },
click: { type: Function }
},
computed: {
reactionList() {
let reactions = this.reactions.filter(reaction => reaction.reactionType >= 2000 && reaction.reactionType < 3000)
reactions.forEach((reaction) => {
let reactionId = reaction.reactionType
let icon = this.icons.find(icon => icon.id == reactionId)
reaction.icon = icon
})
return reactions.sort((a, b) => b.sender - a.sender).slice(0, 4)
}
},
watch: {
},
data() {
return {
icons: [
{ name: 'heart', id: 2000, style: { color: '#FA5E96' } },
{ name: 'thumbs-up', id: 2001 },
{ name: 'thumbs-down', id: 2002 },
{ name: 'laugh-squint', id: 2003 },
{ name: 'exclamation', id: 2004 },
{ name: 'question', id: 2005 },
],
position: { top: -16, left: 0 }
}
},
mounted() {
this.adjustPostion()
},
beforeMount () {
this.adjustPostion()
},
methods: {
adjustPostion () {
let target = $(this.target).children().last()
this.position.left = (target.width() + 10)
if (this.targetFromMe) {
this.position.left = (-target.width() - 36)
}
}
},
}
</script>
<style lang="scss" scoped>
.reactions {
position: relative;
margin-top: 18px;
.bubble {
background-color: #3A3A3C;
padding: 5px;
position: absolute;
border: solid 1px #1D1D1D;
border-radius: 50%;
width: 14px;
height: 14px;
line-height: 12px;
transition: 0.3s;
svg {
width: 100%;
height: 100%;
}
&.isMe {
background-color: #2284FF;
}
}
}
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .3s ease;
}
.slide-fade-enter, .slide-fade-leave-to {
transform: scaleY(0);
margin-top: 0px;
opacity: 0;
}
</style>

View File

@ -13,6 +13,9 @@ import $ from 'jquery'
import Popover from 'vue-js-popover'
import VueConfirmDialog from 'vue-confirm-dialog'
import { longClickDirective } from 'vue-long-click'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faHeart, faThumbsUp, faThumbsDown, faLaughSquint, faExclamation, faQuestion } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
window.$ = $
const https = require('https')
@ -29,6 +32,15 @@ Vue.use(VueNativeSock, 'ws://', {
Vue.use(VueFeather)
Vue.use(Popover, { tooltip: true })
Vue.use(VueConfirmDialog)
library.add(faHeart)
library.add(faThumbsUp)
library.add(faThumbsDown)
library.add(faLaughSquint)
library.add(faExclamation)
library.add(faQuestion)
Vue.component('font-awesome-icon', FontAwesomeIcon)
Vue.component('vue-confirm-dialog', VueConfirmDialog.default)
Vue.config.productionTip = false

View File

@ -877,6 +877,30 @@
global-agent "^2.0.2"
global-tunnel-ng "^2.7.1"
"@fortawesome/fontawesome-common-types@^0.2.34":
version "0.2.34"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.34.tgz#0a8c348bb23b7b760030f5b1d912e582be4ec915"
integrity sha512-XcIn3iYbTEzGIxD0/dY5+4f019jIcEIWBiHc3KrmK/ROahwxmZ/s+tdj97p/5K0klz4zZUiMfUlYP0ajhSJjmA==
"@fortawesome/fontawesome-svg-core@^1.2.34":
version "1.2.34"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.34.tgz#1d1a7c92537cbc2b8a83eef6b6d824b4b5b46b26"
integrity sha512-0KNN0nc5eIzaJxlv43QcDmTkDY1CqeN6J7OCGSs+fwGPdtv0yOQqRjieopBCmw+yd7uD3N2HeNL3Zm5isDleLg==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.34"
"@fortawesome/free-solid-svg-icons@^5.15.2":
version "5.15.2"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.2.tgz#25bb035de57cf85aee8072965732368ccc8e8943"
integrity sha512-ZfCU+QjaFsdNZmOGmfqEWhzI3JOe37x5dF4kz9GeXvKn/sTxhqMtZ7mh3lBf76SvcYY5/GKFuyG7p1r4iWMQqw==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.34"
"@fortawesome/vue-fontawesome@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.2.tgz#5b86cd2fb7b4c17e5dede722c1c2855c97eceaea"
integrity sha512-ecpKSBUWXsxRJVi/dbOds4tkKwEcBQ1JSDZFzE2jTFpF8xIh3OgTX8POIor6bOltjibr3cdEyvnDjecMwUmxhQ==
"@hapi/address@2.x.x":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"