Status update

This commit is contained in:
supertiger 2019-02-06 21:08:52 +00:00
parent d2bd1b13c6
commit fb43e6ea0b
12 changed files with 220 additions and 52 deletions

View 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

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -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;
}

View 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>

View 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>

View file

@ -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 );
}
}
}

View file

@ -0,0 +1,7 @@
import {instance, wrapper} from './Api';
export default {
post (channelID) {
return wrapper(instance().post(`messages/${channelID}/typing`))
}
}

View file

@ -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))
}

View file

@ -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,

View file

@ -61,7 +61,7 @@ const actions = {
},
socket_userAvatarChange(context, data) {
context.commit('userAvatarChange', data)
}
}
}
export default {