mirror of
https://github.com/danbulant/Nertivia-Client
synced 2026-06-12 19:20:13 +00:00
Status update
This commit is contained in:
parent
d2bd1b13c6
commit
fb43e6ea0b
12 changed files with 220 additions and 52 deletions
4
src/assets/typing-indicator.svg
Normal file
4
src/assets/typing-indicator.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!-- Generator: SVG Circus (http://svgcircus.com) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg id="SVG-Circus-65973b29-eb9b-a189-c316-0ca597182e29" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="20 20 60 60" preserveAspectRatio="xMidYMid meet"><circle id="actor_4" cx="50" cy="50" r="14" opacity="1" fill="rgba(255,255,255,0.5)" fill-opacity="1" stroke="rgba(209,113,61,1)" stroke-width="0" stroke-opacity="1" stroke-dasharray=""></circle><circle id="actor_1" cx="50" cy="50" r="14.5" opacity="1" fill="rgba(255,255,255,1)" fill-opacity="1" stroke="rgba(106,184,180,1)" stroke-width="0" stroke-opacity="1" stroke-dasharray=""></circle><script type="text/ecmascript"><![CDATA[(function(){var actors={};actors.actor_1={node:document.getElementById("SVG-Circus-65973b29-eb9b-a189-c316-0ca597182e29").getElementById("actor_1"),type:"circle",cx:50,cy:50,dx:29,dy:11,opacity:1};actors.actor_4={node:document.getElementById("SVG-Circus-65973b29-eb9b-a189-c316-0ca597182e29").getElementById("actor_4"),type:"circle",cx:50,cy:50,dx:28,dy:17,opacity:1};var tricks={};tricks.trick_1=(function(_,t){t=(function(n){return.5>n?2*n*n:-1+(4-2*n)*n})(t)%1,t=0>t?1+t:t;var i;i=0.00>=t?2.0-(2.0-1)/0.00*t:t>=0.30?1+(t-0.30)*((2.0-1)/(1-0.30)):1;var a=_._tMatrix,r=-_.cx*i+_.cx,x=-_.cy*i+_.cy,n=a[0]*i,c=a[1]*i,M=a[2]*i,g=a[3]*i,f=a[0]*r+a[2]*x+a[4],m=a[1]*r+a[3]*x+a[5];_._tMatrix[0]=n,_._tMatrix[1]=c,_._tMatrix[2]=M,_._tMatrix[3]=g,_._tMatrix[4]=f,_._tMatrix[5]=m});tricks.trick_2=(function(t,i){i=(function(n){return n})(i)%1,i=0>i?1+i:i;var _=t.node;0.00>=i?_.setAttribute("opacity",i*(t.opacity/0.00)):i>=0.38?_.setAttribute("opacity",t.opacity-(i-0.38)*(t.opacity/(1-0.38))):_.setAttribute("opacity",t.opacity)});var scenarios={};scenarios.scenario_1={actors: ["actor_4"],tricks: [{trick: "trick_1",start:0,end:1.00}],startAfter:0,duration:1000,actorDelay:0,repeat:0,repeatDelay:0};scenarios.scenario_2={actors: ["actor_4"],tricks: [{trick: "trick_2",start:0,end:1}],startAfter:0,duration:1000,actorDelay:0,repeat:0,repeatDelay:0};var _reqAnimFrame=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.oRequestAnimationFrame,fnTick=function(t){var r,a,i,e,n,o,s,c,m,f,d,k,w;for(c in actors)actors[c]._tMatrix=[1,0,0,1,0,0];for(s in scenarios)for(o=scenarios[s],m=t-o.startAfter,r=0,a=o.actors.length;a>r;r++){if(i=actors[o.actors[r]],i&&i.node&&i._tMatrix)for(f=0,m>=0&&(d=o.duration+o.repeatDelay,o.repeat>0&&m>d*o.repeat&&(f=1),f+=m%d/o.duration),e=0,n=o.tricks.length;n>e;e++)k=o.tricks[e],w=(f-k.start)*(1/(k.end-k.start)),tricks[k.trick]&&tricks[k.trick](i,Math.max(0,Math.min(1,w)));m-=o.actorDelay}for(c in actors)i=actors[c],i&&i.node&&i._tMatrix&&i.node.setAttribute("transform","matrix("+i._tMatrix.join()+")");_reqAnimFrame(fnTick)};_reqAnimFrame(fnTick);})()]]></script></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
|
@ -3,7 +3,8 @@
|
|||
<MyMiniInformation />
|
||||
<div class="list">
|
||||
<PendingFriends />
|
||||
<Friends />
|
||||
<online-friends />
|
||||
<offline-friends />
|
||||
</div>
|
||||
<AddFriendPanel/>
|
||||
</div>
|
||||
|
|
@ -14,13 +15,15 @@
|
|||
import MyMiniInformation from '../../components/app/MyMiniInformation.vue'
|
||||
import PendingFriends from './relationships/PendingFriends.vue'
|
||||
import AddFriendPanel from './relationships/AddFriendPanel.vue'
|
||||
import Friends from './relationships/Friends.vue'
|
||||
import OnlineFriends from './relationships/OnlineFriends.vue'
|
||||
import OfflineFriends from './relationships/OfflineFriends.vue'
|
||||
export default {
|
||||
components: {
|
||||
MyMiniInformation,
|
||||
PendingFriends,
|
||||
AddFriendPanel,
|
||||
Friends
|
||||
OnlineFriends,
|
||||
OfflineFriends
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,42 +1,40 @@
|
|||
<template>
|
||||
<div class="message">
|
||||
<div class="profile-picture" :style="`background-image: url(${userAvatar})`"></div>
|
||||
<div class="triangle">
|
||||
<div class="triangle-inner"></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="username">{{this.$props.username}}</div>
|
||||
<div class="content-message">{{this.$props.message}}</div>
|
||||
</div>
|
||||
<div class="sending-status">{{statusMessage}}</div>
|
||||
</div>
|
||||
<div class="message">
|
||||
<div class="profile-picture" :style="`background-image: url(${userAvatar})`"></div>
|
||||
<div class="triangle">
|
||||
<div class="triangle-inner"></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="username">{{this.$props.username}}</div>
|
||||
<div class="content-message">{{this.$props.message}}</div>
|
||||
</div>
|
||||
<div class="sending-status">{{statusMessage}}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import config from '@/config.js'
|
||||
export default {
|
||||
props: ['message', 'status', 'username', 'avatar'],
|
||||
data() {
|
||||
return{
|
||||
userAvatar: config.domain + "/avatars/" + this.$props.avatar
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
statusMessage(){
|
||||
let status = this.$props.status;
|
||||
props: ['message', 'status', 'username', 'avatar'],
|
||||
computed: {
|
||||
userAvatar() {
|
||||
return config.domain + "/avatars/" + this.$props.avatar
|
||||
},
|
||||
statusMessage(){
|
||||
let status = this.$props.status;
|
||||
|
||||
if (status == 0) {
|
||||
return "Sending"
|
||||
} else if (status == 1) {
|
||||
return "Sent"
|
||||
} else if (status == 2) {
|
||||
return "Failed"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
if (status == 0) {
|
||||
return "Sending"
|
||||
} else if (status == 1) {
|
||||
return "Sent"
|
||||
} else if (status == 2) {
|
||||
return "Failed"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
</div>
|
||||
<div class="current-channel"><span v-if="!selectedChannelID">Welcome back!</span><span v-else>{{channelName}}</span></div>
|
||||
</div>
|
||||
<div class="loading" v-if="selectedChannelID && !messages[selectedChannelID]">
|
||||
<typing-status v-if="typing" :username="whosTyping"/>
|
||||
<div class="loading" v-if="selectedChannelID && !messages[selectedChannelID]">
|
||||
<spinner />
|
||||
</div>
|
||||
<div v-else-if="selectedChannelID" class="message-logs">
|
||||
|
|
@ -29,23 +30,30 @@
|
|||
|
||||
<script>
|
||||
import messagesService from '@/services/messagesService'
|
||||
import typingService from '@/services/TypingService'
|
||||
import {bus} from '../../main'
|
||||
import JQuery from 'jquery'
|
||||
let $ = JQuery
|
||||
import News from '../../components/app/News.vue'
|
||||
import Message from '../../components/app/MessageTemplate.vue'
|
||||
import Spinner from '@/components/Spinner.vue'
|
||||
import TypingStatus from '@/components/app/TypingStatus.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Message,
|
||||
Spinner,
|
||||
News
|
||||
News,
|
||||
TypingStatus
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: "",
|
||||
messageLength: 0
|
||||
messageLength: 0,
|
||||
postTimerID: null,
|
||||
getTimerID: null,
|
||||
typing: false,
|
||||
whosTyping: "",
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
|
|
@ -104,8 +112,23 @@ export default {
|
|||
console.log(error)
|
||||
}
|
||||
},
|
||||
messageKeyUp(event){
|
||||
async postTimer() {
|
||||
this.postTimerID = setInterval(async () => {
|
||||
if (this.$refs['input-box'].value.trim() == "") {
|
||||
clearInterval(this.postTimerID);
|
||||
return this.postTimerID = null;
|
||||
}
|
||||
await typingService.post(this.selectedChannelID);
|
||||
}, 2000);
|
||||
},
|
||||
async messageKeyUp(event){
|
||||
this.messageLength = this.message.length;
|
||||
const value = event.target.value.trim();
|
||||
if (value && this.postTimerID == null) {
|
||||
// Post typing status
|
||||
this.postTimer()
|
||||
await typingService.post(this.selectedChannelID);
|
||||
}
|
||||
},
|
||||
chatInput(event) {
|
||||
if (event.keyCode == 13) {
|
||||
|
|
@ -113,20 +136,36 @@ export default {
|
|||
this.sendMessage();
|
||||
}
|
||||
},
|
||||
scrollDown(){
|
||||
scrollDown(speed){
|
||||
//Scroll to bottom
|
||||
$(".message-logs").stop(true).animate({
|
||||
scrollTop: $(".message-logs")[0].scrollHeight
|
||||
}, 300);
|
||||
}, speed);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
bus.$on('scrollDown', this.scrollDown);
|
||||
this.$options.sockets.typingStatus = (data) => {
|
||||
const {channelID, userID} = data;
|
||||
if (channelID !== this.selectedChannelID) return;
|
||||
this.typing = true;
|
||||
this.whosTyping = this.channel.recipients.find(function(recipient) {
|
||||
return recipient.uniqueID == userID;
|
||||
}).username
|
||||
clearTimeout(this.getTimerID);
|
||||
this.getTimerID = setTimeout(() => {
|
||||
this.typing = false;
|
||||
}, 2500);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off('status-scrollDown', this.scrollDown)
|
||||
delete this.$options.sockets.typingStatus;
|
||||
bus.$off('scrollDown', this.scrollDown)
|
||||
},
|
||||
computed: {
|
||||
channel() {
|
||||
return this.$store.getters.channels[this.selectedChannelID];
|
||||
},
|
||||
messages() {
|
||||
return this.$store.getters.messages;
|
||||
},
|
||||
|
|
@ -193,10 +232,10 @@ export default {
|
|||
flex: 1;
|
||||
}
|
||||
.chat-input-area{
|
||||
height: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.chat-input-area .info{
|
||||
color: white;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export default {
|
|||
data(){
|
||||
return {
|
||||
alert: {
|
||||
content: 'Image size must be less than 10mb!',
|
||||
content: '',
|
||||
show: false
|
||||
},
|
||||
}
|
||||
|
|
@ -47,21 +47,22 @@ export default {
|
|||
},
|
||||
async avatarBrowse(event) {
|
||||
const file = event.target.files[0];
|
||||
event.target.value = "";
|
||||
const allowedFormats = ['.png', '.jpeg', '.gif', '.jpg' ];
|
||||
|
||||
if (!allowedFormats.includes(path.extname(file.name).toLowerCase())){
|
||||
this.alert.content = 'Unsupported image file.';
|
||||
this.alert.content = 'Upload failed - Unsupported image file.';
|
||||
return this.alert.show = true;
|
||||
} else if (file.size >= 10490000){
|
||||
// 10490000 = 10mb
|
||||
this.alert.content = 'Image size must be less than 10 megabytes.';
|
||||
this.alert.content = 'Upload failed - Image size must be less than 10 megabytes.';
|
||||
return this.alert.show = true;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', file);
|
||||
const {ok, error, result} = await UploadService.uploadAvatar(formData, this.onProgress);
|
||||
if (!ok) {
|
||||
this.alert.content = 'Something went wrong. Try again later.';
|
||||
this.alert.content = 'Upload failed - Something went wrong. Try again later.';
|
||||
return this.alert.show = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
52
src/components/app/TypingStatus.vue
Normal file
52
src/components/app/TypingStatus.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div class="typing-status">
|
||||
<object class="animation" type="image/svg+xml" :data="animation"></object>
|
||||
<div class="text">{{this.$props.username}} is typing...</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['username'],
|
||||
data() {
|
||||
return {
|
||||
animation: require('@/assets/typing-indicator.svg')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.typing-status {
|
||||
color: white;
|
||||
background: rgba(0, 0, 0, 0.246);
|
||||
padding: 5px;
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
transition: 0.3s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.animation {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
}
|
||||
.text {
|
||||
margin: auto;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
@keyframes moveBall {
|
||||
from{
|
||||
height: 0px;
|
||||
width: 0px;
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
64
src/components/app/relationships/OfflineFriends.vue
Normal file
64
src/components/app/relationships/OfflineFriends.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="friends" >
|
||||
<div class="tab" @click="expanded = !expanded">
|
||||
<Tab :expanded="expanded" tabname="Offline" />
|
||||
</div>
|
||||
<transition name="list" appear>
|
||||
<div class="list" v-if="expanded">
|
||||
<FriendsTemplate v-for="(friend, key) of friends" :key="key" :channelID="friend.channelID" :uniqueID="friends[key].recipient.uniqueID" :username="friend.recipient.username" :tag="friend.recipient.tag"/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tab from './Tab.vue'
|
||||
import FriendsTemplate from './FriendsTemplate.vue'
|
||||
export default {
|
||||
components: {
|
||||
Tab,
|
||||
FriendsTemplate
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
friends() {
|
||||
const allFriend = this.$store.getters.user.friends;
|
||||
const result = Object.keys(allFriend).map(function(key) {
|
||||
return allFriend[key];
|
||||
});
|
||||
return result.filter(friend => friend.status == 2 && friend.recipient.status === undefined || friend.recipient.status == 0 );
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.list-enter-active, .list-leave-active {
|
||||
transition: .3s;
|
||||
}
|
||||
.list-enter, .list-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.friends{
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
margin: 5px;
|
||||
user-select: none;
|
||||
padding-bottom: 3px;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.tab{
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.tab:hover{
|
||||
background-color: rgba(0, 0, 0, 0.123);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="friends" >
|
||||
<div class="tab" @click="expanded = !expanded">
|
||||
<Tab :expanded="expanded" tabname="Friends" />
|
||||
<Tab :expanded="expanded" tabname="Online" />
|
||||
</div>
|
||||
<transition name="list" appear>
|
||||
<div class="list" v-if="expanded">
|
||||
|
|
@ -30,7 +30,7 @@ export default {
|
|||
const result = Object.keys(allFriend).map(function(key) {
|
||||
return allFriend[key];
|
||||
});
|
||||
return result.filter(friend => friend.status == 2);
|
||||
return result.filter(friend => friend.status == 2 && friend.recipient.status !== undefined || friend.recipient.status > 0 );
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/services/TypingService.js
Normal file
7
src/services/TypingService.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import {instance, wrapper} from './Api';
|
||||
|
||||
export default {
|
||||
post (channelID) {
|
||||
return wrapper(instance().post(`messages/${channelID}/typing`))
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ export default {
|
|||
get ( channelID ) {
|
||||
return wrapper(instance().get(`messages/${channelID}`));
|
||||
},
|
||||
// TODO tempID
|
||||
|
||||
post (channelID, data) {
|
||||
return wrapper(instance().post(`messages/${channelID}`, data))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ const mutations = {
|
|||
messages(state, data) {
|
||||
Vue.set(state.messages, data.channelID, data.messages.reverse())
|
||||
setTimeout(() => {
|
||||
bus.$emit('scrollDown');
|
||||
bus.$emit('scrollDown', 0);
|
||||
}, 300);
|
||||
},
|
||||
addMessage(state, data) {
|
||||
bus.$emit('scrollDown');
|
||||
bus.$emit('scrollDown', 300);
|
||||
Vue.set(
|
||||
state.messages[data.channelID],
|
||||
state.messages[data.channelID].length,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const actions = {
|
|||
},
|
||||
socket_userAvatarChange(context, data) {
|
||||
context.commit('userAvatarChange', data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
|
|||
Loading…
Reference in a new issue