Client fixes aug 31 (#9010)

* Separated private message model

* Added markdown to profile

* Add color backgrounds

* Added broken challenge flow

* Added summary field to getgroups

* Fixed group form information loading

* Updated autocomplete to use chat

* Fixed npc styles

* Fixed onload mentions
This commit is contained in:
Keith Holliday
2017-08-31 16:14:09 -06:00
committed by GitHub
parent 87d57dab13
commit cdd3bc3cd6
12 changed files with 322 additions and 114 deletions

View File

@@ -14,7 +14,7 @@ div.autocomplete-selection(v-if='searchResults.length > 0', :style='autocomplete
import groupBy from 'lodash/groupBy';
export default {
props: ['selections', 'text', 'coords', 'groupId', 'chat'],
props: ['selections', 'text', 'coords', 'chat'],
data () {
return {
currentSearch: '',
@@ -43,6 +43,9 @@ export default {
});
},
},
mounted () {
this.grabUserNames();
},
watch: {
text (newText) {
if (newText[newText.length - 1] !== '@') return;
@@ -50,22 +53,19 @@ export default {
this.currentSearchPosition = newText.length - 1;
},
chat () {
this.grabUserNames();
},
},
methods: {
grabUserNames () {
let usersThatMessage = groupBy(this.chat, 'user');
for (let userName in usersThatMessage) {
if (this.tmpSelections.indexOf(userName) === -1) {
let systemMessage = userName === 'undefined';
if (!systemMessage && this.tmpSelections.indexOf(userName) === -1) {
this.tmpSelections.push(userName);
}
}
},
async groupId () {
if (!this.groupId) return;
let members = await this.$store.dispatch('members:getGroupMembers', {groupId: this.groupId});
this.tmpSelections = members.map((member) => {
return member.profile.name;
});
},
},
methods: {
select (result) {
let newText = this.text.slice(0, this.currentSearchPosition + 1) + result;
this.searchActive = false;

View File

@@ -107,11 +107,11 @@
color: #277eab;
}
.tier10 {
.tier9 {
color: #6133b4;
}
.tier9 {
.tier10 {
color: #77f4c7;
fill: #77f4c7;
stroke: #005737;

View File

@@ -172,6 +172,16 @@ b-modal#avatar-modal(title="", size='lg', :hide-header='true', :hide-footer='tru
ng-click='user.items.gear.owned[item.key] ? equip(item.key) : purchase(item.type,item)')
#backgrounds.section.container.customize-section(v-if='activeTopPage === "backgrounds"')
.row.col-12.text-center.set-title
strong {{backgroundShopSets[0].text}}
.row.incentive-background-row
.col-2(v-for='bg in backgroundShopSets[0].items',
@click='unlock("background." + bg.key)',
:popover-title='bg.text',
:popover='bg.notes',
popover-trigger='mouseenter')
.incentive-background(:class='[`background_${bg.key}`]')
.small-rectangle
.row.sub-menu.col-6.offset-3
.col-3.text-center.sub-menu-item(@click='changeSubPage("2017")', :class='{active: activeSubPage === "2017"}')
strong(v-once) 2017
@@ -520,6 +530,61 @@ b-modal#avatar-modal(title="", size='lg', :hide-header='true', :hide-footer='tru
border-radius: 2px;
}
strong {
margin: 0 auto;
}
.incentive-background-row {
margin-bottom: 2em;
}
.incentive-background {
background-image: none;
width: 68px;
height: 68px;
border-radius: 8px;
background-color: #92b6bd;
margin: 0 auto;
padding-top: .3em;
.small-rectangle {
width: 60px;
height: 40px;
border-radius: 4px;
margin: 0 auto;
opacity: .6;
background: white;
}
}
.background_violet {
background-color: #a993ed;
}
.background_blue {
background-color: #92b6bd;
}
.background_green {
background-color: #92bd94;
}
.background_purple {
background-color: #9397bd;
}
.background_red {
background-color: #b77e80;
}
.background_yellow {
background-color: #bcbb91;
}
.incentive-background:hover {
cursor: pointer;
}
.background:hover {
cursor: pointer;
}

View File

@@ -28,7 +28,7 @@
h3(v-once) {{ $t('chat') }}
textarea(:placeholder="!isParty ? $t('chatPlaceholder') : $t('partyChatPlaceholder')", v-model='newMessage', @keydown='updateCarretPosition')
autocomplete(:text='newMessage', v-on:select="selectedAutocomplete", :coords='coords', :groupId='groupId')
autocomplete(:text='newMessage', v-on:select="selectedAutocomplete", :coords='coords', :chat='group.chat')
button.btn.btn-secondary.send-chat.float-right(v-once, @click='sendMessage()') {{ $t('send') }}
button.btn.btn-secondary.float-left(v-once, @click='fetchRecentMessages()') {{ $t('fetchRecentMessages') }}
.col-12

View File

@@ -11,7 +11,7 @@
select.form-control(v-model="workingGroup.newLeader")
option(v-for='member in members', :value="member._id") {{ member.profile.name }}
.form-group
.form-group(v-if='!this.workingGroup.id')
label
strong(v-once) {{$t('privacySettings')}} *
br
@@ -45,7 +45,7 @@
span.custom-control-description(v-once) {{ $t('allowGuildInvitationsFromNonMembers') }}
// "allowGuildInvitationsFromNonMembers": "Allow Guild invitations from non-members",
.form-group(v-if='!creatingParty')
.form-group(v-if='!isParty')
label
strong(v-once) {{$t('guildSummary')}} *
div.summary-count {{charactersRemaining}} {{ $t('charactersRemaining') }}
@@ -56,7 +56,7 @@
label
strong(v-once) {{$t('groupDescription')}} *
a.float-right {{ $t('markdownFormattingHelp') }}
b-form-input.description-textarea(type="text", textarea, :placeholder="isParty ? $t('partyDescriptionPlaceholder') : $t('guildDescriptionPlaceholder')", v-model="workingGroup.description")
textarea.form-control.description-textarea(type="text", textarea, :placeholder="isParty ? $t('partyDescriptionPlaceholder') : $t('guildDescriptionPlaceholder')", v-model="workingGroup.description")
.form-group(v-if='creatingParty && !workingGroup.id')
span
@@ -285,10 +285,31 @@ export default {
return data;
},
mounted () {
// @TODO: do we need this? Maybe us computed. If we need, then make it on show a specific modal
this.$root.$on('shown::modal', () => {
let editingGroup = this.$store.state.editingGroup;
computed: {
editingGroup () {
return this.$store.state.editingGroup;
},
charactersRemaining () {
let currentLength = this.workingGroup.summary ? this.workingGroup.summary.length : 0;
return MAX_SUMMARY_SIZE_FOR_GUILDS - currentLength;
},
title () {
if (this.creatingParty) return this.$t('createParty');
if (!this.workingGroup._id && !this.workingGroup.id) return this.$t('createGuild');
if (this.isParty) return this.$t('updateParty');
return this.$t('updateGuild');
},
creatingParty () {
return this.$store.state.groupFormOptions.createParty;
},
isParty () {
return this.workingGroup.type === 'party';
},
},
watch: {
editingGroup () {
let editingGroup = this.editingGroup;
if (!editingGroup._id) {
this.resetWorkingGroup();
return;
@@ -313,24 +334,6 @@ export default {
if (editingGroup._id) this.workingGroup.id = editingGroup._id;
if (editingGroup.leader._id) this.workingGroup.newLeader = editingGroup.leader._id;
if (editingGroup._id) this.getMembers();
});
},
computed: {
charactersRemaining () {
let currentLength = this.workingGroup.summary ? this.workingGroup.summary.length : 0;
return MAX_SUMMARY_SIZE_FOR_GUILDS - currentLength;
},
title () {
if (this.creatingParty) return this.$t('createParty');
if (!this.workingGroup.id) return this.$t('createGuild');
if (this.isParty) return this.$t('updateParty');
return this.$t('updateGuild');
},
creatingParty () {
return this.$store.state.groupFormOptions.createParty;
},
isParty () {
return this.workingGroup.type === 'party';
},
},
methods: {
@@ -444,15 +447,18 @@ export default {
},
resetWorkingGroup () {
this.workingGroup = {
id: '',
name: '',
type: 'guild',
privacy: 'private',
summary: '',
description: '',
categories: [],
onlyLeaderCreatesChallenges: true,
guildLeaderCantBeMessaged: true,
privateGuild: true,
allowGuildInvitationsFromNonMembers: true,
newLeader: '',
};
},
},

View File

@@ -11,7 +11,7 @@
.row
textarea(placeholder="Friendly reminder: this is an all-ages chat, so please keep content and language appropriate! Consult the Community Guidelines in the sidebar if you have questions.", v-model='newMessage', @keydown='updateCarretPosition')
autocomplete(:text='newMessage', v-on:select="selectedAutocomplete", :coords='coords', :groupId='groupId', :chat='group.chat')
autocomplete(:text='newMessage', v-on:select="selectedAutocomplete", :coords='coords', :chat='group.chat')
button.btn.btn-secondary.send-chat.float-right(v-once, @click='sendMessage()') {{ $t('send') }}
button.btn.btn-secondary.float-left(v-once, @click='fetchRecentMessages()') {{ $t('fetchRecentMessages') }}

View File

@@ -0,0 +1,35 @@
<template lang="pug">
b-modal#private-message(title="Message", size='sm', :hide-footer="true")
textarea.form-control(v-model='privateMessage')
button.btn.btn-primary(@click='sendPrivateMessage()') Send
</template>
<script>
import bModal from 'bootstrap-vue/lib/components/modal';
import notifications from 'client/mixins/notifications';
export default {
mixins: [notifications],
props: ['userIdToMessage'],
components: {
bModal,
},
data () {
return {
privateMessage: '',
};
},
methods: {
async sendPrivateMessage () {
if (!this.privateMessage || !this.userIdToMessage) return;
await this.$store.dispatch('members:sendPrivateMessage', {
message: this.privateMessage,
toUserId: this.userIdToMessage,
});
this.text(this.$t('messageSentAlert'));
},
},
};
</script>

View File

@@ -0,0 +1,77 @@
<template lang='pug'>
b-modal#broken-task-modal(title="Broken Challenge", size='sm', :hide-footer="true", v-if='brokenChallengeTask.challenge')
.modal-body
div(v-if='brokenChallengeTask.challenge.broken === "TASK_DELETED" || brokenChallengeTask.challenge.broken === "CHALLENGE_TASK_NOT_FOUND"')
h2 {{ $t('brokenTask') }}
div
button.btn.btn-primary(@click='unlink("keep")') {{ $t('keepIt') }}
button.btn.btn-danger(@click='removeTask(obj)') {{ $t('removeIt') }}
div(v-if='brokenChallengeTask.challenge.broken === "CHALLENGE_DELETED"')
h2 {{ $t('brokenChallenge') }}
div
button.btn.btn-primary(@click='unlink("keep-all")') {{ $t('keepThem') }}
button.btn.btn-danger(@click='unlink("remove-all")') {{ $t('removeThem') }}
div(v-if='brokenChallengeTask.challenge.broken === "CHALLENGE_CLOSED"')
h2(v-html="$t('challengeCompleted', {user: brokenChallengeTask.challenge.winner})")
div
button.btn.btn-primary(@click='unlink("keep-all")') {{ $t('keepThem') }}
button.btn.btn-danger(@click='unlink("remove-all")') {{ $t('removeThem') }}
// @TODO: I ported this over, but do we use it anymore?
//div(v-if='brokenChallengeTask.challenge.broken === "UNSUBSCRIBED"')
p {{ $t('unsubChallenge') }}
p
a(@click="unlink('keep-all')") {{ $t('keepThem') }}
| &nbsp;|&nbsp;
a(@click="unlink('remove-all')") {{ $t('removeThem') }}
</template>
<style scoped>
.modal-body {
padding-bottom: 2em;
}
</style>
<script>
import { mapActions } from 'client/libs/store';
import bModal from 'bootstrap-vue/lib/components/modal';
import notifications from 'client/mixins/notifications';
export default {
mixins: [notifications],
props: ['brokenChallengeTask'],
components: {
bModal,
},
methods: {
...mapActions({
destroyTask: 'tasks:destroy',
unlinkOneTask: 'tasks:unlinkOneTask',
unlinkAllTasks: 'tasks:unlinkAllTasks',
}),
async unlink (keepOption) {
if (keepOption.indexOf('-all') !== -1) {
this.unlinkAllTasks({
challengeId: this.brokenChallengeTask.challenge.id,
keep: keepOption,
});
await this.$store.dispatch('tasks:fetchUserTasks', true);
this.close();
return;
}
this.unlinkOneTask({
task: this.brokenChallengeTask,
keep: keepOption,
});
this.close();
},
removeTask () {
if (!confirm('Are you sure you want to delete this task?')) return;
this.destroyTask(this.brokenChallengeTask);
},
close () {
this.$root.$emit('hide::modal', 'broken-task-modal');
},
},
};
</script>

View File

@@ -1,4 +1,6 @@
<template lang="pug">
div
broken-task-modal(:brokenChallengeTask='brokenChallengeTask')
.task(@click='castEnd($event, task)')
approval-header(:task='task', v-if='this.task.group.id', :group='group')
.d-flex(:class="{'task-not-scoreable': isUser !== true}")
@@ -35,7 +37,8 @@
span.m-0(v-if="task.up && task.down") &nbsp;|&nbsp;
span.m-0(v-if="task.down") -{{task.counterDown}}
.d-flex.align-items-center(v-if="task.challenge && task.challenge.id")
.svg-icon.challenge(v-html="icons.challenge")
.svg-icon.challenge(v-html="icons.challenge", v-if='!task.challenge.broken')
.svg-icon.challenge.broken(v-html="icons.challenge", v-if='task.challenge.broken', @click='handleBrokenTask(task)')
b-popover.tags-popover.no-span-margin(
:triggers="['hover']",
:placement="'bottom'",
@@ -194,6 +197,10 @@
margin: 9px 8px;
}
.challenge.broken {
color: $red-50;
}
.left-control, .right-control {
width: 40px;
flex-shrink: 0;
@@ -301,6 +308,7 @@ import checkIcon from 'assets/svg/check.svg';
import bPopover from 'bootstrap-vue/lib/components/popover';
import markdownDirective from 'client/directives/markdown';
import notifications from 'client/mixins/notifications';
import brokenTaskModal from './brokenTaskModal';
import approvalHeader from './approvalHeader';
import approvalFooter from './approvalFooter';
@@ -310,6 +318,7 @@ export default {
bPopover,
approvalFooter,
approvalHeader,
brokenTaskModal,
},
directives: {
markdown: markdownDirective,
@@ -327,6 +336,7 @@ export default {
tags: tagsIcon,
check: checkIcon,
}),
brokenChallengeTask: {},
};
},
computed: {
@@ -468,6 +478,10 @@ export default {
}
}
},
handleBrokenTask (task) {
this.brokenChallengeTask = task;
this.$root.$emit('show::modal', 'broken-task-modal');
},
},
};
</script>

View File

@@ -23,7 +23,7 @@ div
.col-8
.about
h2 About
p {{user.profile.blurb}}
p(v-markdown='user.profile.blurb')
.photo
h2 Photo
img.img-rendering-auto(v-if='user.profile.imageUrl', :src='user.profile.imageUrl')
@@ -254,11 +254,7 @@ div
.col-4(v-if='user.stats.points', @click='allocate(stat)')
button.btn.btn-primary(popover-trigger='mouseenter', popover-placement='right',
:popover='$t(statInfo.allocatepop)') +
// @TODO: Make separate componenet
b-modal#private-message(title="Message", size='sm', :hide-footer="true")
textarea.form-control(v-model='privateMessage')
button.btn.btn-primary(@click='sendPrivateMessage()') Send
private-message-modal(:userIdToMessage='userIdToMessage')
</template>
<style lang="scss" scoped>
@@ -335,8 +331,9 @@ import { beastMasterProgress, mountMasterProgress } from '../../../common/script
import statsComputed from '../../../common/script/libs/statsComputed';
import autoAllocate from '../../../common/script/fns/autoAllocate';
import allocate from '../../../common/script/ops/allocate';
import notifications from 'client/mixins/notifications';
import privateMessageModal from 'client/components/private-message-modal';
import markdown from 'client/directives/markdown';
import achievementsLib from '../../../common/script/libs/achievements';
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
@@ -345,14 +342,16 @@ const DROP_ANIMALS = keys(Content.pets);
const TOTAL_NUMBER_OF_DROP_ANIMALS = DROP_ANIMALS.length;
export default {
directives: {
markdown,
},
components: {
bModal,
privateMessageModal,
},
mixins: [notifications],
data () {
return {
userIdToMessage: '',
privateMessage: '',
editing: false,
editingProfile: {
name: '',
@@ -447,15 +446,6 @@ export default {
this.userIdToMessage = this.user._id;
this.$root.$emit('show::modal', 'private-message');
},
async sendPrivateMessage () {
if (!this.privateMessage || !this.userIdToMessage) return;
await this.$store.dispatch('members:sendPrivateMessage', {
message: this.privateMessage,
toUserId: this.userIdToMessage,
});
this.text(this.$t('messageSentAlert'));
},
getProgressDisplay () {
// let currentLoginDay = Content.loginIncentives[this.user.loginIncentives];
// if (!currentLoginDay) return this.$t('checkinReceivedAllRewardsMessage');

View File

@@ -174,3 +174,24 @@ export async function approve (store, payload) {
let response = await axios.post(`/api/v3/tasks/${payload.taskId}/approve/${payload.userId}`);
return response.data.data;
}
export async function unlinkOneTask (store, payload) {
if (!payload.keep) payload.keep = 'keep';
let task = payload.task;
const list = store.state.tasks.data[`${task.type}s`];
const taskIndex = list.findIndex(t => t._id === task._id);
if (taskIndex > -1) {
list.splice(taskIndex, 1);
}
let response = await axios.post(`/api/v3/tasks/unlink-one/${payload.task._id}?keep=${payload.keep}`);
return response.data.data;
}
export async function unlinkAllTasks (store, payload) {
if (!payload.keep) payload.keep = 'keep-all';
let response = await axios.post(`/api/v3/tasks/unlink-all/${payload.challengeId}?keep=${payload.keep}`);
return response.data.data;
}

View File

@@ -164,7 +164,7 @@ schema.statics.sanitizeUpdate = function sanitizeUpdate (updateObj) {
};
// Basic fields to fetch for populating a group info
export let basicFields = 'name type privacy leader';
export let basicFields = 'name type privacy leader summary';
schema.pre('remove', true, async function preRemoveGroup (next, done) {
next();