New client chat (#8890)

* Added initial challenge pages

* Added challenge item and find guilds page

* Added challenge detail

* Added challenge modals

* Ported over challenge service code

* Ported over challenge ctrl code

* Added styles and column

* Minor modal updates

* Removed duplicate keys

* Fixed casing

* Added initial chat component

* Added copy as todo modal

* Added sync

* Added chat to groups

* Fixed lint
This commit is contained in:
Keith Holliday
2017-07-21 11:00:36 -06:00
committed by GitHub
parent 0a59b8e85b
commit ecc18fc093
9 changed files with 311 additions and 81 deletions

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#A5A1AC" fill-rule="evenodd" d="M11 10c0 1.654-1.346 3-3 3s-3-1.346-3-3h6zm2-3.5a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 13 6.5zm-7 0a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 6 6.5zM2 14h12V2H2v12zM14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
<path fill-rule="evenodd" d="M11 10c0 1.654-1.346 3-3 3s-3-1.346-3-3h6zm2-3.5a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 13 6.5zm-7 0a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 6 6.5zM2 14h12V2H2v12zM14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
</svg>

Before

Width:  |  Height:  |  Size: 377 B

After

Width:  |  Height:  |  Size: 362 B

View File

@@ -0,0 +1,139 @@
<template lang="pug">
div
copy-as-todo-modal(:copying-message='copyingMessage', :group-name='groupName', :group-id='groupId')
.row
// .col-md-2
// @TODO: Implement when we pull avatars .svg-icon(v-html="icons.like")
.col-md-12(v-for="(msg, index) in chat", :key="msg.id")
.card
.card-block
h3.leader {{msg.user}}
p {{msg.timestamp | timeAgo}}
.text {{msg.text}}
hr
.action(v-once, @click='like(msg)', :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') }}
span.action(v-once, @click='copyAsTodo(msg)')
.svg-icon(v-html="icons.copy")
| {{$t('copyAsTodo')}}
span.action(v-once, v-if='user.contributor.admin || (!msg.sent && user.flags.communityGuidelinesAccepted)', @click='report(msg)')
.svg-icon(v-html="icons.report")
| {{$t('report')}}
span.action(v-once, v-if='msg.uuid === user._id', @click='remove(msg, index)')
.svg-icon(v-html="icons.delete")
| {{$t('delete')}}
span.action.float-right
.svg-icon(v-html="icons.liked")
| + {{ likeCount(msg) }}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.card {
margin-bottom: 1em;
}
.text {
font-size: 14px;
color: #4e4a57;
}
.action {
display: inline-block;
color: #878190;
margin-right: 1em;
}
.action:hover {
cursor: pointer;
}
.action .svg-icon {
margin-right: .2em;
width: 16px;
display: inline-block;
color: #A5A1AC;
}
.action.active, .active .svg-icon {
color: #46a7d9
}
</style>
<script>
import moment from 'moment';
import { mapState } from 'client/libs/store';
import copyAsTodoModal from './copyAsTodoModal';
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';
export default {
props: ['chat', 'groupId', 'groupName'],
components: {
copyAsTodoModal,
},
data () {
return {
icons: Object.freeze({
like: likeIcon,
copy: copyIcon,
report: reportIcon,
delete: deleteIcon,
liked: likedIcon,
}),
copyingMessage: {},
messages: [],
};
},
filters: {
timeAgo (value) {
return moment(value).fromNow();
},
},
computed: {
...mapState({user: 'user.data'}),
},
methods: {
likeCount (message) {
return Object.keys(message.likes).length;
},
async like (message) {
if (!message.likes[this.user._id]) {
message.likes[this.user._id] = true;
} else {
message.likes[this.user._id] = !message.likes[this.user._id];
}
await this.$store.dispatch('chat:like', {
groupId: this.groupId,
chatId: message.id,
});
},
copyAsTodo (message) {
this.copyingMessage = message;
this.$root.$emit('show::modal', 'copyAsTodo');
},
async report (message) {
await this.$store.dispatch('chat:flag', {
groupId: this.groupId,
chatId: message.id,
});
},
async remove (message, index) {
this.chat.splice(index, 1);
await this.$store.dispatch('chat:deleteChat', {
groupId: this.groupId,
chatId: message.id,
});
},
},
};
</script>

View File

@@ -0,0 +1,66 @@
<template lang="pug">
b-modal#copyAsTodo(:title="$t('copyMessageAsToDo')", :hide-footer="true", size='md')
.form-group
input.form-control(type='text', v-model='text')
.form-group
textarea.form-control(rows='5', v-model='notes' focus-element='true')
hr
// @TODO: Implement when tasks are done
//div.task-column.preview
div(v-init='popoverOpen = false', class='task todo uncompleted color-neutral', popover-trigger='mouseenter', data-popover-html="{{popoverOpen ? '' : notes | markdown}}", popover-placement="top")
.task-meta-controls
span(v-if='!obj._locked')
span.task-notes(v-show='notes', @click='popoverOpen = !popoverOpen', popover-trigger='click', data-popover-html="{{notes | markdown}}", popover-placement="top")
span.glyphicon.glyphicon-comment
| &nbsp;
div.task-text
markdown(text='text',target='_blank')
.modal-footer
button.btn.btn-default(@click='close()') {{ $t('close') }}
button.btn.btn-primary(@click='saveTodo()') {{ $t('submit') }}
</template>
<script>
import bModal from 'bootstrap-vue/lib/components/modal';
export default {
components: {
bModal,
},
props: ['copyingMessage', 'groupName', 'groupId'],
data () {
return {
text: '',
notes: '',
};
},
watch: {
copyingMessage () {
this.text = this.copyingMessage.text;
let baseUrl = 'https://habitica.com';
this.notes = `[${this.copyingMessage.user}](${baseUrl}/static/front/#?memberId=${this.copyingMessage.uuid}) wrote in [${this.groupName}](${baseUrl}/#/options/groups/${this.groupId})`;
},
},
methods: {
close () {
this.$root.$emit('hide::modal', 'copyAsTodo');
},
saveTodo () {
// let newTask = {
// text: this.text,
// type: 'todo',
// notes: this.notes,
// };
// @TODO: Add after tasks: User.addTask({body:newTask});
// @TODO: Notification.text(window.env.t('messageAddedAsToDo'));
this.$root.$emit('hide::modal', 'copyAsTodo');
},
},
};
</script>

View File

@@ -20,40 +20,13 @@
.col-12
h3(v-once) {{ $t('chat') }}
textarea(:placeholder="$t('chatPlaceHolder')")
button.btn.btn-secondary.send-chat.float-right(v-once) {{ $t('send') }}
textarea(:placeholder="$t('chatPlaceHolder')", v-model='newMessage')
button.btn.btn-secondary.send-chat.float-right(v-once, @click='sendMessage()') {{ $t('send') }}
.hr
.hr-middle(v-once) {{ $t('today') }}
.row
.col-md-2
.svg-icon(v-html="icons.like")
.col-md-10
.card(v-for="msg in group.chat", :key="msg.id")
.card-block
h3.leader Character name
span 2 hours ago
.clearfix
strong.float-left {{msg.user}}
.float-right {{msg.timestamp}}
.text {{msg.text}}
hr
span.action(v-once)
.svg-icon(v-html="icons.like")
| {{$t('like')}}
span.action(v-once)
.svg-icon(v-html="icons.copy")
| {{$t('copyAsTodo')}}
span.action(v-once)
.svg-icon(v-html="icons.report")
| {{$t('report')}}
span.action(v-once)
.svg-icon(v-html="icons.delete")
| {{$t('delete')}}
span.action.float-right
.svg-icon(v-html="icons.liked")
| +3
chat-message(:chat.sync='group.chat', :group-id='group._id', group-name='group.name')
.col-md-4.sidebar
.guild-background.row
@@ -379,10 +352,10 @@ import groupUtilities from 'client/mixins/groupsUtilities';
import { mapState } from 'client/libs/store';
import membersModal from './membersModal';
import ownedQuestsModal from './ownedQuestsModal';
import { TAVERN_ID } from 'common/script/constants';
import quests from 'common/script/content/quests';
import percent from 'common/script/libs/percent';
import groupFormModal from './groupFormModal';
import chatMessage from '../chat/chatMessages';
import bCollapse from 'bootstrap-vue/lib/components/collapse';
import bCard from 'bootstrap-vue/lib/components/card';
@@ -404,7 +377,7 @@ import downIcon from 'assets/svg/down.svg';
export default {
mixins: [groupUtilities],
props: ['guildId'],
props: ['groupId'],
components: {
membersModal,
ownedQuestsModal,
@@ -412,6 +385,7 @@ export default {
bCard,
bTooltip,
groupFormModal,
chatMessage,
},
directives: {
bToggle,
@@ -441,6 +415,7 @@ export default {
information: true,
challenges: true,
},
newMessage: '',
};
},
computed: {
@@ -512,8 +487,6 @@ export default {
// Load challenges
// Load group tasks for group plan
// Load approvals for group plan
} else if (this.$route.path.startsWith('/guilds/tavern')) {
this.groupId = TAVERN_ID;
}
this.fetchGuild();
},
@@ -522,6 +495,14 @@ export default {
$route: 'fetchGuild',
},
methods: {
async sendMessage () {
let response = await this.$store.dispatch('chat:postChat', {
groupId: this.group._id,
message: this.newMessage,
});
this.group.chat.unshift(response.message);
this.newMessage = '';
},
updateGuild () {
this.$store.state.editingGroup = this.group;
this.$root.$emit('show::modal', 'guild-form');

View File

@@ -9,8 +9,8 @@
.col-12
h3(v-once) {{ $t('welcomeToTavern') }}
textarea(:placeholder="$t('chatPlaceHolder')")
button.btn.btn-secondary.send-chat.float-right(v-once) {{ $t('send') }}
textarea(:placeholder="$t('chatPlaceHolder')", v-model='newMessage')
button.btn.btn-secondary.send-chat.float-right(v-once, @click='sendMessage()') {{ $t('send') }}
.container.community-guidelines(v-if='communityGuidelinesAccepted')
.row
@@ -21,34 +21,7 @@
.hr
.hr-middle(v-once) {{ $t('today') }}
.row
.col-md-2
.svg-icon(v-html="icons.like")
.col-md-10
.card(v-for="msg in group.chat", :key="msg.id")
.card-block
h3.leader Character name
span 2 hours ago
.clearfix
strong.float-left {{msg.user}}
.float-right {{msg.timestamp}}
.text {{msg.text}}
hr
span.action(v-once)
.svg-icon(v-html="icons.like")
| {{$t('like')}}
span.action(v-once)
.svg-icon(v-html="icons.copy")
| {{$t('copyAsTodo')}}
span.action(v-once)
.svg-icon(v-html="icons.report")
| {{$t('report')}}
span.action(v-once)
.svg-icon(v-html="icons.delete")
| {{$t('delete')}}
span.action.float-right
.svg-icon(v-html="icons.liked")
| +3
chat-message(:chat.sync='group.chat', :group-id='group._id', group-name='group.name')
.col-md-4.sidebar
.section
@@ -96,7 +69,7 @@
li
a(herf='', v-once) {{ $t('faq') }}
li
a(herf='', v-once) {{ $t('glossary') }}
a(herf='', v-html="$t('glossary')")
li
a(herf='', v-once) {{ $t('wiki') }}
li
@@ -106,7 +79,7 @@
li
a(herf='', v-once) {{ $t('requestFeature') }}
li
a(herf='', v-once) {{ $t('communityForum') }}
a(herf='', v-html="$t('communityForum')")
li
a(herf='', v-once) {{ $t('askQuestionGuild') }}
@@ -138,7 +111,6 @@
<style lang='scss' scoped>
@import '~client/assets/scss/colors.scss';
// @TODO: Move chat to component
.chat-row {
position: relative;
@@ -308,11 +280,9 @@
<script>
import { mapState } from 'client/libs/store';
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';
import { TAVERN_ID } from '../../../common/script/constants';
import chatMessage from '../chat/chatMessages';
import gemIcon from 'assets/svg/gem.svg';
import questIcon from 'assets/svg/quest.svg';
import challengeIcon from 'assets/svg/challenge.svg';
@@ -322,15 +292,13 @@ import upIcon from 'assets/svg/up.svg';
import downIcon from 'assets/svg/down.svg';
export default {
components: {
chatMessage,
},
data () {
return {
icons: Object.freeze({
like: likeIcon,
copy: copyIcon,
report: reportIcon,
delete: deleteIcon,
gem: gemIcon,
liked: likedIcon,
questIcon,
challengeIcon,
information: informationIcon,
@@ -418,6 +386,7 @@ export default {
type: 'Moderator',
},
],
newMessage: '',
};
},
computed: {
@@ -426,8 +395,8 @@ export default {
return this.user.flags.communityGuidelinesAccepted;
},
},
mounted () {
// @TODO: Load tavern
async mounted () {
this.group = await this.$store.dispatch('guilds:getGroup', {groupId: TAVERN_ID});
},
methods: {
aggreeToGuideLines () {
@@ -442,6 +411,14 @@ export default {
toggleSleep () {
this.$store.dispatch('user:sleep');
},
async sendMessage () {
let response = await this.$store.dispatch('chat:postChat', {
groupId: TAVERN_ID,
message: this.newMessage,
});
this.group.chat.unshift(response.message);
this.newMessage = '';
},
},
};
</script>

View File

@@ -124,7 +124,7 @@ const router = new VueRouter({
},
{
name: 'guild',
path: 'guild/:guildId',
path: 'guild/:groupId',
component: GuildPage,
props: true,
},

View File

@@ -0,0 +1,64 @@
import axios from 'axios';
export async function getChat (store, payload) {
let response = await axios.get(`/api/v3/groups/${payload.groupId}/chat`);
return response.data.data;
}
export async function postChat (store, payload) {
let url = `/api/v3/groups/${payload.groupId}/chat`;
if (payload.previousMsg) {
url += `?previousMsg=${payload.previousMsg}`;
}
let response = await axios.post(url, {
message: payload.message,
});
// @TODO: pusherSocketId: $rootScope.pusherSocketId, // to make sure the send doesn't get notified of it's own message
return response.data.data;
}
export async function deleteChat (store, payload) {
let url = `/api/v3/groups/${payload.groupId}/chat/${payload.chatId}`;
if (payload.previousMsg) {
url += `?previousMsg=${payload.previousMsg}`;
}
let response = await axios.delete(url);
return response.data.data;
}
export async function like (store, payload) {
let url = `/api/v3/groups/${payload.groupId}/chat/${payload.chatId}/like`;
let response = await axios.post(url);
return response.data.data;
}
export async function flag (store, payload) {
let url = `/api/v3/groups/${payload.groupId}/chat/${payload.chatId}/flag`;
let response = await axios.post(url);
return response.data.data;
}
export async function clearFlagCount (store, payload) {
let url = `/api/v3/groups/${payload.groupId}/chat/${payload.chatId}/clearflags`;
let response = await axios.post(url);
return response.data.data;
}
export async function markChatSeen (store, payload) {
if (store.user.newMessages) delete store.user.newMessages[payload.groupId];
let url = `/api/v3/groups/${payload.groupId}/chat/seen`;
let response = await axios.post(url);
return response.data.data;
}
// @TODO: should this be here?
// function clearCards () {
// User.user._wrapped && User.set({'flags.cardReceived':false});
// }

View File

@@ -9,6 +9,7 @@ import * as members from './members';
import * as auth from './auth';
import * as quests from './quests';
import * as challenges from './challenges';
import * as chat from './chat';
// Actions should be named as 'actionName' and can be accessed as 'namespace:actionName'
// Example: fetch in user.js -> 'user:fetch'
@@ -23,6 +24,7 @@ const actions = flattenAndNamespace({
auth,
quests,
challenges,
chat,
});
export default actions;

View File

@@ -21,6 +21,7 @@
"wishlist": "Wishlist",
"scheduled": "Scheduled",
"like": "Like",
"liked": "Liked",
"copyAsTodo": "Copy as To-Do",
"report": "Report",
"joinGuild": "Join Guild",