mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
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:
@@ -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 |
139
website/client/components/chat/chatMessages.vue
Normal file
139
website/client/components/chat/chatMessages.vue
Normal 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>
|
||||
66
website/client/components/chat/copyAsTodoModal.vue
Normal file
66
website/client/components/chat/copyAsTodoModal.vue
Normal 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
|
||||
|
|
||||
|
||||
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>
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -124,7 +124,7 @@ const router = new VueRouter({
|
||||
},
|
||||
{
|
||||
name: 'guild',
|
||||
path: 'guild/:guildId',
|
||||
path: 'guild/:groupId',
|
||||
component: GuildPage,
|
||||
props: true,
|
||||
},
|
||||
|
||||
64
website/client/store/actions/chat.js
Normal file
64
website/client/store/actions/chat.js
Normal 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});
|
||||
// }
|
||||
@@ -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;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"wishlist": "Wishlist",
|
||||
"scheduled": "Scheduled",
|
||||
"like": "Like",
|
||||
"liked": "Liked",
|
||||
"copyAsTodo": "Copy as To-Do",
|
||||
"report": "Report",
|
||||
"joinGuild": "Join Guild",
|
||||
|
||||
Reference in New Issue
Block a user