refactor(client): Use v-model for note component, freeze object

Related: #6595
This commit is contained in:
syuilo 2020-07-27 23:25:37 +09:00
parent cf43dd6ec5
commit 14b7f05af4
10 changed files with 111 additions and 70 deletions

View file

@ -40,14 +40,14 @@
<x-note-header class="header" :note="appearNote" :mini="true"/> <x-note-header class="header" :note="appearNote" :mini="true"/>
<div class="body" 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="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"/> <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="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> <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">
@ -59,7 +59,7 @@
</div> </div>
</div> </div>
<footer class="footer"> <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"> <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>
@ -71,10 +71,10 @@
<button v-else class="button _button"> <button v-else class="button _button">
<fa :icon="faBan"/> <fa :icon="faBan"/>
</button> </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"/> <fa :icon="faPlus"/>
</button> </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"/> <fa :icon="faMinus"/>
</button> </button>
<button class="button _button" @click="menu()" ref="menuButton"> <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'; import { checkWordMute } from '../scripts/check-word-mute';
export default Vue.extend({ export default Vue.extend({
model: {
prop: 'note',
event: 'updated'
},
components: { components: {
XSub, XSub,
XNoteHeader, XNoteHeader,
@ -152,9 +157,6 @@ export default Vue.extend({
showContent: false, showContent: false,
isDeleted: false, isDeleted: false,
muted: false, muted: 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
}; };
@ -211,7 +213,9 @@ export default Vue.extend({
}, },
reactionsCount(): number { reactionsCount(): number {
return sum(Object.values(this.reactions)); return this.appearNote.reactions
? sum(Object.values(this.appearNote.reactions))
: 0;
}, },
urls(): string[] { urls(): string[] {
@ -242,9 +246,8 @@ export default Vue.extend({
this.connection = this.$root.stream; this.connection = this.$root.stream;
} }
this.emojis = [...this.appearNote.emojis]; console.log(this.note);
this.reactions = { ...this.appearNote.reactions };
this.myReaction = this.appearNote.myReaction;
this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords); this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
if (this.detail) { if (this.detail) {
@ -284,6 +287,19 @@ export default Vue.extend({
}, },
methods: { methods: {
updateAppearNote(v) {
this.$emit('updated', Object.freeze(this.isRenote ? {
...this.note,
renote: {
...this.note.renote,
...v
}
} : {
...this.note,
...v
}));
},
readPromo() { readPromo() {
(this as any).$root.api('promo/read', { (this as any).$root.api('promo/read', {
noteId: this.appearNote.id noteId: this.appearNote.id
@ -320,47 +336,83 @@ export default Vue.extend({
case 'reacted': { case 'reacted': {
const reaction = body.reaction; const reaction = body.reaction;
// DeepShallown.reactions[reaction] = hoge()
let n = {
...this.appearNote,
};
if (body.emoji) { if (body.emoji) {
if (!this.emojis.includes(body.emoji)) { const emojis = this.appearNote.emojis || [];
this.emojis.push(body.emoji); if (!emojis.includes(body.emoji)) {
n.emojis = [...emojis, body.emoji];
} }
} }
if (this.reactions[reaction] == null) { // TODO: reactions || {}
Vue.set(this.reactions, reaction, 0); const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
}
// Increment the count // Increment the count
this.reactions[reaction]++; n.reactions = {
...this.appearNote.reactions,
[reaction]: currentCount + 1
};
if (body.userId === this.$store.state.i.id) { if (body.userId === this.$store.state.i.id) {
this.myReaction = reaction; n.myReaction = reaction;
} }
this.updateAppearNote(n);
break; break;
} }
case 'unreacted': { case 'unreacted': {
const reaction = body.reaction; const reaction = body.reaction;
if (this.reactions[reaction] == null) { // DeepShallown.reactions[reaction] = hoge()
return; let n = {
} ...this.appearNote,
};
// TODO: reactions || {}
const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
// Decrement the count // 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) { if (body.userId === this.$store.state.i.id) {
this.myReaction = null; n.myReaction = null;
} }
this.updateAppearNote(n);
break; break;
} }
case 'pollVoted': { case 'pollVoted': {
const choice = body.choice; const choice = body.choice;
this.appearNote.poll.choices[choice].votes++;
if (body.userId === this.$store.state.i.id) { // DeepShallown.reactions[reaction] = hoge()
Vue.set(this.appearNote.poll.choices[choice], 'isVoted', true); 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; break;
} }
@ -438,11 +490,11 @@ export default Vue.extend({
}); });
}, },
undoReact() { undoReact(note) {
const oldReaction = this.myReaction; const oldReaction = note.myReaction;
if (!oldReaction) return; if (!oldReaction) return;
this.$root.api('notes/reactions/delete', { this.$root.api('notes/reactions/delete', {
noteId: this.appearNote.id noteId: note.id
}); });
}, },

View file

@ -15,7 +15,7 @@
</div> </div>
<x-list ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> <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> </x-list>
<div v-show="more && !reversed" style="margin-top: var(--margin);"> <div v-show="more && !reversed" style="margin-top: var(--margin);">
@ -62,14 +62,15 @@ export default Vue.extend({
default: false default: false
}, },
extract: { prop: {
type: String,
required: false required: false
} }
}, },
computed: { computed: {
notes(): any[] { 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 { reversed(): boolean {
@ -78,6 +79,15 @@ export default Vue.extend({
}, },
methods: { 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() { focus() {
this.$refs.notes.focus(); this.$refs.notes.focus();
} }

View file

@ -1,7 +1,7 @@
<template> <template>
<button <button
class="hkzvhatu _button" class="hkzvhatu _button"
:class="{ reacted: myReaction == reaction, canToggle }" :class="{ reacted: note.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="emojis" ref="icon"/> <x-reaction-icon :reaction="reaction" :custom-emojis="note.emojis" ref="icon"/>
<span>{{ count }}</span> <span>{{ count }}</span>
</button> </button>
</template> </template>
@ -30,14 +30,6 @@ 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,
@ -79,7 +71,7 @@ export default Vue.extend({
toggleReaction() { toggleReaction() {
if (!this.canToggle) return; if (!this.canToggle) return;
const oldReaction = this.myReaction; const oldReaction = this.note.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 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> </div>
</template> </template>
@ -12,28 +12,16 @@ 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,7 +52,6 @@ export default Vue.extend({
}); });
const prepend = note => { const prepend = note => {
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

@ -2,7 +2,7 @@
<div> <div>
<portal to="icon"><fa :icon="faStar"/></portal> <portal to="icon"><fa :icon="faStar"/></portal>
<portal to="title">{{ $t('favorites') }}</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> </div>
</template> </template>

View file

@ -436,7 +436,7 @@ export default Vue.extend({
}, },
onStatsLog(statsLog) { onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) { for (const stats of [...statsLog].reverse()) {
this.onStats(stats); this.onStats(stats);
} }
} }

View file

@ -169,7 +169,7 @@ export default Vue.extend({
}, },
onStatsLog(statsLog) { onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) { for (const stats of [...statsLog].reverse()) {
this.onStats(stats); this.onStats(stats);
} }
}, },

View file

@ -14,7 +14,7 @@
<hr v-if="showNext"/> <hr v-if="showNext"/>
<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/> <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> <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"/> <hr v-if="showPrev"/>

View file

@ -112,10 +112,10 @@ export default class Stream extends EventEmitter {
} }
for (const c of connections.filter(c => c != null)) { for (const c of connections.filter(c => c != null)) {
c.emit(body.type, body.body); c.emit(body.type, Object.freeze(body.body));
} }
} else { } else {
this.emit(type, body); this.emit(type, Object.freeze(body));
} }
} }