Files
habitica/website/client/src/components/groups/chat.vue
T
negue 7c9c45ac5f combined messages restyling - next round (#15386)
* split component prepare new views / states

* extract empty and disabled state as components

* fix empty state mail icon

* first logic switching between modes, move page to /private-messages/index.vue

* extract autoCompleteHelper.js

* style header + start new message input

* style plus button + focus input

* state logic, types for sanity

* WIP PM new Message started

* add /members/username test

* first design changes to messageCard

* delete private message or chat - based on the mode

* copy as todo

* mention links to modal

* report chat or private message

* WIP likeButton

* likeButton styling

* hide like on private message cards

* fix unit test

* replace copy as todo - to just a copy to clipboard

* style changes

* menu position + like button width

* dropdown items background + like font

* fix like button padding

* move api endpoints and tests around to group inbox methods  + like for inbox private messages

* restyle system messages

* Dropdown Radius and Padding

* WIP system messages

* fix lint

* copy delta commit of allowing liking own private messages

* enable liking private messages

* fix menu non hovered item icon color

* fix import path

* ignore background on system messages

* requested changes + migration

* update migration to update the unique id to some messages and delete the duplicates

* migration based on users pagination

* fix(migration): use Promise.all

* change to bulkWrites per User, and all messages in one run (of a user)

* check for array

* use rest operator ...

* skip sorting to get the users

* remove migration, disable like for private messages without uniqueMessageId

* lean+bulkWrite for likes, add time checks for like and auth for further debugging

* add a limit 2 get the messages by uniqueId

* Adding a simple server start script

* remove pinned nodemon dep

* fix inbox controller/tests

* fix / requested style changes

* fix empty state padding /

* hide avatar weapons on messages - fix avatar spacing on messages

* Hourglass Simplification (#15323)

* begin removing obsolete tests

* begin refactoring

* update cron tests

* cleanup

* finish basic implementation of new logic

* add more subscription tests

* subscription test improvements

* return nextHourglassDate again

* fix gem limit

* fix(test): short circuit this.

* fix(admin): correct logic and style for shrimple subs

* WIP(frontend): draft of main subs page view

* fix hourglass count

* Fix hourglass logic for upgrades

* fix admin panel display

* WIP(subs): extant Stripe state

* fix admin panel strings

* fix missing transaction type

* add new field for cumulative subscription count

* show date for hourglass bonus if it was received

* fix test

* feat(subscription): max Gems progress readout

* fix(css): correct and refactor heights and selection states

* fix(subs): correct border-radius and redirect

* fix(stripe): correct redirect after success

* Admin panel display fixes

* don’t give additional HG for new sub if they already got one this month

* fix issue with promo hourglasses

* fix(subscription): update layout when gifting

* fix(subscriptions): more gift layout revisions

* fix(subscriptions): minor visual updates

* fix(subs): pass autoRenews through Stripe

* fix(subs): gifts DON't renew

* fix(lint): unnecessary ternary

* fix(lint): do negate object ig

* fix(subs): try again on gifts

* fix(subs): unhovery and un-12-monthy

* fix bug with incorrectly giving HG bonus

* remove only

* fix test

* fix test

* fix(subs): also redirect to subs after gift sub

* fix(subs): fix typeError

* fix(g1g1): don't try to find Gems promo during bogo

---------

Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Kalista Payne <sabe@habitica.com>

* chore(sprites): update subproject

* fix(layout): tighten cancellation note

* fix(subs): Google wording and HG escape

* chore(testing): fake g1g1 dates

* fix(subs): don't hide HG preview entirely

* fix(subs): center next hourglass message

* working validatedTextInput.vue within start-new-conversation-input-header.vue 🎉

* fix(git): remove changes from old develop

* Revert "fix(git): remove changes from old develop"

This reverts commit 0e30f7df00.

* fix(git): no actually just this file i guesss

* adding an empty loading state, hiding

* fought the avatar arch nemesis again

* fix chatMessages (party chat) message spacing

* move disabled text back to above the input area - re-enable input area

* show disabled private messages top panel

* fix font color

* fixing uiStates - removing disabled - moving the own user check to the last

* fix(lint): add missing prop defaults

* fix(lint): object default should be fn

* fix(chat): correct grammar in error

* remove weapon position relative

* revert most of avatar.vue changes, add back weapons in chat message UI

* show date tooltip above system / skill messages

* fix toggle disable icon position

* trivial CSS cleanup

* fix(typo): English syntax in test

* chore(test): small style cleanup

* chore(logging): revert debug function

* chore(debug): remove timers from inbox like

---------

Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
Co-authored-by: Phillip Thelen <phillip@habitica.com>
2025-03-04 17:00:24 -06:00

291 lines
7.3 KiB
Vue

<template>
<div class="row chat-row">
<div class="col-12">
<h3
class="float-left label"
:class="{accepted: communityGuidelinesAccepted }"
>
{{ label }}
</h3>
<div
v-markdown="$t('markdownFormattingHelp')"
class="float-right"
></div>
<div
v-if="communityGuidelinesAccepted"
class="row"
>
<textarea
ref="user-entry"
v-model="newMessage"
dir="auto"
:placeholder="placeholder"
:class="{'user-entry': newMessage}"
:maxlength="MAX_MESSAGE_LENGTH"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keyup.ctrl.enter="sendMessageShortcut()"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
@paste="disableMessageSendShortcut()"
></textarea>
<span>{{ currentLength }} / {{ MAX_MESSAGE_LENGTH }}</span>
<autocomplete
ref="autocomplete"
:text="newMessage"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
:chat="group.chat"
@select="selectedAutocomplete"
/>
</div>
<community-guidelines />
<div class="row chat-actions">
<div class="col-6 chat-receive-actions">
<button
v-once
class="btn btn-secondary float-left fetch"
@click="fetchRecentMessages()"
>
{{ $t('fetchRecentMessages') }}
</button>
<button
v-once
class="btn btn-secondary float-left"
@click="reverseChat()"
>
{{ $t('reverseChat') }}
</button>
</div>
<div class="col-6 chat-send-actions">
<button
class="btn btn-primary send-chat float-right"
:disabled="!communityGuidelinesAccepted"
:class="{ disabled: !communityGuidelinesAccepted }"
@click="sendMessage()"
>
{{ $t('send') }}
</button>
</div>
</div>
<slot name="additionRow"></slot>
<div class="row">
<div class="hr col-12"></div>
<chat-messages
:chat.sync="group.chat"
:group-type="group.type"
:group-id="group._id"
:group-name="group.name"
/>
</div>
</div>
</div>
</template>
<script>
import { MAX_MESSAGE_LENGTH } from '@/../../common/script/constants';
import externalLinks from '../../mixins/externalLinks';
import autocomplete from '../chat/autoComplete';
import communityGuidelines from './communityGuidelines';
import chatMessages from '../chat/chatMessages';
import { mapState } from '@/libs/store';
import markdownDirective from '@/directives/markdown';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
export default {
directives: {
markdown: markdownDirective,
},
components: {
autocomplete,
communityGuidelines,
chatMessages,
},
mixins: [externalLinks, autoCompleteHelperMixin],
props: ['label', 'group', 'placeholder'],
data () {
return {
newMessage: '',
sending: false,
chat: {
submitDisable: false,
submitTimeout: null,
},
textbox: null,
MAX_MESSAGE_LENGTH: MAX_MESSAGE_LENGTH.toString(),
};
},
computed: {
...mapState({ user: 'user.data' }),
currentLength () {
return this.newMessage.length;
},
communityGuidelinesAccepted () {
return this.user.flags.communityGuidelinesAccepted;
},
},
mounted () {
this.textbox = this.$refs['user-entry'];
this.handleExternalLinks();
},
updated () {
this.handleExternalLinks();
},
methods: {
async sendMessageShortcut () {
// If the user recently pasted in the text field, don't submit
if (!this.chat.submitDisable) {
this.sendMessage();
}
},
async sendMessage () {
if (this.sending) return;
this.sending = true;
let response;
try {
response = await this.$store.dispatch('chat:postChat', {
group: this.group,
message: this.newMessage,
});
} catch (e) {
// catch exception to allow function to continue
}
if (response) {
this.group.chat.unshift(response.message);
this.newMessage = '';
}
this.sending = false;
// @TODO: I would like to not reload everytime we send. Why are we reloading?
// The response has all the necessary data...
const chat = await this.$store.dispatch('chat:getChat', { groupId: this.group._id });
this.group.chat = chat;
},
disableMessageSendShortcut () {
// Some users were experiencing accidental sending of messages after pasting
// So, after pasting, disable the shortcut for a second.
this.chat.submitDisable = true;
if (this.chat.submitTimeout) {
// If someone pastes during the disabled period, prevent early re-enable
clearTimeout(this.chat.submitTimeout);
this.chat.submitTimeout = null;
}
this.chat.submitTimeout = window.setTimeout(() => {
this.chat.submitTimeout = null;
this.chat.submitDisable = false;
}, 500);
},
selectedAutocomplete (newText, newCaret) {
this.newMessage = newText;
// Wait for v-modal to update
this.$nextTick(() => {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
});
},
fetchRecentMessages () {
this.$emit('fetchRecentMessages');
},
reverseChat () {
this.group.chat.reverse();
},
},
beforeRouteUpdate (to, from, next) {
// Reset chat
this.newMessage = '';
this.autoCompleteMixinResetCoordsPosition();
next();
},
};
</script>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
@import '~@/assets/scss/variables.scss';
.chat-actions {
margin-top: 1em;
.chat-receive-actions {
padding-left: 0;
button {
margin-bottom: 1em;
&:not(:last-child) {
margin-right: 1em;
}
}
}
.chat-send-actions {
padding-right: 0;
}
}
.chat-row {
position: relative;
.label:not(.accepted) {
color: #a5a1ac;
}
.row {
margin-left: 0;
margin-right: 0;
clear: both;
}
textarea {
min-height: 150px;
width: 100%;
background-color: $white;
border: solid 1px $gray-400;
font-style: italic;
line-height: 1.43;
color: $gray-300;
padding: 10px 12px;
}
.user-entry {
font-style: normal;
color: $black;
}
.hr {
width: 100%;
height: 20px;
border-bottom: 1px solid $gray-500;
text-align: center;
margin: 2em 0;
}
.hr-middle {
font-size: 16px;
font-weight: bold;
font-family: 'Roboto Condensed';
line-height: 1.5;
text-align: center;
color: $gray-200;
background-color: $gray-700;
padding: .2em;
margin-top: .2em;
display: inline-block;
width: 100px;
}
}
</style>