refactor(client): Do not mutate prop directly

Related #6595
This commit is contained in:
syuilo 2020-07-27 08:46:21 +09:00
parent b32737cdff
commit b5a1fdd4c7
5 changed files with 73 additions and 70 deletions

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
class="note _panel" class="note _panel"
v-show="!isDeleted && !hideThisNote" v-show="!isDeleted"
:tabindex="!isDeleted ? '-1' : null" :tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }" :class="{ renote: isRenote }"
v-hotkey="keymap" v-hotkey="keymap"
@ -37,16 +37,16 @@
<mk-avatar class="avatar" :user="appearNote.user" v-once/> <mk-avatar class="avatar" :user="appearNote.user" v-once/>
<div class="main"> <div class="main">
<x-note-header class="header" :note="appearNote" :mini="true"/> <x-note-header class="header" :note="appearNote" :mini="true"/>
<div class="body" v-if="appearNote.deletedAt == null" ref="noteBody"> <div class="body" ref="noteBody">
<p v-if="appearNote.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/> <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="emojis" v-once/>
<x-cw-button v-model="showContent" :note="appearNote"/> <x-cw-button v-model="showContent" :note="appearNote"/>
</p> </p>
<div class="content" v-show="appearNote.cw == null || showContent"> <div class="content" v-show="appearNote.cw == null || showContent">
<div class="text"> <div class="text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link> <router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link>
<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/> <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="emojis" v-once/>
<a class="rp" v-if="appearNote.renote != null">RN:</a> <a class="rp" v-if="appearNote.renote != null">RN:</a>
</div> </div>
<div class="files" v-if="appearNote.files.length > 0"> <div class="files" v-if="appearNote.files.length > 0">
@ -57,8 +57,8 @@
<div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div> <div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div>
</div> </div>
</div> </div>
<footer v-if="appearNote.deletedAt == null" class="footer"> <footer class="footer">
<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/> <x-reactions-viewer :note="appearNote" :reactions="reactions" :my-reaction="myReaction" :emojis="emojis" ref="reactionsViewer"/>
<button @click="reply()" class="button _button"> <button @click="reply()" class="button _button">
<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template> <template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template>
<template v-else><fa :icon="faReply"/></template> <template v-else><fa :icon="faReply"/></template>
@ -70,17 +70,16 @@
<button v-else class="button _button"> <button v-else class="button _button">
<fa :icon="faBan"/> <fa :icon="faBan"/>
</button> </button>
<button v-if="!isMyNote && appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> <button v-if="!isMyNote && myReaction == null" class="button _button" @click="react()" ref="reactButton">
<fa :icon="faPlus"/> <fa :icon="faPlus"/>
</button> </button>
<button v-if="!isMyNote && appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton"> <button v-if="!isMyNote && myReaction != null" class="button _button reacted" @click="undoReact()" ref="reactButton">
<fa :icon="faMinus"/> <fa :icon="faMinus"/>
</button> </button>
<button class="button _button" @click="menu()" ref="menuButton"> <button class="button _button" @click="menu()" ref="menuButton">
<fa :icon="faEllipsisH"/> <fa :icon="faEllipsisH"/>
</button> </button>
</footer> </footer>
<div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div>
</div> </div>
</article> </article>
<x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/> <x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
@ -142,7 +141,10 @@ export default Vue.extend({
conversation: [], conversation: [],
replies: [], replies: [],
showContent: false, showContent: false,
hideThisNote: false, isDeleted: false,
myReaction: null,
reactions: {},
emojis: [],
noteBody: this.$refs.noteBody, noteBody: this.$refs.noteBody,
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
}; };
@ -186,10 +188,6 @@ export default Vue.extend({
return this.isRenote ? this.note.renote : this.note; return this.isRenote ? this.note.renote : this.note;
}, },
isDeleted(): boolean {
return this.appearNote.deletedAt != null || this.note.deletedAt != null;
},
isMyNote(): boolean { isMyNote(): boolean {
return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId); return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId);
}, },
@ -203,9 +201,7 @@ export default Vue.extend({
}, },
reactionsCount(): number { reactionsCount(): number {
return this.appearNote.reactions return sum(Object.values(this.reactions));
? sum(Object.values(this.appearNote.reactions))
: 0;
}, },
urls(): string[] { urls(): string[] {
@ -232,6 +228,10 @@ export default Vue.extend({
}, },
created() { created() {
this.emojis = [...this.appearNote.emojis];
this.reactions = { ...this.appearNote.reactions };
this.myReaction = this.appearNote.myReaction;
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection = this.$root.stream; this.connection = this.$root.stream;
} }
@ -261,7 +261,7 @@ export default Vue.extend({
this.connection.on('_connected_', this.onStreamConnected); this.connection.on('_connected_', this.onStreamConnected);
} }
this.noteBody = this.$refs.noteBody this.noteBody = this.$refs.noteBody;
}, },
beforeDestroy() { beforeDestroy() {
@ -277,7 +277,7 @@ export default Vue.extend({
(this as any).$root.api('promo/read', { (this as any).$root.api('promo/read', {
noteId: this.appearNote.id noteId: this.appearNote.id
}); });
this.hideThisNote = true; this.isDeleted = true;
}, },
capture(withHandler = false) { capture(withHandler = false) {
@ -310,26 +310,20 @@ export default Vue.extend({
const reaction = body.reaction; const reaction = body.reaction;
if (body.emoji) { if (body.emoji) {
const emojis = this.appearNote.emojis || []; if (!this.emojis.includes(body.emoji)) {
if (!emojis.includes(body.emoji)) { this.emojis.push(body.emoji);
emojis.push(body.emoji);
Vue.set(this.appearNote, 'emojis', emojis);
} }
} }
if (this.appearNote.reactions == null) { if (this.reactions[reaction] == null) {
Vue.set(this.appearNote, 'reactions', {}); Vue.set(this.reactions, reaction, 0);
}
if (this.appearNote.reactions[reaction] == null) {
Vue.set(this.appearNote.reactions, reaction, 0);
} }
// Increment the count // Increment the count
this.appearNote.reactions[reaction]++; this.reactions[reaction]++;
if (body.userId == this.$store.state.i.id) { if (body.userId === this.$store.state.i.id) {
Vue.set(this.appearNote, 'myReaction', reaction); this.myReaction = reaction;
} }
break; break;
} }
@ -337,19 +331,15 @@ export default Vue.extend({
case 'unreacted': { case 'unreacted': {
const reaction = body.reaction; const reaction = body.reaction;
if (this.appearNote.reactions == null) { if (this.reactions[reaction] == null) {
return;
}
if (this.appearNote.reactions[reaction] == null) {
return; return;
} }
// Decrement the count // Decrement the count
if (this.appearNote.reactions[reaction] > 0) this.appearNote.reactions[reaction]--; if (this.reactions[reaction] > 0) this.reactions[reaction]--;
if (body.userId == this.$store.state.i.id) { if (body.userId === this.$store.state.i.id) {
Vue.set(this.appearNote, 'myReaction', null); this.myReaction = null;
} }
break; break;
} }
@ -357,19 +347,14 @@ export default Vue.extend({
case 'pollVoted': { case 'pollVoted': {
const choice = body.choice; const choice = body.choice;
this.appearNote.poll.choices[choice].votes++; this.appearNote.poll.choices[choice].votes++;
if (body.userId == this.$store.state.i.id) { if (body.userId === this.$store.state.i.id) {
Vue.set(this.appearNote.poll.choices[choice], 'isVoted', true); Vue.set(this.appearNote.poll.choices[choice], 'isVoted', true);
} }
break; break;
} }
case 'deleted': { case 'deleted': {
Vue.set(this.appearNote, 'deletedAt', body.deletedAt); this.isDeleted = true;
Vue.set(this.appearNote, 'renote', null);
this.appearNote.text = null;
this.appearNote.fileIds = [];
this.appearNote.poll = null;
this.appearNote.cw = null;
break; break;
} }
} }
@ -442,11 +427,11 @@ export default Vue.extend({
}); });
}, },
undoReact(note) { undoReact() {
const oldReaction = note.myReaction; const oldReaction = this.myReaction;
if (!oldReaction) return; if (!oldReaction) return;
this.$root.api('notes/reactions/delete', { this.$root.api('notes/reactions/delete', {
noteId: note.id noteId: this.appearNote.id
}); });
}, },
@ -638,7 +623,7 @@ export default Vue.extend({
this.$root.api('notes/delete', { this.$root.api('notes/delete', {
noteId: this.note.id noteId: this.note.id
}); });
Vue.set(this.note, 'deletedAt', new Date()); this.isDeleted = true;
} }
}], }],
source: this.$refs.renoteTime, source: this.$refs.renoteTime,
@ -925,10 +910,6 @@ export default Vue.extend({
} }
} }
} }
> .deleted {
opacity: 0.7;
}
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<button <button
class="hkzvhatu _button" class="hkzvhatu _button"
:class="{ reacted: note.myReaction == reaction, canToggle }" :class="{ reacted: myReaction == reaction, canToggle }"
@click="toggleReaction(reaction)" @click="toggleReaction(reaction)"
v-if="count > 0" v-if="count > 0"
@touchstart="onMouseover" @touchstart="onMouseover"
@ -11,7 +11,7 @@
ref="reaction" ref="reaction"
v-particle="canToggle" v-particle="canToggle"
> >
<x-reaction-icon :reaction="reaction" :custom-emojis="note.emojis" ref="icon"/> <x-reaction-icon :reaction="reaction" :custom-emojis="emojis" ref="icon"/>
<span>{{ count }}</span> <span>{{ count }}</span>
</button> </button>
</template> </template>
@ -30,6 +30,14 @@ export default Vue.extend({
type: String, type: String,
required: true, required: true,
}, },
myReaction: {
type: String,
required: false,
},
emojis: {
type: Array,
required: true,
},
count: { count: {
type: Number, type: Number,
required: true, required: true,
@ -71,7 +79,7 @@ export default Vue.extend({
toggleReaction() { toggleReaction() {
if (!this.canToggle) return; if (!this.canToggle) return;
const oldReaction = this.note.myReaction; const oldReaction = this.myReaction;
if (oldReaction) { if (oldReaction) {
this.$root.api('notes/reactions/delete', { this.$root.api('notes/reactions/delete', {
noteId: this.note.id noteId: this.note.id

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="tdflqwzn" :class="{ isMe }"> <div class="tdflqwzn" :class="{ isMe }">
<x-reaction v-for="(count, reaction) in note.reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :key="reaction"/> <x-reaction v-for="(count, reaction) in reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :my-reaction="myReaction" :emojis="emojis" :key="reaction"/>
</div> </div>
</template> </template>
@ -12,16 +12,28 @@ export default Vue.extend({
components: { components: {
XReaction XReaction
}, },
data() {
return {
initialReactions: new Set(Object.keys(this.note.reactions))
};
},
props: { props: {
note: { note: {
type: Object, type: Object,
required: true required: true
}, },
reactions: {
type: Object,
required: true
},
myReaction: {
type: String,
required: false,
},
emojis: {
type: Array,
required: true,
},
},
data() {
return {
initialReactions: new Set(Object.keys(this.note.reactions))
};
}, },
computed: { computed: {
isMe(): boolean { isMe(): boolean {

View File

@ -52,8 +52,8 @@ export default Vue.extend({
}); });
const prepend = note => { const prepend = note => {
const _note = JSON.parse(JSON.stringify(note)); // deepcopy Object.freeze(note);
(this.$refs.tl as any).prepend(_note); (this.$refs.tl as any).prepend(note);
this.$emit('note'); this.$emit('note');

View File

@ -74,10 +74,6 @@ export default (opts) => ({
}, },
methods: { methods: {
updateItem(i, item) {
Vue.set((this as any).items, i, item);
},
reload() { reload() {
this.items = []; this.items = [];
this.init(); this.init();
@ -94,6 +90,9 @@ export default (opts) => ({
...params, ...params,
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1, limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
}).then(items => { }).then(items => {
for (const item of items) {
Object.freeze(item);
}
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) { if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
items.pop(); items.pop();
this.items = this.pagination.reversed ? [...items].reverse() : items; this.items = this.pagination.reversed ? [...items].reverse() : items;
@ -130,6 +129,9 @@ export default (opts) => ({
untilId: this.items[this.items.length - 1].id, untilId: this.items[this.items.length - 1].id,
}), }),
}).then(items => { }).then(items => {
for (const item of items) {
Object.freeze(item);
}
if (items.length > SECOND_FETCH_LIMIT) { if (items.length > SECOND_FETCH_LIMIT) {
items.pop(); items.pop();
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);