diff --git a/src/client/components/note.vue b/src/client/components/note.vue index 9bbf763494..fba812fc71 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -40,14 +40,14 @@ <x-note-header class="header" :note="appearNote" :mini="true"/> <div class="body" ref="noteBody"> <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="emojis" v-once/> + <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/> <x-cw-button v-model="showContent" :note="appearNote"/> </p> <div class="content" v-show="appearNote.cw == null || showContent"> <div class="text"> <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> - <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="emojis" v-once/> + <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/> <a class="rp" v-if="appearNote.renote != null">RN:</a> </div> <div class="files" v-if="appearNote.files.length > 0"> @@ -59,7 +59,7 @@ </div> </div> <footer class="footer"> - <x-reactions-viewer :note="appearNote" :reactions="reactions" :my-reaction="myReaction" :emojis="emojis" ref="reactionsViewer"/> + <x-reactions-viewer :note="appearNote" ref="reactionsViewer"/> <button @click="reply()" class="button _button"> <template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template> <template v-else><fa :icon="faReply"/></template> @@ -71,10 +71,10 @@ <button v-else class="button _button"> <fa :icon="faBan"/> </button> - <button v-if="!isMyNote && myReaction == null" class="button _button" @click="react()" ref="reactButton"> + <button v-if="!isMyNote && appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> <fa :icon="faPlus"/> </button> - <button v-if="!isMyNote && myReaction != null" class="button _button reacted" @click="undoReact()" ref="reactButton"> + <button v-if="!isMyNote && appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton"> <fa :icon="faMinus"/> </button> <button class="button _button" @click="menu()" ref="menuButton"> @@ -116,6 +116,11 @@ import copyToClipboard from '../scripts/copy-to-clipboard'; import { checkWordMute } from '../scripts/check-word-mute'; export default Vue.extend({ + model: { + prop: 'note', + event: 'updated' + }, + components: { XSub, XNoteHeader, @@ -152,9 +157,6 @@ export default Vue.extend({ showContent: false, isDeleted: false, muted: false, - myReaction: null, - reactions: {}, - emojis: [], noteBody: this.$refs.noteBody, faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug }; @@ -211,7 +213,9 @@ export default Vue.extend({ }, reactionsCount(): number { - return sum(Object.values(this.reactions)); + return this.appearNote.reactions + ? sum(Object.values(this.appearNote.reactions)) + : 0; }, urls(): string[] { @@ -242,9 +246,8 @@ export default Vue.extend({ this.connection = this.$root.stream; } - this.emojis = [...this.appearNote.emojis]; - this.reactions = { ...this.appearNote.reactions }; - this.myReaction = this.appearNote.myReaction; + console.log(this.note); + this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords); if (this.detail) { @@ -284,6 +287,19 @@ export default Vue.extend({ }, methods: { + updateAppearNote(v) { + this.$emit('updated', Object.freeze(this.isRenote ? { + ...this.note, + renote: { + ...this.note.renote, + ...v + } + } : { + ...this.note, + ...v + })); + }, + readPromo() { (this as any).$root.api('promo/read', { noteId: this.appearNote.id @@ -320,47 +336,83 @@ export default Vue.extend({ case 'reacted': { const reaction = body.reaction; + // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) + let n = { + ...this.appearNote, + }; + if (body.emoji) { - if (!this.emojis.includes(body.emoji)) { - this.emojis.push(body.emoji); + const emojis = this.appearNote.emojis || []; + if (!emojis.includes(body.emoji)) { + n.emojis = [...emojis, body.emoji]; } } - if (this.reactions[reaction] == null) { - Vue.set(this.reactions, reaction, 0); - } + // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる + const currentCount = (this.appearNote.reactions || {})[reaction] || 0; // Increment the count - this.reactions[reaction]++; + n.reactions = { + ...this.appearNote.reactions, + [reaction]: currentCount + 1 + }; if (body.userId === this.$store.state.i.id) { - this.myReaction = reaction; + n.myReaction = reaction; } + + this.updateAppearNote(n); break; } case 'unreacted': { const reaction = body.reaction; - if (this.reactions[reaction] == null) { - return; - } + // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) + let n = { + ...this.appearNote, + }; + + // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる + const currentCount = (this.appearNote.reactions || {})[reaction] || 0; // Decrement the count - if (this.reactions[reaction] > 0) this.reactions[reaction]--; + n.reactions = { + ...this.appearNote.reactions, + [reaction]: Math.max(0, currentCount - 1) + }; if (body.userId === this.$store.state.i.id) { - this.myReaction = null; + n.myReaction = null; } + + this.updateAppearNote(n); break; } case 'pollVoted': { const choice = body.choice; - this.appearNote.poll.choices[choice].votes++; - if (body.userId === this.$store.state.i.id) { - Vue.set(this.appearNote.poll.choices[choice], 'isVoted', true); - } + + // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) + let n = { + ...this.appearNote, + }; + + n.poll = { + ...this.appearNote.poll, + choices: { + ...this.appearNote.poll.choices, + [choice]: { + ...this.appearNote.poll.choices[choice], + votes: this.appearNote.poll.choices[choice].votes + 1, + ...(body.userId === this.$store.state.i.id ? { + isVoted: true + } : {}) + } + } + }; + + this.updateAppearNote(n); break; } @@ -438,11 +490,11 @@ export default Vue.extend({ }); }, - undoReact() { - const oldReaction = this.myReaction; + undoReact(note) { + const oldReaction = note.myReaction; if (!oldReaction) return; this.$root.api('notes/reactions/delete', { - noteId: this.appearNote.id + noteId: note.id }); }, diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index 7653062ba0..2ae8f696f6 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -15,7 +15,7 @@ </div> <x-list ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> - <x-note :note="note" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/> + <x-note :note="note" @updated="updated(note, $event)" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/> </x-list> <div v-show="more && !reversed" style="margin-top: var(--margin);"> @@ -62,14 +62,15 @@ export default Vue.extend({ default: false }, - extract: { + prop: { + type: String, required: false } }, computed: { notes(): any[] { - return this.extract ? this.extract(this.items) : this.items; + return this.prop ? this.items.map(item => item[this.prop]) : this.items; }, reversed(): boolean { @@ -78,6 +79,15 @@ export default Vue.extend({ }, methods: { + updated(oldValue, newValue) { + const i = this.notes.findIndex(n => n === oldValue); + if (this.prop) { + Vue.set(this.items[i], this.prop, newValue); + } else { + Vue.set(this.items, i, newValue); + } + }, + focus() { this.$refs.notes.focus(); } diff --git a/src/client/components/reactions-viewer.reaction.vue b/src/client/components/reactions-viewer.reaction.vue index 97d019d17f..639a1603ca 100644 --- a/src/client/components/reactions-viewer.reaction.vue +++ b/src/client/components/reactions-viewer.reaction.vue @@ -1,7 +1,7 @@ <template> <button class="hkzvhatu _button" - :class="{ reacted: myReaction == reaction, canToggle }" + :class="{ reacted: note.myReaction == reaction, canToggle }" @click="toggleReaction(reaction)" v-if="count > 0" @touchstart="onMouseover" @@ -11,7 +11,7 @@ ref="reaction" v-particle="canToggle" > - <x-reaction-icon :reaction="reaction" :custom-emojis="emojis" ref="icon"/> + <x-reaction-icon :reaction="reaction" :custom-emojis="note.emojis" ref="icon"/> <span>{{ count }}</span> </button> </template> @@ -30,14 +30,6 @@ export default Vue.extend({ type: String, required: true, }, - myReaction: { - type: String, - required: false, - }, - emojis: { - type: Array, - required: true, - }, count: { type: Number, required: true, @@ -79,7 +71,7 @@ export default Vue.extend({ toggleReaction() { if (!this.canToggle) return; - const oldReaction = this.myReaction; + const oldReaction = this.note.myReaction; if (oldReaction) { this.$root.api('notes/reactions/delete', { noteId: this.note.id diff --git a/src/client/components/reactions-viewer.vue b/src/client/components/reactions-viewer.vue index 353e72ccfa..88e7df4646 100644 --- a/src/client/components/reactions-viewer.vue +++ b/src/client/components/reactions-viewer.vue @@ -1,6 +1,6 @@ <template> <div class="tdflqwzn" :class="{ isMe }"> - <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"/> + <x-reaction v-for="(count, reaction) in note.reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :key="reaction"/> </div> </template> @@ -12,28 +12,16 @@ export default Vue.extend({ components: { XReaction }, + data() { + return { + initialReactions: new Set(Object.keys(this.note.reactions)) + }; + }, props: { note: { type: Object, 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: { isMe(): boolean { diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue index 5fd55e8ca2..28ff6ab1b3 100644 --- a/src/client/components/timeline.vue +++ b/src/client/components/timeline.vue @@ -52,7 +52,6 @@ export default Vue.extend({ }); const prepend = note => { - Object.freeze(note); (this.$refs.tl as any).prepend(note); this.$emit('note'); diff --git a/src/client/pages/favorites.vue b/src/client/pages/favorites.vue index 59bef2ca91..0e625f84cf 100644 --- a/src/client/pages/favorites.vue +++ b/src/client/pages/favorites.vue @@ -2,7 +2,7 @@ <div> <portal to="icon"><fa :icon="faStar"/></portal> <portal to="title">{{ $t('favorites') }}</portal> - <x-notes :pagination="pagination" :detail="true" :extract="items => items.map(item => item.note)" @before="before()" @after="after()"/> + <x-notes :pagination="pagination" :detail="true" :prop="'note'" @before="before()" @after="after()"/> </div> </template> diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue index d21f8d455e..3aedcb65af 100644 --- a/src/client/pages/instance/index.vue +++ b/src/client/pages/instance/index.vue @@ -436,7 +436,7 @@ export default Vue.extend({ }, onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) { + for (const stats of [...statsLog].reverse()) { this.onStats(stats); } } diff --git a/src/client/pages/instance/queue.queue.vue b/src/client/pages/instance/queue.queue.vue index 1649d1e172..c2aa545fc0 100644 --- a/src/client/pages/instance/queue.queue.vue +++ b/src/client/pages/instance/queue.queue.vue @@ -169,7 +169,7 @@ export default Vue.extend({ }, onStatsLog(statsLog) { - for (const stats of statsLog.reverse()) { + for (const stats of [...statsLog].reverse()) { this.onStats(stats); } }, diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue index 5464875dfb..3f42516323 100644 --- a/src/client/pages/note.vue +++ b/src/client/pages/note.vue @@ -14,7 +14,7 @@ <hr v-if="showNext"/> <mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/> - <x-note :note="note" :key="note.id" :detail="true"/> + <x-note v-model="note" :key="note.id" :detail="true"/> <button class="_panel _button" v-if="hasPrev && !showPrev" @click="showPrev = true" style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></button> <hr v-if="showPrev"/> diff --git a/src/client/scripts/stream.ts b/src/client/scripts/stream.ts index 4dcd3f1b2e..8a525ba002 100644 --- a/src/client/scripts/stream.ts +++ b/src/client/scripts/stream.ts @@ -112,10 +112,10 @@ export default class Stream extends EventEmitter { } for (const c of connections.filter(c => c != null)) { - c.emit(body.type, body.body); + c.emit(body.type, Object.freeze(body.body)); } } else { - this.emit(type, body); + this.emit(type, Object.freeze(body)); } }